diff --git a/README.md b/README.md index 2503690..34e1d4d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ If you would like to propose a challenge, this project is open source, so feel f di anchor-scrolling router-input +interop signal rxjs
Typescript @@ -92,7 +93,7 @@ If you would like to propose a challenge, this project is open source, so feel f - +
Thomas Laforge
Thomas Laforge

29 🧩
Thomas Laforge
Thomas Laforge

30 🧩
diff --git a/apps/crud/src/app/app.component.ts b/apps/crud/src/app/app.component.ts index 3a6584e..400d403 100644 --- a/apps/crud/src/app/app.component.ts +++ b/apps/crud/src/app/app.component.ts @@ -24,7 +24,6 @@ export class AppComponent implements OnInit { this.http .get('https://jsonplaceholder.typicode.com/todos') .subscribe((todos) => { - console.log('return', todos); this.todos = todos; }); } diff --git a/apps/rxjs-to-signal/.eslintrc.json b/apps/interop-rxjs-signal/.eslintrc.json similarity index 100% rename from apps/rxjs-to-signal/.eslintrc.json rename to apps/interop-rxjs-signal/.eslintrc.json diff --git a/apps/rxjs-to-signal/README.md b/apps/interop-rxjs-signal/README.md similarity index 52% rename from apps/rxjs-to-signal/README.md rename to apps/interop-rxjs-signal/README.md index 37ff9a2..d6a6a51 100644 --- a/apps/rxjs-to-signal/README.md +++ b/apps/interop-rxjs-signal/README.md @@ -1,32 +1,26 @@ -

migrate application to signals

+

interoperability Rxjs and Signal

> Author: Thomas Laforge - - ### Information -### Statement +In this challenge, we have a small reactive application using RxJS and NgRx/Component-Store. -### Step 1 - -### Step 2 - -### Constraints: +The goal of this challenge is to use the new **Signal API** introduced in Angular v16. However, we should not convert everything. Certain portions of the code are better suited for RxJS rather than Signal. It is up to you to determine the threshold and observe how **Signal and RxJS coexist**, as well as how the interoperability is achieved in Angular. ### Submitting your work 1. Fork the project 2. clone it 3. npm ci -4. `npx nx serve rxjs-to-signal` +4. `npx nx serve interop-rxjs-signal` 5. _...work on it_ 6. Commit your work 7. Submit a PR with a title beginning with **Answer:30** that I will review and other dev can review. -rxjs-to-signal -rxjs-to-signal solution author +interop-rxjs-signal +interop-rxjs-signal solution author - + _You can ask any question on_ twitter diff --git a/apps/rxjs-to-signal/jest.config.ts b/apps/interop-rxjs-signal/jest.config.ts similarity index 85% rename from apps/rxjs-to-signal/jest.config.ts rename to apps/interop-rxjs-signal/jest.config.ts index e0adc03..cfc3de3 100644 --- a/apps/rxjs-to-signal/jest.config.ts +++ b/apps/interop-rxjs-signal/jest.config.ts @@ -1,9 +1,9 @@ /* eslint-disable */ export default { - displayName: 'rxjs-to-signal', + displayName: 'interop-rxjs-signal', preset: '../../jest.preset.js', setupFilesAfterEnv: ['/src/test-setup.ts'], - coverageDirectory: '../../coverage/apps/rxjs-to-signal', + coverageDirectory: '../../coverage/apps/interop-rxjs-signal', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', diff --git a/apps/rxjs-to-signal/project.json b/apps/interop-rxjs-signal/project.json similarity index 71% rename from apps/rxjs-to-signal/project.json rename to apps/interop-rxjs-signal/project.json index 741d5ed..f101002 100644 --- a/apps/rxjs-to-signal/project.json +++ b/apps/interop-rxjs-signal/project.json @@ -1,26 +1,26 @@ { - "name": "rxjs-to-signal", + "name": "interop-rxjs-signal", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "prefix": "app", - "sourceRoot": "apps/rxjs-to-signal/src", + "sourceRoot": "apps/interop-rxjs-signal/src", "tags": [], "targets": { "build": { "executor": "@angular-devkit/build-angular:browser", "outputs": ["{options.outputPath}"], "options": { - "outputPath": "dist/apps/rxjs-to-signal", - "index": "apps/rxjs-to-signal/src/index.html", - "main": "apps/rxjs-to-signal/src/main.ts", + "outputPath": "dist/apps/interop-rxjs-signal", + "index": "apps/interop-rxjs-signal/src/index.html", + "main": "apps/interop-rxjs-signal/src/main.ts", "polyfills": ["zone.js"], - "tsConfig": "apps/rxjs-to-signal/tsconfig.app.json", + "tsConfig": "apps/interop-rxjs-signal/tsconfig.app.json", "assets": [ - "apps/rxjs-to-signal/src/favicon.ico", - "apps/rxjs-to-signal/src/assets" + "apps/interop-rxjs-signal/src/favicon.ico", + "apps/interop-rxjs-signal/src/assets" ], "styles": [ - "apps/rxjs-to-signal/src/styles.scss", + "apps/interop-rxjs-signal/src/styles.scss", "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css" ], "scripts": [] @@ -56,10 +56,10 @@ "executor": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "browserTarget": "rxjs-to-signal:build:production" + "browserTarget": "interop-rxjs-signal:build:production" }, "development": { - "browserTarget": "rxjs-to-signal:build:development" + "browserTarget": "interop-rxjs-signal:build:development" } }, "defaultConfiguration": "development" @@ -67,7 +67,7 @@ "extract-i18n": { "executor": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "rxjs-to-signal:build" + "browserTarget": "interop-rxjs-signal:build" } }, "lint": { @@ -75,8 +75,8 @@ "outputs": ["{options.outputFile}"], "options": { "lintFilePatterns": [ - "apps/rxjs-to-signal/**/*.ts", - "apps/rxjs-to-signal/**/*.html" + "apps/interop-rxjs-signal/**/*.ts", + "apps/interop-rxjs-signal/**/*.html" ] } }, @@ -84,7 +84,7 @@ "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { - "jestConfig": "apps/rxjs-to-signal/jest.config.ts", + "jestConfig": "apps/interop-rxjs-signal/jest.config.ts", "passWithNoTests": true }, "configurations": { diff --git a/apps/rxjs-to-signal/src/app/app.component.ts b/apps/interop-rxjs-signal/src/app/app.component.ts similarity index 100% rename from apps/rxjs-to-signal/src/app/app.component.ts rename to apps/interop-rxjs-signal/src/app/app.component.ts diff --git a/apps/rxjs-to-signal/src/app/app.config.ts b/apps/interop-rxjs-signal/src/app/app.config.ts similarity index 100% rename from apps/rxjs-to-signal/src/app/app.config.ts rename to apps/interop-rxjs-signal/src/app/app.config.ts diff --git a/apps/interop-rxjs-signal/src/app/detail/detail.component.ts b/apps/interop-rxjs-signal/src/app/detail/detail.component.ts new file mode 100644 index 0000000..c1493b2 --- /dev/null +++ b/apps/interop-rxjs-signal/src/app/detail/detail.component.ts @@ -0,0 +1,33 @@ +import { DatePipe } from '@angular/common'; +import { Component, Input as RouterInput } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { Photo } from '../photo.model'; + +@Component({ + selector: 'app-photos', + standalone: true, + imports: [DatePipe, RouterLink], + template: ` + {{ photo.title }} +

Title: {{ photo.title }}

+

Owner: {{ photo.ownername }}

+

Date: {{ photo.datetaken | date }}

+

Tags: {{ photo.tags }}

+ + + `, + host: { + class: 'p-5 block', + }, +}) +export default class DetailComponent { + @RouterInput({ + required: true, + transform: (value: string) => JSON.parse(decodeURIComponent(value)), + }) + photo!: Photo; +} diff --git a/apps/rxjs-to-signal/src/app/list/photos.component.ts b/apps/interop-rxjs-signal/src/app/list/photos.component.ts similarity index 87% rename from apps/rxjs-to-signal/src/app/list/photos.component.ts rename to apps/interop-rxjs-signal/src/app/list/photos.component.ts index 0d8b948..b8705e3 100644 --- a/apps/rxjs-to-signal/src/app/list/photos.component.ts +++ b/apps/interop-rxjs-signal/src/app/list/photos.component.ts @@ -7,7 +7,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar'; import { RouterLinkWithHref } from '@angular/router'; import { LetDirective } from '@ngrx/component'; import { provideComponentStore } from '@ngrx/component-store'; -import { debounceTime, distinctUntilChanged } from 'rxjs'; +import { debounceTime, distinctUntilChanged, skipWhile, tap } from 'rxjs'; import { Photo } from '../photo.model'; import { PhotoStore } from './photos.store'; @@ -38,10 +38,6 @@ import { PhotoStore } from './photos.store';
-
+
    @@ -87,13 +87,25 @@ import { PhotoStore } from './photos.store'; }) export default class PhotosComponent implements OnInit { store = inject(PhotoStore); - readonly vm$ = this.store.vm$; + readonly vm$ = this.store.vm$.pipe( + tap(({ search }) => { + if (!this.formInit) { + this.search.setValue(search); + this.formInit = true; + } + }) + ); + private formInit = false; search = new FormControl(); ngOnInit(): void { this.store.search( - this.search.valueChanges.pipe(debounceTime(300), distinctUntilChanged()) + this.search.valueChanges.pipe( + skipWhile(() => !this.formInit), + debounceTime(300), + distinctUntilChanged() + ) ); } diff --git a/apps/rxjs-to-signal/src/app/list/photos.store.ts b/apps/interop-rxjs-signal/src/app/list/photos.store.ts similarity index 83% rename from apps/rxjs-to-signal/src/app/list/photos.store.ts rename to apps/interop-rxjs-signal/src/app/list/photos.store.ts index 2d720cc..8f039c4 100644 --- a/apps/rxjs-to-signal/src/app/list/photos.store.ts +++ b/apps/interop-rxjs-signal/src/app/list/photos.store.ts @@ -10,6 +10,8 @@ import { filter, mergeMap, tap } from 'rxjs/operators'; import { Photo } from '../photo.model'; import { PhotoService } from '../photos.service'; +const PHOTO_STATE_KEY = 'photo_search'; + export interface PhotoState { photos: Photo[]; search: string; @@ -51,6 +53,7 @@ export class PhotoStore readonly vm$ = this.select( { photos: this.photos$, + search: this.search$, page: this.page$, pages: this.pages$, endOfPage: this.endOfPage$, @@ -61,7 +64,17 @@ export class PhotoStore ); ngrxOnStoreInit() { - this.setState(initialState); + const savedJSONState = localStorage.getItem(PHOTO_STATE_KEY); + if (savedJSONState === null) { + this.setState(initialState); + } else { + const savedState = JSON.parse(savedJSONState); + this.setState({ + ...initialState, + search: savedState.search, + page: savedState.page, + }); + } } ngrxOnStateInit() { @@ -108,6 +121,10 @@ export class PhotoStore photos: photo, pages, }); + localStorage.setItem( + PHOTO_STATE_KEY, + JSON.stringify({ search, page }) + ); }, (error: unknown) => this.patchState({ error, loading: false }) ) diff --git a/apps/rxjs-to-signal/src/app/photo.model.ts b/apps/interop-rxjs-signal/src/app/photo.model.ts similarity index 100% rename from apps/rxjs-to-signal/src/app/photo.model.ts rename to apps/interop-rxjs-signal/src/app/photo.model.ts diff --git a/apps/rxjs-to-signal/src/app/photos.service.ts b/apps/interop-rxjs-signal/src/app/photos.service.ts similarity index 100% rename from apps/rxjs-to-signal/src/app/photos.service.ts rename to apps/interop-rxjs-signal/src/app/photos.service.ts diff --git a/apps/rxjs-to-signal/src/assets/.gitkeep b/apps/interop-rxjs-signal/src/assets/.gitkeep similarity index 100% rename from apps/rxjs-to-signal/src/assets/.gitkeep rename to apps/interop-rxjs-signal/src/assets/.gitkeep diff --git a/apps/rxjs-to-signal/src/favicon.ico b/apps/interop-rxjs-signal/src/favicon.ico similarity index 100% rename from apps/rxjs-to-signal/src/favicon.ico rename to apps/interop-rxjs-signal/src/favicon.ico diff --git a/apps/rxjs-to-signal/src/index.html b/apps/interop-rxjs-signal/src/index.html similarity index 100% rename from apps/rxjs-to-signal/src/index.html rename to apps/interop-rxjs-signal/src/index.html diff --git a/apps/rxjs-to-signal/src/main.ts b/apps/interop-rxjs-signal/src/main.ts similarity index 100% rename from apps/rxjs-to-signal/src/main.ts rename to apps/interop-rxjs-signal/src/main.ts diff --git a/apps/rxjs-to-signal/src/styles.scss b/apps/interop-rxjs-signal/src/styles.scss similarity index 100% rename from apps/rxjs-to-signal/src/styles.scss rename to apps/interop-rxjs-signal/src/styles.scss diff --git a/apps/rxjs-to-signal/src/test-setup.ts b/apps/interop-rxjs-signal/src/test-setup.ts similarity index 100% rename from apps/rxjs-to-signal/src/test-setup.ts rename to apps/interop-rxjs-signal/src/test-setup.ts diff --git a/apps/rxjs-to-signal/tailwind.config.js b/apps/interop-rxjs-signal/tailwind.config.js similarity index 100% rename from apps/rxjs-to-signal/tailwind.config.js rename to apps/interop-rxjs-signal/tailwind.config.js diff --git a/apps/rxjs-to-signal/tsconfig.app.json b/apps/interop-rxjs-signal/tsconfig.app.json similarity index 100% rename from apps/rxjs-to-signal/tsconfig.app.json rename to apps/interop-rxjs-signal/tsconfig.app.json diff --git a/apps/rxjs-to-signal/tsconfig.editor.json b/apps/interop-rxjs-signal/tsconfig.editor.json similarity index 100% rename from apps/rxjs-to-signal/tsconfig.editor.json rename to apps/interop-rxjs-signal/tsconfig.editor.json diff --git a/apps/rxjs-to-signal/tsconfig.json b/apps/interop-rxjs-signal/tsconfig.json similarity index 100% rename from apps/rxjs-to-signal/tsconfig.json rename to apps/interop-rxjs-signal/tsconfig.json diff --git a/apps/rxjs-to-signal/tsconfig.spec.json b/apps/interop-rxjs-signal/tsconfig.spec.json similarity index 100% rename from apps/rxjs-to-signal/tsconfig.spec.json rename to apps/interop-rxjs-signal/tsconfig.spec.json diff --git a/apps/rxjs-to-signal/src/app/detail/detail.component.ts b/apps/rxjs-to-signal/src/app/detail/detail.component.ts deleted file mode 100644 index 63cf622..0000000 --- a/apps/rxjs-to-signal/src/app/detail/detail.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { JsonPipe, NgFor, NgIf } from '@angular/common'; -import { Component, Input } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { LetDirective } from '@ngrx/component'; -import { Photo } from '../photo.model'; - -@Component({ - selector: 'app-photos', - standalone: true, - imports: [ - ReactiveFormsModule, - MatFormFieldModule, - MatProgressBarModule, - NgIf, - NgFor, - MatInputModule, - LetDirective, - JsonPipe, - ], - template: ` - {{ photo.title }} - {{ photo | json }} - `, - // providers: [provideComponentStore(PhotoStore)], - host: { - class: 'p-5 block', - }, -}) -export default class DetailComponent { - @Input({ required: true }) photo!: Photo; - // store = inject(PhotoStore); - // readonly vm$ = this.store.vm$; - - // search = new FormControl(); - - // ngOnInit(): void { - // this.store.search( - // this.search.valueChanges.pipe(debounceTime(300), distinctUntilChanged()) - // ); - // } - - // trackById(index: number, photo: Photo) { - // return photo.id; - // } -}