Commit 9a3f9ce1 authored by Julian's avatar Julian
Browse files

refactor to blog example

parent 8c0b7311
...@@ -42,13 +42,40 @@ If we understand every of those "broken parts" we are intuitively able to unders ...@@ -42,13 +42,40 @@ If we understand every of those "broken parts" we are intuitively able to unders
## Combination Operators - Exercise walk through ## Combination Operators - Exercise walk through
In order to showcase the different capabilities and constraints of the combination operators we will create a very
simple Blog application. `Post` & `Comment` are the actual entities which can be fetched from service endpoints.
The combination operators will be used to combine `Post` & `Comment` to `BlogPost`.
```Typescript
// entity
interface Post {
id: string;
title: string;
content: string;
}
// entity
interface Comment {
id: string;
postId: string;
text: string;
}
// derivation
interface BlogPost {
id: string;
title: string;
comments: Comment[];
commentCount: number;
}
```
Within this set of lessons we will walk through the following exercises: Within this set of lessons we will walk through the following exercises:
- [ ] `forkJoin` -> `combineLatest` - [ ] `forkJoin` -> `http-service-v1`
- We start with a simple setup where we derive data in our component directly over HTTP requests by using `forkJoin` - We start with a very simple list example where we derive data in our component directly over HTTP requests by using `forkJoin`
- We notice that `forkJoin` results in too many http calls - List is not "reactive" in terms of "adding/updating" -> build a refetch
- Rebuild - We notice that this architecture results in HTTP over-fetching -> introduce simple state -> forkJoin vs. combineLatest
- [ ] To solve it we refactor the give HTTP service to get more control over when we fetch the data - [ ] `combineLatest`
- 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 `forkJoin` 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 - http-service-v1
- [ ] We learn the difference of `forkJoin` and `combineLatest` - [ ] We learn the difference of `forkJoin` and `combineLatest`
...@@ -63,26 +90,5 @@ Within this set of lessons we will walk through the following exercises: ...@@ -63,26 +90,5 @@ Within this set of lessons we will walk through the following exercises:
- [ ] 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;
}
```
# Theory of `combineLatest` # `combineLatest` Theory
## Intro
In the previous example we implemented a self-refetching list of `BlogPost`. We noticed that we were calling problem of over-fetching http calls. To come across this issue
you typically want to introduce some sort of state management.
...@@ -10,9 +10,9 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; ...@@ -10,9 +10,9 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
<mat-form-field> <mat-form-field>
<label>Name</label> <label>Name</label>
<input matInput name="text" [(ngModel)]="text"/> <input matInput name="text" [(ngModel)]="title"/>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="listService.addComment({'text': text, 'postId': 1})">AddItem</button> <button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<div *ngIf="blog$ | async as blog"> <div *ngIf="blog$ | async as blog">
<mat-list> <mat-list>
...@@ -27,7 +27,8 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; ...@@ -27,7 +27,8 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class SolutionCombineLatestComponent { export class SolutionCombineLatestComponent {
text: string = ''; title: string = '';
blog$: Observable<BlogPost[]> = combineLatest([ blog$: Observable<BlogPost[]> = combineLatest([
this.listService.posts$, this.listService.posts$,
this.listService.comments$ this.listService.comments$
...@@ -40,4 +41,14 @@ export class SolutionCombineLatestComponent { ...@@ -40,4 +41,14 @@ export class SolutionCombineLatestComponent {
this.listService.fetchPosts(); this.listService.fetchPosts();
this.listService.fetchComments(); this.listService.fetchComments();
} }
addPost() {
this.listService.addPost({title: this.title});
this.refetch();
}
refetch() {
this.listService.fetchPosts();
this.listService.fetchComments();
}
} }
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { forkJoin, Observable } from 'rxjs'; import { combineLatest, forkJoin, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
...@@ -9,9 +9,9 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; ...@@ -9,9 +9,9 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
<h3>combineLatest</h3> <h3>combineLatest</h3>
<mat-form-field> <mat-form-field>
<label>Title</label> <label>Title</label>
<input matInput name="comment" [(ngModel)]="comment"/> <input matInput name="title" [(ngModel)]="title"/>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="listService.addPost({'title': comment})">Add Post</button> <button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<div *ngIf="blog$ | async as blog"> <div *ngIf="blog$ | async as blog">
<mat-list> <mat-list>
<mat-list-item *ngFor="let post of blog"> <mat-list-item *ngFor="let post of blog">
...@@ -23,18 +23,30 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; ...@@ -23,18 +23,30 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
` `
}) })
export class StartCombineLatestComponent { export class StartCombineLatestComponent {
comment: string = ''; title: string = '';
blog$: Observable<BlogPost[]> = forkJoin([ blog$: Observable<BlogPost[]>;
this.listService.posts$,
this.listService.comments$
])
.pipe(
map(([posts, comments]) => mergeListsAndItems(posts, comments))
);
constructor(public listService: BlogBasicService) { constructor(public listService: BlogBasicService) {
this.listService.fetchPosts(); this.refetch();
this.listService.fetchComments(); }
addPost() {
this.listService.addPost({title: this.title});
this.refetch();
}
refetch() {
this.blog$ = this.getBlogList();
}
private getBlogList(): Observable<BlogPost[]> {
return forkJoin([
this.listService.posts$,
this.listService.comments$
])
.pipe(
map(([posts, comments]) => mergeListsAndItems(posts, comments))
);
} }
} }
# `forkJoin` Exercise
## Intro
We want to build a simple Blog application.
To get things started let's display a simple list of `BlogPost`.
There are HTTP Endpoints which provide us with `Comment` & `Post` data.
In addition, we want to add new `BlogPost` entries to the list without having to manually reload the data.
## Exercise
- Use `forkJoin` operator to combine the http calls
- Implement automatic re-fetching after adding a `BlogPost`
## Interfaces
```Typescript
// entity
interface Post {
id: string;
title: string;
content: string;
}
// entity
interface Comment {
id: string;
postId: string;
text: string;
}
// derivation
interface BlogPost {
id: string;
title: string;
comments: Comment[];
commentCount: number;
}
```
# `forkJoin` Solution
## Combining Http Calls
Combining the http calls with the `forkJoin` operator:
```Typescript
forkJoin([
this.listService.httpGetPosts(),
this.listService.httpGetComments()
])
.pipe(
map(([posts, comments]) => mergeListsAndItems(posts, comments))
)
```
## Refetch
Refetching data with `forkJoin` only works if every source emits new values. We have to re-call all
involved HTTP Request in order to update any information.
```html
<button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
```
```Typescript
addPost() {
this.listService.addPost({title: 'new post'});
this.refetch();
}
refetch() {
this.blog$ = this.getBlogList();
}
private getBlogList(): Observable<BlogPost[]> {
return forkJoin([
this.listService.httpGetPosts(),
this.listService.httpGetComments()
])
.pipe(
map(([posts, comments]) => mergeListsAndItems(posts, comments))
);
}
```
...@@ -8,7 +8,7 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; ...@@ -8,7 +8,7 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
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.addPost({title: 'new post'}); refetch();">Add Post</button> <button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<div *ngIf="blog$ | async as blog"> <div *ngIf="blog$ | async as blog">
<mat-list> <mat-list>
...@@ -24,10 +24,15 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; ...@@ -24,10 +24,15 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
}) })
export class SolutionHttpServiceV1Component { export class SolutionHttpServiceV1Component {
blog$: Observable<BlogPost[]> = this.getBlogList(); blog$: Observable<BlogPost[]>;
constructor(public listService: BlogBasicService) { constructor(public listService: BlogBasicService) {
this.refetch();
}
addPost() {
this.listService.addPost({title: 'new post'});
this.refetch();
} }
refetch() { refetch() {
......
...@@ -7,7 +7,7 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared'; ...@@ -7,7 +7,7 @@ import { BlogBasicService, BlogPost, mergeListsAndItems } from 'shared';
selector: 'custom-http-service-v1', selector: 'custom-http-service-v1',
template: `<h3>custom-http-service-v1</h3> template: `<h3>custom-http-service-v1</h3>
<button mat-raised-button color="primary" (click)="listService.addPost({title: 'new post'})">Add Post</button> <button mat-raised-button color="primary">Add Post</button>
<div *ngIf="blog$ | async as list"> <div *ngIf="blog$ | async as list">
<mat-list> <mat-list>
......
...@@ -2,7 +2,8 @@ import { HttpClient } from '@angular/common/http'; ...@@ -2,7 +2,8 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { Comment, Post, upsertEntities } from 'shared'; import { upsertEntities } from 'shared/lib/list-resource/operations/list-operations';
import { Post, Comment } from 'shared/lib/list-resource/models';
interface BlogBasicServiceState { interface BlogBasicServiceState {
posts: Post[]; posts: Post[];
...@@ -59,7 +60,7 @@ export class BlogBasicService { ...@@ -59,7 +60,7 @@ export class BlogBasicService {
this.httpPostPost(post) this.httpPostPost(post)
.subscribe((v) => { .subscribe((v) => {
console.log(v); console.log(v);
this.fetchPosts(); // this.fetchPosts();
}, console.log); }, console.log);
} }
......
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