Commit 87680965 authored by micha's avatar micha
Browse files

progress

parent b623730b
...@@ -10,20 +10,15 @@ export const MENU_ITEMS: MenuItem[] = [ ...@@ -10,20 +10,15 @@ export const MENU_ITEMS: MenuItem[] = [
link: './combineLatest' link: './combineLatest'
}, },
{ {
label: 'opt-in updates-v1', label: 'withLatestFrom',
link: './withLatestFrom' link: './withLatestFrom'
}, },
{ {
label: 'state normalisation and zip', label: 'zip',
link: './zip' link: './zip'
}, },
{ {
label: 'comparison', label: 'comparison',
link: './comparison' link: './comparison'
}, }
{
label: 'http service v1',
link: './http-service-v1'
},
]; ];
...@@ -15,11 +15,6 @@ export const ROUTES = [ ...@@ -15,11 +15,6 @@ export const ROUTES = [
loadChildren: () => import('./exercises/combineLatest/combineLatest.module') loadChildren: () => import('./exercises/combineLatest/combineLatest.module')
.then(m => m.CombineLatestModule) .then(m => m.CombineLatestModule)
}, },
{
path: 'http-service-v1',
loadChildren: () => import('./exercises/http-service-v1/http-service-v1.module')
.then(m => m.HttpServiceV1Module)
},
{ {
path: 'zip', path: 'zip',
loadChildren: () => import('./exercises/zip/zip.module') loadChildren: () => import('./exercises/zip/zip.module')
......
.box {
position: relative;
width: 100%;
height: 400px;
border: 1px solid darkgray;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: column;
background-color: #3E4F85;
}
.separation {
height: 400px;
width: calc(50% - 1px);
position: absolute;
left: 0px;
z-index: 0;
border-right: 3px solid #2B295F;
background-color: #EF407E;
}
.click-area {
position: absolute;
width: 100%;
height: 100%;
z-index: 10;
}
.click-pos {
display: none;
position: absolute;
z-index: 3;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid darkgray;
background: gray;
}
.click-result {
width: 250px;
height: 100px;
line-height: 100px;
text-align: center;
background-color: white;
color: #2B295F;
border: 1px solid #2B295F;
font-size: 20px;
z-index: 1;
}
import {AfterViewInit, Component, OnDestroy, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
import {combineLatest, fromEvent, ReplaySubject, Subscription, zip} from "rxjs"; import {combineLatest, fromEvent, ReplaySubject, Subscription, zip} from "rxjs";
import {map, shareReplay, startWith, tap, withLatestFrom} from "rxjs/operators"; import {map, shareReplay, startWith, tap, withLatestFrom} from "rxjs/operators";
@Component({ @Component({
selector: 'withLatestFrom', selector: 'withLatestFrom',
template: `<h3>(Solution) withLatestFrom</h3> template: `<h3>(Solution) withLatestFrom</h3>
<div #box class="box"> <div #box class="box">
<div class="separation"> <div class="separation">
</div>
<div class="click-result">
combineLatest:
<b>{{clickResultCombine$ | async}}</b>
</div>
<div class="click-result">
withLatestFrom:
<b>{{clickResultWithLatest$ | async}}</b>
</div>
<div class="click-result">
zip:
<b>{{clickResultZip$ | async}}</b>
</div>
</div> </div>
<div class="click-result">
combineLatest:
<b>{{clickResultCombine$ | async}}</b>
</div>
<div class="click-result">
withLatestFrom:
<b>{{clickResultWithLatest$ | async}}</b>
</div>
<div class="click-result">
zip:
<b>{{clickResultZip$ | async}}</b>
</div>
</div>
`, `,
styles: [` styleUrls: ['./comparison.component.scss']
.box {
position: relative;
width: 100%;
height: 400px;
border: 1px solid darkgray;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: column;
background-color: #3E4F85;
}
.separation {
height: 400px;
width: calc(50% - 1px);
position: absolute;
left: 0px;
z-index: 0;
border-right: 3px solid #2B295F;
background-color: #EF407E;
}
.click-result {
width: 250px;
height: 100px;
line-height: 100px;
text-align: center;
background-color: white;
color: #2B295F;
border: 1px solid #2B295F;
font-size: 20px;
z-index: 1;
}
`]
}) })
export class SolutionComparisonComponent implements AfterViewInit, OnDestroy { export class SolutionComparisonComponent implements AfterViewInit, OnDestroy {
subscription = new Subscription(); subscription = new Subscription();
@ViewChild('box') @ViewChild('box')
...@@ -70,8 +36,17 @@ export class SolutionComparisonComponent implements AfterViewInit, OnDestroy { ...@@ -70,8 +36,17 @@ export class SolutionComparisonComponent implements AfterViewInit, OnDestroy {
clickResultWithLatest$ = new ReplaySubject<string>(1); clickResultWithLatest$ = new ReplaySubject<string>(1);
clickResultZip$ = new ReplaySubject<string>(1); clickResultZip$ = new ReplaySubject<string>(1);
constructor(private elemRef: ElementRef) {
}
ngAfterViewInit(): void { ngAfterViewInit(): void {
const clickPosX$ = fromEvent(this.boxViewChild.nativeElement, 'click').pipe( const clickPosX$ = fromEvent(this.boxViewChild.nativeElement, 'click').pipe(
tap((e: any) => {
const elem = this.elemRef.nativeElement.querySelector('.click-pos');
elem.style.top = `${e.offsetY - 15}px`;
elem.style.left = `${e.offsetX - 15}px`;
elem.style.display = 'block';
}),
map((e) => e['offsetX']), map((e) => e['offsetX']),
shareReplay(1) shareReplay(1)
); );
...@@ -84,9 +59,9 @@ export class SolutionComparisonComponent implements AfterViewInit, OnDestroy { ...@@ -84,9 +59,9 @@ export class SolutionComparisonComponent implements AfterViewInit, OnDestroy {
this.subscription.add( this.subscription.add(
combineLatest([clickPosX$, elemWith$]) combineLatest([clickPosX$, elemWith$])
.pipe( .pipe(
map(([posX, width]) => this.getSideOfClick(posX, width)), map(([posX, width]) => this.getSideOfClick(posX, width)),
).subscribe(this.clickResultCombine$) ).subscribe(this.clickResultCombine$)
); );
this.subscription.add(clickPosX$ this.subscription.add(clickPosX$
......
import {AfterViewInit, Component, OnDestroy, ViewChild} from '@angular/core'; import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild} from '@angular/core';
import {fromEvent, ReplaySubject, Subject, Subscription} from "rxjs"; import {fromEvent, ReplaySubject, Subscription} from "rxjs";
import {map, startWith, withLatestFrom} from "rxjs/operators"; import {map, tap} from "rxjs/operators";
@Component({ @Component({
selector: 'withLatestFrom', selector: 'withLatestFrom',
template: `<h3>withLatestFrom</h3> template: `<h3>withLatestFrom</h3>
<div #box class="box"> <div #box class="box">
<div class="separation"> <div class="click-area"></div>
<div class="separation"></div>
</div> <div class="click-pos">&nbsp;</div>
<div class="click-result"> <div class="click-result">
combineLatest combineLatest
...@@ -23,45 +24,9 @@ import {map, startWith, withLatestFrom} from "rxjs/operators"; ...@@ -23,45 +24,9 @@ import {map, startWith, withLatestFrom} from "rxjs/operators";
zip zip
{{clickResultZip$ | async}} {{clickResultZip$ | async}}
</div> </div>
</div> </div>
`, `,
styles: [` styleUrls: ['./comparison.component.scss']
.box {
position: relative;
width: 100%;
height: 400px;
border: 1px solid darkgray;
display: flex;
align-items: center;
justify-content: space-around;
flex-direction: column;
background-color: #3E4F85;
}
.separation {
height: 400px;
width: calc(50% - 1px);
position: absolute;
left: 0px;
z-index: 0;
border-right: 3px solid #2B295F;
background-color: #EF407E;
}
.click-result {
width: 250px;
height: 100px;
line-height: 100px;
text-align: center;
background-color: white;
color: #2B295F;
border: 1px solid #2B295F;
font-size: 20px;
z-index: 1;
}
`]
}) })
export class StartComparisonComponent implements AfterViewInit, OnDestroy { export class StartComparisonComponent implements AfterViewInit, OnDestroy {
subscription = new Subscription(); subscription = new Subscription();
...@@ -73,12 +38,18 @@ export class StartComparisonComponent implements AfterViewInit, OnDestroy { ...@@ -73,12 +38,18 @@ export class StartComparisonComponent implements AfterViewInit, OnDestroy {
clickResultWithLatest$ = new ReplaySubject<string>(1); clickResultWithLatest$ = new ReplaySubject<string>(1);
clickResultZip$ = new ReplaySubject<string>(1); clickResultZip$ = new ReplaySubject<string>(1);
constructor() { constructor(private elemRef: ElementRef) {
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
const clickPosX$ = fromEvent(this.boxViewChild.nativeElement, 'click').pipe( const clickPosX$ = fromEvent(this.boxViewChild.nativeElement, 'click').pipe(
tap((e: any) => {
const elem = this.elemRef.nativeElement.querySelector('.click-pos');
elem.style.top = `${e.offsetY - 15}px`;
elem.style.left = `${e.offsetX - 15}px`;
elem.style.display = 'block';
}),
map((e) => e['offsetX']) map((e) => e['offsetX'])
); );
this.subscription.add( this.subscription.add(
...@@ -95,8 +66,7 @@ export class StartComparisonComponent implements AfterViewInit, OnDestroy { ...@@ -95,8 +66,7 @@ export class StartComparisonComponent implements AfterViewInit, OnDestroy {
} }
getSideOfClick(posX: number, width: number): string {
getSideOfClick(posX: number, width: number) {
return (width / 2) < posX ? 'Right' : 'Left'; return (width / 2) < posX ? 'Right' : 'Left';
} }
......
# `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))
);
}
```
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatListModule } from '@angular/material/list';
import { RouterModule } from '@angular/router';
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 { ROUTES } from './http-service-v1.routes';
@NgModule({
declarations: [
StartHttpServiceV1Component,
SolutionHttpServiceV1Component
],
imports: [
CommonModule,
MatButtonModule,
MatListModule,
HttpClientModule,
RouterModule.forChild(ROUTES)
]
})
export class HttpServiceV1Module {
}
import {SolutionHttpServiceV1Component} from "./solution.http-service-v1.component";
import {StartHttpServiceV1Component} from "./start.http-service-v1.component";
export const ROUTES = [
{
path: '',
children: [
{
path: '',
component: StartHttpServiceV1Component
},
{
path: 'solution',
component: SolutionHttpServiceV1Component
}
]
}
];
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { forkJoin, Observable } from 'rxjs';
import {map, switchMap} from 'rxjs/operators';
import { BlogBasicService, BlogPost, toBlogPosts } from 'shared';
@Component({
selector: 'solution-custom-http-service-v1',
template: `<h3>(Solution) custom-http-service-v1</h3>
<button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<div *ngIf="blog$ | async as blog">
<mat-list>
<mat-list-item *ngFor="let post of blog">
<span mat-line>{{post.title}}</span>
<span mat-line>Comments: {{post.commentCount}}</span>
</mat-list-item>
</mat-list>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class SolutionHttpServiceV1Component {
blog$: Observable<BlogPost[]>;
constructor(public listService: BlogBasicService) {
this.refetch();
}
addPost() {
this.listService.httpPostPost({title: 'new post'})
.subscribe((v) => {
console.log(v);
// this.fetchPosts();
}, console.log);
this.refetch();
}
refetch() {
this.blog$ = this.getBlogList();
}
private getBlogList(): Observable<BlogPost[]> {
return forkJoin([
this.listService.httpGetPosts(),
this.listService.httpGetComments()
])
.pipe(
map(([posts, comments]) => toBlogPosts(posts, comments))
);
}
}
import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core';
import { forkJoin, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { BlogBasicService, BlogPost, toBlogPosts } from 'shared';
@Component({
selector: 'custom-http-service-v1',
template: `<h3>custom-http-service-v1</h3>
<button mat-raised-button color="primary">Add Post</button>
<div *ngIf="blog$ | async as list">
<mat-list>
<mat-list-item *ngFor="let item of list">
<span mat-line>{{item.title}}</span>
<span mat-line>Comments: {{item.commentCount}}</span>
</mat-list-item>
</mat-list>