mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-13 06:13:03 -05:00
feat(doc): move interop rxjs signal
This commit is contained in:
11
apps/angular/interop-rxjs-signal/src/app/app.component.ts
Normal file
11
apps/angular/interop-rxjs-signal/src/app/app.component.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
selector: 'app-root',
|
||||
template: `<router-outlet />`,
|
||||
styles: [''],
|
||||
})
|
||||
export class AppComponent {}
|
||||
29
apps/angular/interop-rxjs-signal/src/app/app.config.ts
Normal file
29
apps/angular/interop-rxjs-signal/src/app/app.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideAnimations(),
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadComponent: () => import('./list/photos.component'),
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
loadComponent: () => import('./detail/detail.component'),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '',
|
||||
},
|
||||
],
|
||||
withComponentInputBinding()
|
||||
),
|
||||
],
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { NgFor, NgIf } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, 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 { RouterLinkWithHref } from '@angular/router';
|
||||
import { LetDirective } from '@ngrx/component';
|
||||
import { provideComponentStore } from '@ngrx/component-store';
|
||||
import { debounceTime, distinctUntilChanged, skipWhile, tap } from 'rxjs';
|
||||
import { Photo } from '../photo.model';
|
||||
import { PhotoStore } from './photos.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-photos',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatProgressBarModule,
|
||||
NgIf,
|
||||
NgFor,
|
||||
MatInputModule,
|
||||
LetDirective,
|
||||
RouterLinkWithHref,
|
||||
],
|
||||
template: `
|
||||
<h2 class="text-xl mb-2">Photos</h2>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Search</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
[formControl]="search"
|
||||
placeholder="write an article" />
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container *ngrxLet="vm$ as vm">
|
||||
<section class="flex flex-col">
|
||||
<section class="flex gap-3 items-center">
|
||||
<button
|
||||
[disabled]="vm.page === 1"
|
||||
[class.bg-gray-400]="vm.page === 1"
|
||||
class="text-xl border rounded-md p-3"
|
||||
(click)="store.previousPage()">
|
||||
<
|
||||
</button>
|
||||
<button
|
||||
[disabled]="vm.endOfPage"
|
||||
[class.bg-gray-400]="vm.endOfPage"
|
||||
class="text-xl border rounded-md p-3"
|
||||
(click)="store.nextPage()">
|
||||
>
|
||||
</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">
|
||||
<li *ngFor="let photo of vm.photos; trackBy: trackById">
|
||||
<a routerLink="detail" [queryParams]="{ photo: encode(photo) }">
|
||||
<img
|
||||
src="{{ photo.url_q }}"
|
||||
alt="{{ photo.title }}"
|
||||
class="image" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ng-template #noPhoto>
|
||||
<div>No Photos found. Type a search word.</div>
|
||||
</ng-template>
|
||||
<footer class="text-red-500">
|
||||
{{ vm.error }}
|
||||
</footer>
|
||||
</section>
|
||||
</ng-container>
|
||||
`,
|
||||
providers: [provideComponentStore(PhotoStore)],
|
||||
host: {
|
||||
class: 'p-5 block',
|
||||
},
|
||||
})
|
||||
export default class PhotosComponent implements OnInit {
|
||||
store = inject(PhotoStore);
|
||||
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(
|
||||
skipWhile(() => !this.formInit),
|
||||
debounceTime(300),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
trackById(index: number, photo: Photo) {
|
||||
return photo.id;
|
||||
}
|
||||
|
||||
encode(photo: Photo) {
|
||||
return encodeURIComponent(JSON.stringify(photo));
|
||||
}
|
||||
}
|
||||
135
apps/angular/interop-rxjs-signal/src/app/list/photos.store.ts
Normal file
135
apps/angular/interop-rxjs-signal/src/app/list/photos.store.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import {
|
||||
ComponentStore,
|
||||
OnStateInit,
|
||||
OnStoreInit,
|
||||
tapResponse,
|
||||
} from '@ngrx/component-store';
|
||||
import { pipe } from 'rxjs';
|
||||
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;
|
||||
page: number;
|
||||
pages: number;
|
||||
loading: boolean;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
const initialState: PhotoState = {
|
||||
photos: [],
|
||||
search: '',
|
||||
page: 1,
|
||||
pages: 1,
|
||||
loading: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PhotoStore
|
||||
extends ComponentStore<PhotoState>
|
||||
implements OnStoreInit, OnStateInit
|
||||
{
|
||||
private photoService = inject(PhotoService);
|
||||
|
||||
private readonly photos$ = this.select((s) => s.photos);
|
||||
private readonly search$ = this.select((s) => s.search);
|
||||
private readonly page$ = this.select((s) => s.page);
|
||||
private readonly pages$ = this.select((s) => s.pages);
|
||||
private readonly error$ = this.select((s) => s.error);
|
||||
private readonly loading$ = this.select((s) => s.loading);
|
||||
|
||||
private readonly endOfPage$ = this.select(
|
||||
this.page$,
|
||||
this.pages$,
|
||||
(page, pages) => page === pages
|
||||
);
|
||||
|
||||
readonly vm$ = this.select(
|
||||
{
|
||||
photos: this.photos$,
|
||||
search: this.search$,
|
||||
page: this.page$,
|
||||
pages: this.pages$,
|
||||
endOfPage: this.endOfPage$,
|
||||
loading: this.loading$,
|
||||
error: this.error$,
|
||||
},
|
||||
{ debounce: true }
|
||||
);
|
||||
|
||||
ngrxOnStoreInit() {
|
||||
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() {
|
||||
this.searchPhotos(
|
||||
this.select({
|
||||
search: this.search$,
|
||||
page: this.page$,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
readonly search = this.updater(
|
||||
(state, search: string): PhotoState => ({
|
||||
...state,
|
||||
search,
|
||||
page: 1,
|
||||
})
|
||||
);
|
||||
|
||||
readonly nextPage = this.updater(
|
||||
(state): PhotoState => ({
|
||||
...state,
|
||||
page: state.page + 1,
|
||||
})
|
||||
);
|
||||
|
||||
readonly previousPage = this.updater(
|
||||
(state): PhotoState => ({
|
||||
...state,
|
||||
page: state.page - 1,
|
||||
})
|
||||
);
|
||||
|
||||
readonly searchPhotos = this.effect<{ search: string; page: number }>(
|
||||
pipe(
|
||||
filter(({ search }) => search.length >= 3),
|
||||
tap(() => this.patchState({ loading: true, error: '' })),
|
||||
mergeMap(({ search, page }) =>
|
||||
this.photoService.searchPublicPhotos(search, page).pipe(
|
||||
tapResponse(
|
||||
({ photos: { photo, pages } }) => {
|
||||
this.patchState({
|
||||
loading: false,
|
||||
photos: photo,
|
||||
pages,
|
||||
});
|
||||
localStorage.setItem(
|
||||
PHOTO_STATE_KEY,
|
||||
JSON.stringify({ search, page })
|
||||
);
|
||||
},
|
||||
(error: unknown) => this.patchState({ error, loading: false })
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
10
apps/angular/interop-rxjs-signal/src/app/photo.model.ts
Normal file
10
apps/angular/interop-rxjs-signal/src/app/photo.model.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Photo {
|
||||
id: string;
|
||||
title: string;
|
||||
tags: string;
|
||||
owner: string;
|
||||
ownername: string;
|
||||
datetaken: string;
|
||||
url_q: string;
|
||||
url_m: string;
|
||||
}
|
||||
39
apps/angular/interop-rxjs-signal/src/app/photos.service.ts
Normal file
39
apps/angular/interop-rxjs-signal/src/app/photos.service.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Photo } from './photo.model';
|
||||
|
||||
export interface FlickrAPIResponse {
|
||||
photos: {
|
||||
pages: number;
|
||||
photo: Photo[];
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PhotoService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
public searchPublicPhotos(
|
||||
searchTerm: string,
|
||||
page: number
|
||||
): Observable<FlickrAPIResponse> {
|
||||
return this.http.get<FlickrAPIResponse>(
|
||||
'https://www.flickr.com/services/rest/',
|
||||
{
|
||||
params: {
|
||||
tags: searchTerm,
|
||||
method: 'flickr.photos.search',
|
||||
format: 'json',
|
||||
nojsoncallback: '1',
|
||||
tag_mode: 'all',
|
||||
media: 'photos',
|
||||
per_page: '30',
|
||||
page,
|
||||
extras: 'tags,date_taken,owner_name,url_q,url_m',
|
||||
api_key: 'c3050d39a5bb308d9921bef0e15c437d',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user