Commit 50b24802 authored by micha's avatar micha
Browse files

progress

parent c38fa0ba
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
## Intro ## Intro
![Combination operators comparison - screenshot](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-comparison-solution-screenshot_michael-hladky.png) ![Combination operators comparison - screenshot](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-comparison-solution-screenshot_michael-hladky.png)
_Combination operators comparison - screenshot_
In the start component you will find a screen split into 2 sections. In the start component you will find a screen split into 2 sections.
Left and right. In the center Left and right. In the center
......
...@@ -9,7 +9,7 @@ order to get the final list of `BlogPost`; ...@@ -9,7 +9,7 @@ order to get the final list of `BlogPost`;
## Exercise ## Exercise
Use the `forkJoin` operator to combine the http calls for `Comment` & `Post`. Use the `forkJoin` operator to combine the http calls for `Comment` & `Post`.
You can find the methods `httpGetComments` and `httpGetPosts` in the `ForkJoinBlogService` service. You can find the methods `httpGetComments` and `httpGetPosts` in the `ZipService` service.
After retrieving the 2 results from the `forkJoin` creation function, After retrieving the 2 results from the `forkJoin` creation function,
we use the `map` operator to calculate the new list of `BlogPost` with `toBlogPosts`. we use the `map` operator to calculate the new list of `BlogPost` with `toBlogPosts`.
...@@ -20,7 +20,7 @@ This `Component` will be the starting point for you. All needed dependencies are ...@@ -20,7 +20,7 @@ This `Component` will be the starting point for you. All needed dependencies are
```Typescript ```Typescript
// start.forkJoin.component.ts // start.forkJoin.component.ts
import {BlogPost, toBlogPosts} from 'shared'; import {BlogPost, toBlogPosts} from 'shared';
import {ForkJoinBlogService} from './fork-join-blog.service'; import {ZipService} from './fork-join-blog.service';
export class StartForkJoinComponent { export class StartForkJoinComponent {
...@@ -28,7 +28,7 @@ export class StartForkJoinComponent { ...@@ -28,7 +28,7 @@ export class StartForkJoinComponent {
blog$: Observable<BlogPost[]>; // join http calls and mat to blog posts here blog$: Observable<BlogPost[]>; // join http calls and mat to blog posts here
constructor(private blogPostService: ForkJoinBlogService) { constructor(private blogPostService: ZipService) {
} }
} }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
import {forkJoin, Observable} from 'rxjs'; import {forkJoin, Observable} from 'rxjs';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {BlogPost, toBlogPosts} from 'shared'; import {BlogPost, toBlogPosts} from 'shared';
import {ForkJoinBlogService} from './fork-join-blog.service'; import {ZipService} from './fork-join-blog.service';
export class SolutionForkJoinComponent { export class SolutionForkJoinComponent {
// ... // ...
...@@ -22,7 +22,7 @@ export class SolutionForkJoinComponent { ...@@ -22,7 +22,7 @@ export class SolutionForkJoinComponent {
map(([posts, comments]) => toBlogPosts(posts, comments)) map(([posts, comments]) => toBlogPosts(posts, comments))
) )
constructor(private blogPostService: ForkJoinBlogService) { constructor(private blogPostService: ZipService) {
} }
} }
......
# zip behavior and gotchas
## Behavior
![zip - inner ongoing](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-inner-ongoing_michael-hladky.png)
_zip - inner ongoing_
![zip - inner error](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-inner-error_michael-hladky.png)
_zip - inner error_
![zip - inner complete](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-inner-complete_michael-hladky.png)
_zip - inner complete_
## 💡 Gotcha(s)!
![zip - never emits if one source never emits](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-never-emits_michael-hladky.png)
_zip - never emits if one source never emits_
![zip - EMPTY results in immediate completion](./assets/images/Reactive-architecture-and-ux-patterns_angular_combination-operators-zip-never-emits_michael-hladky.png)
_zip - EMPTY results in immediate completion_
import { Component } from '@angular/core'; import {Component} from '@angular/core';
import { combineLatest, Observable, zip } from 'rxjs'; import {combineLatest, Observable,} from 'rxjs';
import { filter, map, shareReplay, tap } from 'rxjs/operators'; import {map, share, shareReplay, tap} from 'rxjs/operators';
import { BlogBasicService, BlogPost, toBlogPosts } from 'shared'; import {BlogPost, toBlogPosts} from 'shared';
import {ZipBlogService} from "combining-streams/lib/exercises/zip/zip-blog-post.service";
@Component({ @Component({
selector: 'solution-zip', selector: 'solution-zip',
template: `<h3>(solution) zip</h3> template: `<h3>(Solution) zip</h3>
<mat-form-field> <mat-form-field>
<label>Name</label> <label>Name</label>
<input matInput name="comment" [(ngModel)]="comment"/> <input matInput name="post" [(ngModel)]="title"/>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="listService.addComment({'text': comment, 'postId': 1})">AddItem</button> <button mat-raised-button color="primary" (click)="blogPostService.addPost({title: title})">Add Comment
</button>
<p><b>renders: {{renders()}}</b></p> <p><b>renders: {{renders()}}</b></p>
<p><b>processJoinedList: {{processJoinedList()}}</b></p> <p><b>processJoinedList: {{processJoinedList()}}</b></p>
<p><b>processLikedList: {{processLikedList()}}</b></p> <p><b>processLikedList: {{processLikedList()}}</b></p>
<div class="row"> <div class="row">
<div style="width: 49%" *ngIf="joinedList$ | async as list">
<div style="width: 49%" *ngIf="blog$ | async as list">
<b>All items</b> <b>All items</b>
<mat-list> <mat-list>
<mat-list-item *ngFor="let item of list"> <mat-list-item *ngFor="let item of list">
...@@ -26,10 +29,11 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared'; ...@@ -26,10 +29,11 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared';
</mat-list> </mat-list>
</div> </div>
<div style="width: 49%" *ngIf="commentedPosts$ | async as commentedPosts">
<div style="width: 49%" *ngIf="commentedPosts$ | async as likedItems">
<b>Liked items</b> <b>Liked items</b>
<mat-list> <mat-list>
<mat-list-item *ngFor="let item of commentedPosts"> <mat-list-item *ngFor="let item of likedItems">
{{item.title}} - Comments: {{item.commentCount}} {{item.title}} - Comments: {{item.commentCount}}
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
...@@ -46,42 +50,36 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared'; ...@@ -46,42 +50,36 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared';
] ]
}) })
export class SolutionZipComponent { export class SolutionZipComponent {
comment = 'my new item'; title = 'my new Title';
numProcessJoinedList = 0; numProcessJoinedList = 0;
numRenders = 0; numRenders = 0;
numProcessLikedList = 0; numProcessLikedList = 0;
blog$ = combineLatest([
joinedList$ = combineLatest([ this.blogPostService.posts$,
this.listService.posts$.pipe( this.blogPostService.comments$
filter(l => !!l.length)
),
this.listService.comments$.pipe(
filter(l => !!l.length)
)
]).pipe( ]).pipe(
map(([list, items]) => toBlogPosts(list, items)), map(([list, items]) => toBlogPosts(list, items)),
tap(v => ++this.numProcessJoinedList), tap(v => ++this.numProcessJoinedList)
shareReplay(1)
); );
commentedIds$ = this.joinedList$.pipe(map(list => list
commentedIds$ = this.blog$.pipe(map(list => list
.filter(i => i.commentCount > 0) .filter(i => i.commentCount > 0)
.map(i => i.id)) .map(i => i.id))
); );
commentedPosts$: Observable<BlogPost[]> = zip( commentedPosts$: Observable<BlogPost[]> = combineLatest([
this.joinedList$, this.blog$,
this.commentedIds$ this.commentedIds$
) ])
.pipe( .pipe(
map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.id)))), map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.id)))),
tap(v => ++this.numProcessLikedList) tap(v => ++this.numProcessLikedList)
); );
constructor(public listService: BlogBasicService) { constructor(public blogPostService: ZipBlogService) {
this.listService.fetchPosts(); this.blogPostService.fetchPosts();
this.listService.fetchComments(); this.blogPostService.fetchComments();
} }
processJoinedList() { processJoinedList() {
......
import { Component } from '@angular/core'; import {Component} from '@angular/core';
import { combineLatest, Observable, } from 'rxjs'; import {combineLatest, Observable, zip,} from 'rxjs';
import { map, tap } from 'rxjs/operators'; import {distinctUntilChanged, filter, map, share, shareReplay, tap} from 'rxjs/operators';
import { BlogBasicService, BlogPost, toBlogPosts } from 'shared'; import {BlogPost, toBlogPosts} from 'shared';
import {ZipBlogService} from "combining-streams/lib/exercises/zip/zip-blog-post.service";
@Component({ @Component({
selector: 'zip', selector: 'zip',
...@@ -9,9 +10,10 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared'; ...@@ -9,9 +10,10 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared';
<mat-form-field> <mat-form-field>
<label>Name</label> <label>Name</label>
<input matInput name="comment" [(ngModel)]="comment"/> <input matInput name="post" [(ngModel)]="title"/>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="listService.addComment({'text': comment, 'postId': 1})">AddItem</button> <button mat-raised-button color="primary" (click)="blogPostService.addPost({title: title})">Add Comment
</button>
<p><b>renders: {{renders()}}</b></p> <p><b>renders: {{renders()}}</b></p>
<p><b>processJoinedList: {{processJoinedList()}}</b></p> <p><b>processJoinedList: {{processJoinedList()}}</b></p>
...@@ -48,35 +50,41 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared'; ...@@ -48,35 +50,41 @@ import { BlogBasicService, BlogPost, toBlogPosts } from 'shared';
] ]
}) })
export class StartZipComponent { export class StartZipComponent {
comment = 'my new item'; title = 'my new Title';
numProcessJoinedList = 0; numProcessJoinedList = 0;
numRenders = 0; numRenders = 0;
numProcessLikedList = 0; numProcessLikedList = 0;
blog$ = combineLatest([ blog$ = combineLatest([
this.listService.posts$, this.blogPostService.posts$,
this.listService.comments$ this.blogPostService.comments$
]).pipe( ]).pipe(
map(([list, items]) => toBlogPosts(list, items)), map(([list, items]) => toBlogPosts(list, items)),
tap(v => ++this.numProcessJoinedList) tap(v => ++this.numProcessJoinedList),
share()
); );
commentedIds$ = this.blog$.pipe(map(list => list
commentedIds$ = this.blog$.pipe(
map(list => list
.filter(i => i.commentCount > 0) .filter(i => i.commentCount > 0)
.map(i => i.id)) .map(i => i.id)
),
distinctUntilChanged()
); );
commentedPosts$: Observable<BlogPost[]> = combineLatest([ //
commentedPosts$: Observable<BlogPost[]> = zip(
this.blog$, this.blog$,
this.commentedIds$ this.commentedIds$
]) )
.pipe( .pipe(
map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.id)))), map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.id)))),
tap(v => ++this.numProcessLikedList) tap(v => ++this.numProcessLikedList)
); );
constructor(public listService: BlogBasicService) { constructor(public blogPostService: ZipBlogService) {
this.listService.fetchPosts(); this.blogPostService.fetchPosts();
this.listService.fetchComments(); this.blogPostService.fetchComments();
} }
processJoinedList() { processJoinedList() {
......
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {catchError, distinctUntilChanged, map} from 'rxjs/operators';
import {Comment, Post, upsertEntities} from 'shared';
interface BlogServiceState {
posts: Post[];
comments: Comment[];
}
@Injectable({
providedIn: 'root'
})
export class ZipBlogService {
private readonly baseUrl = 'api';
private readonly commentUrl = [this.baseUrl, 'comment'].join('/');
private readonly postUrl = [this.baseUrl, 'post'].join('/');
private readonly state$ = new BehaviorSubject<BlogServiceState>({
posts: [] as Post[],
comments: [] as Comment[]
});
readonly posts$ = this.state$.pipe(map(s => s.posts));
readonly comments$ = this.state$.pipe(map(s => s.comments));
constructor(private http: HttpClient) {
this.fetchPosts();
this.fetchComments();
}
fetchPosts() {
this.httpGetPosts()
.subscribe(posts => {
this.state$.next({
...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, posts, 'id')
});
});
}
fetchComments() {
this.httpGetComments()
.subscribe(comments => {
this.state$.next({
...this.state$.getValue(),
comments: upsertEntities(this.state$.getValue().comments, comments, 'id')
});
});
}
addPost(post: Pick<Post, 'title'>) {
this.httpPostPost(post)
.subscribe((newPost) => {
console.log('saved ', newPost , 'to the server');
this.state$.next({
...this.state$.getValue(),
posts: upsertEntities(this.state$.getValue().posts, [newPost], 'id')
});
}, console.log);
}
httpGetPosts(): Observable<Post[]> {
return this.http.get<Post[]>(this.postUrl).pipe(
catchError(() => of([] as Post[]))
);
}
httpPostComment(item: Pick<Comment, 'text' | 'postId'>): Observable<Comment[]> {
return this.http.post<Comment[]>(this.commentUrl, item);
}
httpPostPost(post: Pick<Post, 'title'>): Observable<Post> {
return this.http.post<Post>(this.postUrl, post);
}
httpGetComments(): Observable<Comment[]> {
return this.http.get<Comment[]>(this.commentUrl).pipe(
catchError(() => of([] as Comment[]))
);
}
}
...@@ -24,7 +24,8 @@ export class InMemoryDataService implements InMemoryDbService { ...@@ -24,7 +24,8 @@ export class InMemoryDataService implements InMemoryDbService {
const posts: Post[] = [ const posts: Post[] = [
{id: 1, title: 'Post 1'}, {id: 1, title: 'Post 1'},
{id: 2, title: 'Post 2'}, {id: 2, title: 'Post 2'},
{id: 3, title: 'Post 3'} {id: 3, title: 'Post 3'},
{id: 4, title: 'Post 4'}
]; ];
return {comment: comments, post: posts}; return {comment: comments, post: posts};
} }
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment