mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 12:53:03 -05:00
feat(challenge30): interop rxjs signal
This commit is contained in:
@@ -24,7 +24,6 @@ export class AppComponent implements OnInit {
|
||||
this.http
|
||||
.get<any[]>('https://jsonplaceholder.typicode.com/todos')
|
||||
.subscribe((todos) => {
|
||||
console.log('return', todos);
|
||||
this.todos = todos;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
<h1>migrate application to signals</h1>
|
||||
<h1>interoperability Rxjs and Signal</h1>
|
||||
|
||||
> Author: Thomas Laforge
|
||||
|
||||
<!-- TODO: add Information/Statement/Rules/Constraint/Steps -->
|
||||
|
||||
### 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.
|
||||
|
||||
<a href="https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A30+label%3Aanswer"><img src="https://img.shields.io/badge/-Solutions-green" alt="rxjs-to-signal"/></a>
|
||||
<a href='https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A30+label%3A"answer+author"'><img src="https://img.shields.io/badge/-Author solution-important" alt="rxjs-to-signal solution author"/></a>
|
||||
<a href="https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A30+label%3Aanswer"><img src="https://img.shields.io/badge/-Solutions-green" alt="interop-rxjs-signal"/></a>
|
||||
<a href='https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A30+label%3A"answer+author"'><img src="https://img.shields.io/badge/-Author solution-important" alt="interop-rxjs-signal solution author"/></a>
|
||||
|
||||
<!-- <a href="{Blog post url}" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/-Blog post explanation-blue" alt="rxjs-to-signal blog article"/></a> -->
|
||||
<!-- <a href="{Blog post url}" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/-Blog post explanation-blue" alt="interop-rxjs-signal blog article"/></a> -->
|
||||
|
||||
_You can ask any question on_ <a href="https://twitter.com/laforge_toma" target="_blank" rel="noopener noreferrer"><img src="./../../logo/twitter.svg" height=20px alt="twitter"/></a>
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'rxjs-to-signal',
|
||||
displayName: 'interop-rxjs-signal',
|
||||
preset: '../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../coverage/apps/rxjs-to-signal',
|
||||
coverageDirectory: '../../coverage/apps/interop-rxjs-signal',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
@@ -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": {
|
||||
33
apps/interop-rxjs-signal/src/app/detail/detail.component.ts
Normal file
33
apps/interop-rxjs-signal/src/app/detail/detail.component.ts
Normal file
@@ -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: `
|
||||
<img src="{{ photo.url_m }}" alt="{{ photo.title }}" class="image" />
|
||||
<p><span class="font-bold">Title:</span> {{ photo.title }}</p>
|
||||
<p><span class="font-bold">Owner:</span> {{ photo.ownername }}</p>
|
||||
<p><span class="font-bold">Date:</span> {{ photo.datetaken | date }}</p>
|
||||
<p><span class="font-bold">Tags:</span> {{ photo.tags }}</p>
|
||||
|
||||
<button
|
||||
class="border border-black rounded-md px-4 py-2 mt-10"
|
||||
routerLink="">
|
||||
Back
|
||||
</button>
|
||||
`,
|
||||
host: {
|
||||
class: 'p-5 block',
|
||||
},
|
||||
})
|
||||
export default class DetailComponent {
|
||||
@RouterInput({
|
||||
required: true,
|
||||
transform: (value: string) => JSON.parse(decodeURIComponent(value)),
|
||||
})
|
||||
photo!: Photo;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
<ng-container *ngrxLet="vm$ as vm">
|
||||
<section class="flex flex-col">
|
||||
<mat-progress-bar
|
||||
mode="query"
|
||||
*ngIf="vm.loading"
|
||||
class="mt-5"></mat-progress-bar>
|
||||
<section class="flex gap-3 items-center">
|
||||
<button
|
||||
[disabled]="vm.page === 1"
|
||||
@@ -59,6 +55,10 @@ import { PhotoStore } from './photos.store';
|
||||
</button>
|
||||
Page :{{ vm.page }} / {{ vm.pages }}
|
||||
</section>
|
||||
<mat-progress-bar
|
||||
mode="query"
|
||||
*ngIf="vm.loading"
|
||||
class="mt-5"></mat-progress-bar>
|
||||
<ul
|
||||
class="flex flex-wrap gap-4"
|
||||
*ngIf="vm.photos && vm.photos.length > 0; else noPhoto">
|
||||
@@ -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()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
)
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -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: `
|
||||
<img src="{{ photo.url_q }}" alt="{{ photo.title }}" class="image" />
|
||||
{{ 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;
|
||||
// }
|
||||
}
|
||||
Reference in New Issue
Block a user