Commit 8c0b7311 authored by Julian's avatar Julian
Browse files

refactor to blog example

parent eb0bb135
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
![Reactive Architecture and UX Patterns - Combining Stream and Behavior](./assets/images/Reactive-architecture-and-ux-patterns_angular_combining-streams-and-behavior_michael-hladky.png) ![Reactive Architecture and UX Patterns - Combining Stream and Behavior](./assets/images/Reactive-architecture-and-ux-patterns_angular_combining-streams-and-behavior_michael-hladky.png)
This set of lessons are all about combination operators. This set of lessons is all about combination operators.
As this course focuses on real-life use cases, As this course focuses on real-life use cases,
we will use these operators in the context of state derivation, data fetching and update behavior. we will use these operators in the context of state derivation, data fetching and update behavior.
...@@ -44,18 +44,45 @@ If we understand every of those "broken parts" we are intuitively able to unders ...@@ -44,18 +44,45 @@ If we understand every of those "broken parts" we are intuitively able to unders
Within this set of lessons we will walk through the following exercises: Within this set of lessons we will walk through the following exercises:
- [ ] We start we a simple setup where we derive data in our component directly over HTTP requests by using `forkJoin` - [ ] `forkJoin` -> `combineLatest`
- with this architecture we realize, we quickly run into the problem of over-fetching - We start with a simple setup where we derive data in our component directly over HTTP requests by using `forkJoin`
- We notice that `forkJoin` results in too many http calls
- Rebuild
- [ ] To solve it we refactor the give HTTP service to get more control over when we fetch the data - [ ] To solve it we refactor the give HTTP service to get more control over when we fetch the data
- this reviles one of the special behaviours of `frokJoin` and we need to rethink it usage - this reviles one of the special behaviours of `forkJoin` and we need to rethink it usage
- http-service-v1
- [ ] We learn the difference of `forkJoin` and `combineLatest` - [ ] We learn the difference of `forkJoin` and `combineLatest`
- this knowledge helps us to refactor the service and component. - this knowledge helps us to refactor the service and component.
- combineLatest
- [ ] As we go we start to introduce more features into our UI - [ ] As we go we start to introduce more features into our UI
- again we run into a problem, this time over-rendering. - again we run into a problem, this time over-rendering.
- [ ] To understand the problem we learn about the terms `Normalized` and `Denormalized` data - zip
- by using `zip` for our calculation we are able to solve the problem of over-rendering
- [ ] As it was quite technical so far we learn about `withLatestFrom` with a more playful example - [ ] As it was quite technical so far we learn about `withLatestFrom` with a more playful example
- by doing so we understand the concept of `promary` and `secondary` streams - by doing so we understand the concept of `promary` and `secondary` streams
- withLatestFrom
- [ ] With a fresh and open mine we think about those concepts in combination with a UX Pattern called `opt-in updates` - [ ] With a fresh and open mine we think about those concepts in combination with a UX Pattern called `opt-in updates`
- to give a better experience to our users we implement this pattern in our example - to give a better experience to our users we implement this pattern in our example
## Domain
```Typescript
interface Post {
id: string;
title: string;
}
interface BlogPost {
id: string;
title: string;
comments: Comment[];
commentCount: number;
}
interface Comment {
id: string;
postId: string;
text: string;
}
```
import {Injectable} from '@angular/core';
import {BehaviorSubject, combineLatest, Observable, of} from 'rxjs';
import {catchError, map} from 'rxjs/operators';
import {Item, JoinedItem, List, mergeListsAndItems, upsertEntities} from "shared";
import {StartHttpV1Service} from "combining-streams/lib/exercises/http-service-v1/start.http-v1.service";
import {HttpClient} from "@angular/common/http";
interface ListServiceState {
lists: List[];
items: Item[];
}
@Injectable({
providedIn: 'root'
})
export class combineLatestListService {
private readonly baseUrl = 'api';
private readonly itemUrl = [this.baseUrl, 'item'].join('/');
private readonly listUrl = [this.baseUrl, 'list'].join('/');
private readonly state$ = new BehaviorSubject<ListServiceState>({
lists: [] as List[],
items: [] as Item[]
});
lists$ = this.state$.pipe(map(s => s.lists));
items$ = this.state$.pipe(map(s => s.items));
constructor(private http: HttpClient) {
}
refetchLists() {
this.httpGetLists()
.subscribe(lists => {
console.log('upsert:', upsertEntities(this.state$.getValue().lists, lists, 'id'));
this.state$.next({
...this.state$.getValue(),
lists: upsertEntities(this.state$.getValue().lists, lists, 'id')
});
});
}
httpGetLists(): Observable<List[]> {
return this.http.get<List[]>(this.listUrl).pipe(
catchError(() => of([] as List[]))
);
}
httpPostItems(item: Pick<Item, 'iName' | 'lId'>): Observable<Item[]> {
return this.http.post<Item[]>(this.itemUrl, item);
}
addItem(item: Pick<Item, 'iName' | 'lId'>) {
this.httpPostItems(item)
.subscribe((v) => {
console.log(v);
this.refetchItems()
}, console.log);
}
refetchItems() {
this.httpGetItems()
.subscribe(items => {
this.state$.next({
...this.state$.getValue(),
items: upsertEntities(this.state$.getValue().items, items, 'id')
});
});
}
httpGetItems(): Observable<Item[]> {
return this.http.get<Item[]>(this.itemUrl).pipe(
catchError(() => of([] as Item[]))
);
}
}
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import {combineLatest, Observable} from "rxjs"; import { combineLatest, Observable } from 'rxjs';
import {map} from "rxjs/operators"; import { map } from 'rxjs/operators';
import {JoinedItem, ListService, mergeListsAndItems} from "shared"; import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
import {combineLatestListService} from "combining-streams/lib/exercises/combineLatest/combineLatest-list.service";
@Component({ @Component({
selector: 'solution-combineLatest', selector: 'solution-combineLatest',
template: `<h3>(solution) combineLatest</h3> template: `<h3>(solution) combineLatest</h3>
<mat-form-field> <mat-form-field>
<label>Name</label> <label>Name</label>
<input matInput name="iName" [(ngModel)]="iName"/> <input matInput name="text" [(ngModel)]="text"/>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="listService.addItem({'iName': iName, 'lId': 1})">AddItem</button> <button mat-raised-button color="primary" (click)="listService.addComment({'text': text, 'postId': 1})">AddItem</button>
<div *ngIf="list$ | async as list"> <div *ngIf="blog$ | async as blog">
<mat-list> <mat-list>
<mat-list-item *ngFor="let item of list"> <mat-list-item *ngFor="let post of blog">
{{item.iName}} - {{item.lName}} <span mat-line>{{post.title}}</span>
</mat-list-item> <span mat-line>Comments: {{post.commentCount}}</span>
</mat-list> </mat-list-item>
</div> </mat-list>
`, </div>
changeDetection: ChangeDetectionStrategy.OnPush, `,
encapsulation: ViewEncapsulation.None changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
}) })
export class SolutionCombineLatestComponent { export class SolutionCombineLatestComponent {
iName: string = ''; text: string = '';
list$: Observable<JoinedItem[]> = combineLatest([ blog$: Observable<BlogPost[]> = combineLatest([
this.listService.lists$, this.listService.posts$,
this.listService.items$ this.listService.comments$
]) ])
.pipe( .pipe(
map(([list, items]) => mergeListsAndItems(list, items)) map(([posts, comments]) => mergeListsAndItems(posts, comments))
); );
constructor(public listService: combineLatestListService) { constructor(public listService: BlogBasicService) {
this.listService.refetchLists(); this.listService.fetchPosts();
this.listService.refetchItems(); this.listService.fetchComments();
} }
} }
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; import { Component } from '@angular/core';
import {combineLatest, forkJoin, NEVER, Observable} from "rxjs"; import { forkJoin, Observable } from 'rxjs';
import {JoinedItem, ListService, mergeListsAndItems} from "shared"; import { map } from 'rxjs/operators';
import {map} from "rxjs/operators"; import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
import {combineLatestListService} from "combining-streams/lib/exercises/combineLatest/combineLatest-list.service";
@Component({ @Component({
selector: 'combineLatest', selector: 'combineLatest',
template: `<h3>combineLatest</h3> template: `
<h3>combineLatest</h3>
<mat-form-field> <mat-form-field>
<label>Name</label> <label>Title</label>
<input matInput name="iName" [(ngModel)]="iName"/> <input matInput name="comment" [(ngModel)]="comment"/>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="listService.addItem({'iName': iName, 'lId': 1})">AddItem</button> <button mat-raised-button color="primary" (click)="listService.addPost({'title': comment})">Add Post</button>
<div *ngIf="blog$ | async as blog">
<div *ngIf="list$ | async as list">
<mat-list> <mat-list>
<mat-list-item *ngFor="let item of list"> <mat-list-item *ngFor="let post of blog">
{{item.iName}} - {{item.lName}} <span mat-line>{{post.title}}</span>
<span mat-line>Comments: {{post.commentCount}}</span>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</div> </div>
` `
}) })
export class StartCombineLatestComponent { export class StartCombineLatestComponent {
iName: string = ''; comment: string = '';
list$: Observable<JoinedItem[]> = forkJoin([
this.listService.lists$, blog$: Observable<BlogPost[]> = forkJoin([
this.listService.items$ this.listService.posts$,
this.listService.comments$
]) ])
.pipe( .pipe(
map(([list, items]) => mergeListsAndItems(list, items)) map(([posts, comments]) => mergeListsAndItems(posts, comments))
); );
constructor(public listService: combineLatestListService) { constructor(public listService: BlogBasicService) {
this.listService.refetchLists(); this.listService.fetchPosts();
this.listService.refetchItems(); this.listService.fetchComments();
} }
} }
import {Injectable} from '@angular/core'; import { HttpClient } from '@angular/common/http';
import {Observable, of} from 'rxjs'; import { Injectable } from '@angular/core';
import {catchError} from 'rxjs/operators'; import { Observable, of } from 'rxjs';
import {Item, List} from "shared"; import { catchError } from 'rxjs/operators';
import {HttpClient} from "@angular/common/http"; import { Comment, Post } from 'shared';
@Injectable({ @Injectable({
...@@ -17,15 +17,15 @@ export class ForkJoinListService { ...@@ -17,15 +17,15 @@ export class ForkJoinListService {
} }
httpGetItems(): Observable<Item[]> { httpGetItems(): Observable<Comment[]> {
return this.http.get<Item[]>(this.itemUrl).pipe( return this.http.get<Comment[]>(this.itemUrl).pipe(
catchError(e => of([] as Item[])) catchError(e => of([] as Comment[]))
); );
} }
httpGetLists(): Observable<List[]> { httpGetLists(): Observable<Post[]> {
return this.http.get<List[]>(this.listUrl).pipe( return this.http.get<Post[]>(this.listUrl).pipe(
catchError(e => of([] as List[])) catchError(e => of([] as Post[]))
); );
} }
......
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {forkJoin, Observable} from "rxjs"; import {forkJoin, Observable} from "rxjs";
import {map} from "rxjs/operators"; import {map} from "rxjs/operators";
import {JoinedItem, mergeListsAndItems} from "shared"; import {BlogPost, mergeListsAndItems} from "shared";
import {ForkJoinListService} from "combining-streams/lib/exercises/forkJoin/forkJoin-list.service"; import {ForkJoinListService} from "combining-streams/lib/exercises/forkJoin/forkJoin-list.service";
@Component({ @Component({
selector: 'forkJoin', selector: 'forkJoin',
template: `<h3>(Solution) ForkJoin</h3> template: `
<h3>(Solution) ForkJoin</h3>
<div *ngIf="list$ | async as list">
<mat-list> <mat-list>
<mat-list-item *ngFor="let item of list"> <mat-list-item *ngFor="let item of list$ | async">
{{item.iName}} - {{item.lName}} {{item.iName}} - {{item.lName}}
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>`
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
}) })
export class SolutionForkJoinComponent { export class SolutionForkJoinComponent {
list$: Observable<JoinedItem[]> = forkJoin([ list$: Observable<BlogPost[]> = forkJoin([
this.listService.httpGetLists(), this.listService.httpGetLists(),
this.listService.httpGetItems() this.listService.httpGetItems()
]).pipe( ]).pipe(
......
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {ForkJoinListService} from "./forkJoin-list.service"; import {ForkJoinListService} from "./forkJoin-list.service";
import {JoinedItem} from "shared"; import {BlogPost} from "shared";
@Component({ @Component({
selector: 'solution-forkJoin', selector: 'solution-forkJoin',
template: `<h3>forkJoin</h3> template: `
<h3>forkJoin</h3>
<div *ngIf="list$ | async as list">
<mat-list> <mat-list>
<mat-list-item *ngFor="let item of list"> <mat-list-item *ngFor="let item of list$ | async">
{{item.iName}} - {{item.lName}} {{item.iName}} - {{item.lName}}
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</div>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class StartForkJoinComponent { export class StartForkJoinComponent {
list$: Observable<JoinedItem>; list$: Observable<BlogPost>;
// this.listService.httpGetLists(),
// this.listService.httpGetItems()
// mergeListsAndItems(lists, items)
constructor(private listService: ForkJoinListService) { constructor(private listService: ForkJoinListService) {
} }
......
import {NgModule} from '@angular/core'; import { CommonModule } from '@angular/common';
import {CommonModule} from "@angular/common"; import { HttpClientModule } from '@angular/common/http';
import {RouterModule} from "@angular/router"; import { NgModule } from '@angular/core';
import {ROUTES} from "./http-service-v1.routes"; import { MatButtonModule } from '@angular/material/button';
import {MatButtonModule} from "@angular/material/button"; import { MatListModule } from '@angular/material/list';
import {MatListModule} from "@angular/material/list"; import { RouterModule } from '@angular/router';
import {HttpClientModule} from "@angular/common/http"; import { SolutionHttpServiceV1Component } from 'combining-streams/lib/exercises/http-service-v1/solution.http-service-v1.component';
import {StartHttpServiceV1Component} from "combining-streams/lib/exercises/http-service-v1/start.http-service-v1.component"; import { StartHttpServiceV1Component } from 'combining-streams/lib/exercises/http-service-v1/start.http-service-v1.component';
import {SolutionHttpServiceV1Component} from "combining-streams/lib/exercises/http-service-v1/solution.http-service-v1.component"; import { ROUTES } from './http-service-v1.routes';
@NgModule({ @NgModule({
declarations: [ declarations: [
......
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import {forkJoin, Observable} from "rxjs"; import { forkJoin, Observable } from 'rxjs';
import {map} from "rxjs/operators"; import { map } from 'rxjs/operators';
import {JoinedItem, mergeListsAndItems} from "shared"; import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
import {combineLatestListService} from "combining-streams/lib/exercises/combineLatest/combineLatest-list.service";
@Component({ @Component({
selector: 'solution-custom-http-service-v1', selector: 'solution-custom-http-service-v1',
template: `<h3>(Solution) custom-http-service-v1</h3> template: `<h3>(Solution) custom-http-service-v1</h3>
<button mat-raised-button color="primary" (click)="listService.addItem({iName: 'new item', lId: 1})">AddItem</button> <button mat-raised-button color="primary" (click)="listService.addPost({title: 'new post'}); refetch();">Add Post</button>
<div *ngIf="list$ | async as list"> <div *ngIf="blog$ | async as blog">
<mat-list> <mat-list>
<mat-list-item *ngFor="let item of list"> <mat-list-item *ngFor="let post of blog">
{{item.iName}} - {{item.lName}} <span mat-line>{{post.title}}</span>
<span mat-line>Comments: {{post.commentCount}}</span>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</div> </div>
...@@ -24,17 +24,24 @@ import {combineLatestListService} from "combining-streams/lib/exercises/combineL ...@@ -24,17 +24,24 @@ import {combineLatestListService} from "combining-streams/lib/exercises/combineL
}) })
export class SolutionHttpServiceV1Component { export class SolutionHttpServiceV1Component {
list$: Observable<JoinedItem[]> = forkJoin([ blog$: Observable<BlogPost[]> = this.getBlogList();
this.listService.lists$,
this.listService.items$ constructor(public listService: BlogBasicService) {
])
.pipe( }
map(([list, items]) => mergeListsAndItems(list, items))
); refetch() {
this.blog$ = this.getBlogList();
constructor(public listService: combineLatestListService) { }
this.listService.refetchLists();
this.listService.refetchItems(); private getBlogList(): Observable<BlogPost[]> {