Commit dff4f63f authored by micha's avatar micha
Browse files

add code for zip

parent be8c6fff
......@@ -44,18 +44,18 @@ 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:
- We start we a simple setup where we derive data in our component directly over HTTP requests by using `forkJoin`
- [] We start we a simple setup where we derive data in our component directly over HTTP requests by using `forkJoin`
- with this architecture we realize, we quickly run into the problem of over-fetching
- 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
- 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.
- As we go we start to introduce more features into our UI
- again we run into a problem, this time over-rendering.
- To understand the problem we learn about the terms `Normalized` and `Denormalized` data
- [] To understand the problem we learn about the terms `Normalized` and `Denormalized` data
- 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
- 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
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {Observable, zip} from "rxjs";
import {map} from "rxjs/operators";
import {JoinedItem, ListService, mergeListsAndItems} from "shared";
import {Component} from '@angular/core';
import {combineLatest, Observable, zip} from "rxjs";
import {filter, map, shareReplay, tap} from "rxjs/operators";
import {JoinedItem, mergeListsAndItems} from "shared";
import {zipListService} from "combining-streams/lib/exercises/zip/zip-list.service";
@Component({
selector: 'solution-zip',
template: `<h3>zip</h3>
<button mat-raised-button color="primary" (click)="s.refetchItems()">
Refresh Items
</button>
<button mat-raised-button color="primary" (click)="s.refetchLists()">
Refresh Lists
</button>
<div *ngIf="list$ | async as list">
<mat-list>
<mat-list-item *ngFor="let item of list">
{{item.iName}} - {{item.lName}}
</mat-list-item>
</mat-list>
selector: 'solution-zip',
template: `<h3>(solution) zip</h3>
<mat-form-field>
<label>Name</label>
<input matInput name="iName" [(ngModel)]="iName"/>
</mat-form-field>
<button (click)="listService.addItem({'iName': iName, 'lId': 1})">AddItem</button>
<p><b>renders: {{renders()}}</b></p>
<p><b>processJoinedList: {{processJoinedList()}}</b></p>
<p><b>processLikedList: {{processLikedList()}}</b></p>
<div class="row">
<div style="width: 49%" *ngIf="joinedList$ | async as list">
<b>All items</b>
<mat-list>
<mat-list-item *ngFor="let item of list">
{{item.iName}} - {{item.lName}}
</mat-list-item>
</mat-list>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
<div style="width: 49%" *ngIf="likedItems$ | async as likedItems">
<b>Liked items</b>
<mat-list>
<mat-list-item *ngFor="let item of likedItems">
{{item.iName}} - {{item.lName}}
</mat-list-item>
</mat-list>
</div>
</div>
`,
styles: [`
.row {
width: 100%;
display: flex;
}
`]
})
export class SolutionZipComponent {
iName: string = '';
numRenders = 0;
list$: Observable<JoinedItem[]> = zip(
this.s.lists$,
this.s.items$
)
.pipe(
map(([lists, items]) => mergeListsAndItems(lists, items))
);
renders() {
return ++this.numRenders
}
constructor(public s: ListService) {
numProcessJoinedList = 0;
}
processJoinedList() {
return this.numProcessJoinedList
}
numProcessLikedList = 0;
processLikedList() {
return this.numProcessLikedList
}
joinedList$ = combineLatest([
this.listService.lists$.pipe(
filter(l => !!l.length)
),
this.listService.items$.pipe(
filter(l => !!l.length)
)
]).pipe(
map(([list, items]) => mergeListsAndItems(list, items)),
tap(v => ++this.numProcessJoinedList),
shareReplay(1)
);
likedIds$ = this.joinedList$.pipe(map(list => list
.filter(i => i.liked)
.map(i => i.iId))
);
likedItems$: Observable<JoinedItem[]> = zip(
this.joinedList$,
this.likedIds$
)
.pipe(
map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.iId)))),
tap(v => ++this.numProcessLikedList)
);
constructor(private listService: zipListService) {
this.listService.refetchLists();
this.listService.refetchItems();
}
}
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import { of, } from "rxjs";
import {Item, List, ListService} from "shared";
import {Component} from '@angular/core';
import {combineLatest, Observable,} from "rxjs";
import {JoinedItem, mergeListsAndItems} from "shared";
import {map, tap} from "rxjs/operators";
import {zipListService} from "combining-streams/lib/exercises/zip/zip-list.service";
@Component({
selector: 'zip',
template: `<h3>zip</h3>
<button mat-raised-button color="primary" (click)="s.refetchItems()">
Refresh Items
</button>
<button mat-raised-button color="primary" (click)="s.refetchLists()">
Refresh Lists
</button>
<div *ngIf="list$ | async as list">
<mat-list>
<mat-list-item *ngFor="let item of list">
{{item.iName}} - {{item.lName}}
</mat-list-item>
</mat-list>
selector: 'zip',
template: `<h3>zip</h3>
<mat-form-field>
<label>Name</label>
<input matInput name="iName" [(ngModel)]="iName"/>
</mat-form-field>
<button (click)="listService.addItem({'iName': iName, 'lId': 1})">AddItem</button>
<p><b>renders: {{renders()}}</b></p>
<p><b>processJoinedList: {{processJoinedList()}}</b></p>
<p><b>processLikedList: {{processLikedList()}}</b></p>
<div class="row">
<div style="width: 49%" *ngIf="joinedList$ | async as list">
<b>All items</b>
<mat-list>
<mat-list-item *ngFor="let item of list">
{{item.iName}} - {{item.lName}}
</mat-list-item>
</mat-list>
</div>
<div style="width: 49%" *ngIf="likedItems$ | async as likedItems">
<b>Liked items</b>
<mat-list>
<mat-list-item *ngFor="let item of likedItems">
{{item.iName}} - {{item.lName}}
</mat-list-item>
</mat-list>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
</div>
`,
styles: [`
.row {
width: 100%;
display: flex;
}
`]
})
export class StartZipComponent {
iName: string = '';
numRenders = 0;
list$ = of([[] as List[],[] as Item[]])
.pipe(
renders() {
return ++this.numRenders
}
);
numProcessJoinedList = 0;
constructor(public s: ListService) {
processJoinedList() {
return this.numProcessJoinedList
}
}
numProcessLikedList = 0;
processLikedList() {
return this.numProcessLikedList
}
joinedList$ = combineLatest([
this.listService.lists$,
this.listService.items$
]).pipe(
map(([list, items]) => mergeListsAndItems(list, items)),
tap(v => ++this.numProcessJoinedList)
);
likedIds$ = this.joinedList$.pipe(map(list => list
.filter(i => i.liked)
.map(i => i.iId))
);
likedItems$: Observable<JoinedItem[]> = combineLatest([
this.joinedList$,
this.likedIds$
])
.pipe(
map(([mergedList, likedIds]) => (mergedList.filter(i => likedIds.find(li => li === i.iId)))),
tap(v => ++this.numProcessLikedList)
);
constructor(private listService: zipListService) {
this.listService.refetchLists();
this.listService.refetchItems();
}
}
import {Injectable} from '@angular/core';
import {BehaviorSubject, Observable, of} from 'rxjs';
import {catchError, map} from 'rxjs/operators';
import {Item, List, upsertEntities} from "shared";
import {HttpClient} from "@angular/common/http";
interface ListServiceState {
lists: List[];
items: Item[];
}
@Injectable({
providedIn: 'root'
})
export class zipListService {
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 => {
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[]))
);
}
}
......@@ -6,6 +6,9 @@ import {MatButtonModule} from "@angular/material/button";
import {StartZipComponent} from "combining-streams/lib/exercises/zip/start.zip.component";
import {SolutionZipComponent} from "combining-streams/lib/exercises/zip/solution.zip.component";
import {MatListModule} from "@angular/material/list";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {FormsModule} from "@angular/forms";
@NgModule({
declarations: [
......@@ -14,8 +17,11 @@ import {MatListModule} from "@angular/material/list";
],
imports: [
CommonModule,
FormsModule,
MatButtonModule,
MatListModule,
MatFormFieldModule,
MatInputModule,
RouterModule.forChild(ROUTES)
]
})
......
......@@ -9,16 +9,16 @@ export class InMemoryDataService implements InMemoryDbService {
createDb() {
// const name is resource URL
const item: Item[] = [
{id: 11, iName: 'Item 11', lId: 1},
{id: 12, iName: 'Item 12', lId: 1},
{id: 13, iName: 'Item 13', lId: 1},
{id: 14, iName: 'Item 14', lId: 1},
{id: 15, iName: 'Item 15', lId: 2},
{id: 16, iName: 'Item 16', lId: 2},
{id: 17, iName: 'Item 17', lId: 2},
{id: 18, iName: 'Item 18', lId: 3},
{id: 19, iName: 'Item 19', lId: 3},
{id: 20, iName: 'Item 20', lId: 3}
{id: 11, iName: 'Item 11', lId: 1, liked: true},
{id: 12, iName: 'Item 12', lId: 1, liked: true},
{id: 13, iName: 'Item 13', lId: 1, liked: false},
{id: 14, iName: 'Item 14', lId: 1, liked: true},
{id: 15, iName: 'Item 15', lId: 2, liked: true},
{id: 16, iName: 'Item 16', lId: 2, liked: false},
{id: 17, iName: 'Item 17', lId: 2, liked: true},
{id: 18, iName: 'Item 18', lId: 3, liked: false},
{id: 19, iName: 'Item 19', lId: 3, liked: false},
{id: 20, iName: 'Item 20', lId: 3, liked: true}
];
// const name is resource URL
const list: List[] = [
......
......@@ -2,4 +2,5 @@ export interface Item {
id: number;
iName: string;
lId: number;
liked: boolean
}
......@@ -3,4 +3,5 @@ export interface JoinedItem {
lName: string;
iId: number;
iName: string;
liked: boolean
}
......@@ -9,7 +9,8 @@ export function mergeListsAndItems(lists: List[], items: Item[]): Array<JoinedIt
iId: item.id,
iName: item.iName,
lId: list.id,
lName: list.lName
lName: list.lName,
liked: item.liked
})
}
);
......
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