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:
@@ -45,6 +45,7 @@ If you would like to propose a challenge, this project is open source, so feel f
|
|||||||
<a href="./apps/di/README.md"><img src="https://img.shields.io/badge/16-di-red" alt="di"/></a>
|
<a href="./apps/di/README.md"><img src="https://img.shields.io/badge/16-di-red" alt="di"/></a>
|
||||||
<a href="./apps/anchor-scrolling/README.md"><img src="https://img.shields.io/badge/21-anchor--scrolling-green" alt="anchor-scrolling"/></a>
|
<a href="./apps/anchor-scrolling/README.md"><img src="https://img.shields.io/badge/21-anchor--scrolling-green" alt="anchor-scrolling"/></a>
|
||||||
<a href="./apps/router-input/README.md"><img src="https://img.shields.io/badge/22-router--input-green" alt="router-input"/></a>
|
<a href="./apps/router-input/README.md"><img src="https://img.shields.io/badge/22-router--input-green" alt="router-input"/></a>
|
||||||
|
<a href="./apps/interop-rxjs-signal/README.md"><img src="https://img.shields.io/badge/22-interop rxjs signal-red" alt="interop signal rxjs"/></a>
|
||||||
|
|
||||||
</br>
|
</br>
|
||||||
<img src="https://img.shields.io/badge/Typescript--gray?logo=typescript" alt="Typescript"/>
|
<img src="https://img.shields.io/badge/Typescript--gray?logo=typescript" alt="Typescript"/>
|
||||||
@@ -92,7 +93,7 @@ If you would like to propose a challenge, this project is open source, so feel f
|
|||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@thomas.laforge"><img src="https://avatars.githubusercontent.com/u/30832608?s…00&u=6f0ad9676792f29fd7fe6e113df06213d384a813&v=4" width="100px;" alt="Thomas Laforge"/><br /><sub><b>Thomas Laforge</b></sub></a><br />29 🧩</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@thomas.laforge"><img src="https://avatars.githubusercontent.com/u/30832608?s…00&u=6f0ad9676792f29fd7fe6e113df06213d384a813&v=4" width="100px;" alt="Thomas Laforge"/><br /><sub><b>Thomas Laforge</b></sub></a><br />30 🧩</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export class AppComponent implements OnInit {
|
|||||||
this.http
|
this.http
|
||||||
.get<any[]>('https://jsonplaceholder.typicode.com/todos')
|
.get<any[]>('https://jsonplaceholder.typicode.com/todos')
|
||||||
.subscribe((todos) => {
|
.subscribe((todos) => {
|
||||||
console.log('return', todos);
|
|
||||||
this.todos = todos;
|
this.todos = todos;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
<h1>migrate application to signals</h1>
|
<h1>interoperability Rxjs and Signal</h1>
|
||||||
|
|
||||||
> Author: Thomas Laforge
|
> Author: Thomas Laforge
|
||||||
|
|
||||||
<!-- TODO: add Information/Statement/Rules/Constraint/Steps -->
|
|
||||||
|
|
||||||
### Information
|
### Information
|
||||||
|
|
||||||
### Statement
|
In this challenge, we have a small reactive application using RxJS and NgRx/Component-Store.
|
||||||
|
|
||||||
### Step 1
|
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.
|
||||||
|
|
||||||
### Step 2
|
|
||||||
|
|
||||||
### Constraints:
|
|
||||||
|
|
||||||
### Submitting your work
|
### Submitting your work
|
||||||
|
|
||||||
1. Fork the project
|
1. Fork the project
|
||||||
2. clone it
|
2. clone it
|
||||||
3. npm ci
|
3. npm ci
|
||||||
4. `npx nx serve rxjs-to-signal`
|
4. `npx nx serve interop-rxjs-signal`
|
||||||
5. _...work on it_
|
5. _...work on it_
|
||||||
6. Commit your work
|
6. Commit your work
|
||||||
7. Submit a PR with a title beginning with **Answer:30** that I will review and other dev can review.
|
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%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="rxjs-to-signal solution author"/></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>
|
_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 */
|
/* eslint-disable */
|
||||||
export default {
|
export default {
|
||||||
displayName: 'rxjs-to-signal',
|
displayName: 'interop-rxjs-signal',
|
||||||
preset: '../../jest.preset.js',
|
preset: '../../jest.preset.js',
|
||||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||||
coverageDirectory: '../../coverage/apps/rxjs-to-signal',
|
coverageDirectory: '../../coverage/apps/interop-rxjs-signal',
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(ts|mjs|js|html)$': [
|
'^.+\\.(ts|mjs|js|html)$': [
|
||||||
'jest-preset-angular',
|
'jest-preset-angular',
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "rxjs-to-signal",
|
"name": "interop-rxjs-signal",
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"sourceRoot": "apps/rxjs-to-signal/src",
|
"sourceRoot": "apps/interop-rxjs-signal/src",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"targets": {
|
"targets": {
|
||||||
"build": {
|
"build": {
|
||||||
"executor": "@angular-devkit/build-angular:browser",
|
"executor": "@angular-devkit/build-angular:browser",
|
||||||
"outputs": ["{options.outputPath}"],
|
"outputs": ["{options.outputPath}"],
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/apps/rxjs-to-signal",
|
"outputPath": "dist/apps/interop-rxjs-signal",
|
||||||
"index": "apps/rxjs-to-signal/src/index.html",
|
"index": "apps/interop-rxjs-signal/src/index.html",
|
||||||
"main": "apps/rxjs-to-signal/src/main.ts",
|
"main": "apps/interop-rxjs-signal/src/main.ts",
|
||||||
"polyfills": ["zone.js"],
|
"polyfills": ["zone.js"],
|
||||||
"tsConfig": "apps/rxjs-to-signal/tsconfig.app.json",
|
"tsConfig": "apps/interop-rxjs-signal/tsconfig.app.json",
|
||||||
"assets": [
|
"assets": [
|
||||||
"apps/rxjs-to-signal/src/favicon.ico",
|
"apps/interop-rxjs-signal/src/favicon.ico",
|
||||||
"apps/rxjs-to-signal/src/assets"
|
"apps/interop-rxjs-signal/src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"apps/rxjs-to-signal/src/styles.scss",
|
"apps/interop-rxjs-signal/src/styles.scss",
|
||||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
|
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
@@ -56,10 +56,10 @@
|
|||||||
"executor": "@angular-devkit/build-angular:dev-server",
|
"executor": "@angular-devkit/build-angular:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "rxjs-to-signal:build:production"
|
"browserTarget": "interop-rxjs-signal:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"browserTarget": "rxjs-to-signal:build:development"
|
"browserTarget": "interop-rxjs-signal:build:development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "rxjs-to-signal:build"
|
"browserTarget": "interop-rxjs-signal:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
@@ -75,8 +75,8 @@
|
|||||||
"outputs": ["{options.outputFile}"],
|
"outputs": ["{options.outputFile}"],
|
||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": [
|
"lintFilePatterns": [
|
||||||
"apps/rxjs-to-signal/**/*.ts",
|
"apps/interop-rxjs-signal/**/*.ts",
|
||||||
"apps/rxjs-to-signal/**/*.html"
|
"apps/interop-rxjs-signal/**/*.html"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
"executor": "@nx/jest:jest",
|
"executor": "@nx/jest:jest",
|
||||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
"options": {
|
"options": {
|
||||||
"jestConfig": "apps/rxjs-to-signal/jest.config.ts",
|
"jestConfig": "apps/interop-rxjs-signal/jest.config.ts",
|
||||||
"passWithNoTests": true
|
"passWithNoTests": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"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 { RouterLinkWithHref } from '@angular/router';
|
||||||
import { LetDirective } from '@ngrx/component';
|
import { LetDirective } from '@ngrx/component';
|
||||||
import { provideComponentStore } from '@ngrx/component-store';
|
import { provideComponentStore } from '@ngrx/component-store';
|
||||||
import { debounceTime, distinctUntilChanged } from 'rxjs';
|
import { debounceTime, distinctUntilChanged, skipWhile, tap } from 'rxjs';
|
||||||
import { Photo } from '../photo.model';
|
import { Photo } from '../photo.model';
|
||||||
import { PhotoStore } from './photos.store';
|
import { PhotoStore } from './photos.store';
|
||||||
|
|
||||||
@@ -38,10 +38,6 @@ import { PhotoStore } from './photos.store';
|
|||||||
|
|
||||||
<ng-container *ngrxLet="vm$ as vm">
|
<ng-container *ngrxLet="vm$ as vm">
|
||||||
<section class="flex flex-col">
|
<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">
|
<section class="flex gap-3 items-center">
|
||||||
<button
|
<button
|
||||||
[disabled]="vm.page === 1"
|
[disabled]="vm.page === 1"
|
||||||
@@ -59,6 +55,10 @@ import { PhotoStore } from './photos.store';
|
|||||||
</button>
|
</button>
|
||||||
Page :{{ vm.page }} / {{ vm.pages }}
|
Page :{{ vm.page }} / {{ vm.pages }}
|
||||||
</section>
|
</section>
|
||||||
|
<mat-progress-bar
|
||||||
|
mode="query"
|
||||||
|
*ngIf="vm.loading"
|
||||||
|
class="mt-5"></mat-progress-bar>
|
||||||
<ul
|
<ul
|
||||||
class="flex flex-wrap gap-4"
|
class="flex flex-wrap gap-4"
|
||||||
*ngIf="vm.photos && vm.photos.length > 0; else noPhoto">
|
*ngIf="vm.photos && vm.photos.length > 0; else noPhoto">
|
||||||
@@ -87,13 +87,25 @@ import { PhotoStore } from './photos.store';
|
|||||||
})
|
})
|
||||||
export default class PhotosComponent implements OnInit {
|
export default class PhotosComponent implements OnInit {
|
||||||
store = inject(PhotoStore);
|
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();
|
search = new FormControl();
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.store.search(
|
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 { Photo } from '../photo.model';
|
||||||
import { PhotoService } from '../photos.service';
|
import { PhotoService } from '../photos.service';
|
||||||
|
|
||||||
|
const PHOTO_STATE_KEY = 'photo_search';
|
||||||
|
|
||||||
export interface PhotoState {
|
export interface PhotoState {
|
||||||
photos: Photo[];
|
photos: Photo[];
|
||||||
search: string;
|
search: string;
|
||||||
@@ -51,6 +53,7 @@ export class PhotoStore
|
|||||||
readonly vm$ = this.select(
|
readonly vm$ = this.select(
|
||||||
{
|
{
|
||||||
photos: this.photos$,
|
photos: this.photos$,
|
||||||
|
search: this.search$,
|
||||||
page: this.page$,
|
page: this.page$,
|
||||||
pages: this.pages$,
|
pages: this.pages$,
|
||||||
endOfPage: this.endOfPage$,
|
endOfPage: this.endOfPage$,
|
||||||
@@ -61,7 +64,17 @@ export class PhotoStore
|
|||||||
);
|
);
|
||||||
|
|
||||||
ngrxOnStoreInit() {
|
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() {
|
ngrxOnStateInit() {
|
||||||
@@ -108,6 +121,10 @@ export class PhotoStore
|
|||||||
photos: photo,
|
photos: photo,
|
||||||
pages,
|
pages,
|
||||||
});
|
});
|
||||||
|
localStorage.setItem(
|
||||||
|
PHOTO_STATE_KEY,
|
||||||
|
JSON.stringify({ search, page })
|
||||||
|
);
|
||||||
},
|
},
|
||||||
(error: unknown) => this.patchState({ error, loading: false })
|
(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