Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Stéphane Roucheray
Reactive-Architecture-and-UX-Patterns_Angular
Commits
9a27759d
Commit
9a27759d
authored
May 14, 2020
by
Julian
Browse files
improve combineLatest + start withlatestFrom
parent
cd158540
Changes
12
Hide whitespace changes
Inline
Side-by-side
projects/combining-streams/src/lib/exercises/combineLatest/docs/combineLatest.exercise.md
View file @
9a27759d
...
...
@@ -89,7 +89,8 @@ We need to replace `forkJoin` with an operator that matches the new requirements
`combineLatest`
is perfectly suited for this case.
Use it and see if the list of BlogPosts renders now.
Try adding a new
`Post`
using the "Add Post" button. ??
Additionally, use the provided
`addPost()`
method and implement a way to add new
`BlogPost`
to the list.
If done correctly, the list of
`BlogPost`
should update instantly after adding a new entity.
...
...
projects/combining-streams/src/lib/exercises/combineLatest/docs/combineLatest.the
r
oy.md
→
projects/combining-streams/src/lib/exercises/combineLatest/docs/combineLatest.theo
r
y.md
View file @
9a27759d
File moved
projects/combining-streams/src/lib/exercises/combineLatest/solution.combineLatest.component.ts
View file @
9a27759d
import
{
ChangeDetectionStrategy
,
Component
,
ViewEncapsulation
}
from
'
@angular/core
'
;
import
{
c
ombineLatest
,
Observable
}
from
'
rxjs
'
;
import
{
map
}
from
'
rxjs
/operators
'
;
import
{
BlogPost
,
toBlogPosts
}
from
'
shared
'
;
import
{
CombineLatestBlogService
}
from
"
combining-streams/lib/exercises/combineLatest/combine-latest-blog.service
"
;
import
{
ChangeDetectionStrategy
,
Component
,
ViewEncapsulation
}
from
'
@angular/core
'
;
import
{
C
ombineLatest
BlogService
}
from
'
./combine-latest-blog.service
'
;
import
{
combineLatest
,
Observable
}
from
'
rxjs
'
;
import
{
map
}
from
'
rxjs/operators
'
;
import
{
BlogPost
,
toBlogPosts
}
from
'
shared
'
;
@
Component
({
selector
:
'
solution-combineLatest
'
,
template
:
`
<h1>(Solution) combineLatest</h1>
<mat-form-field>
<label>Name</label>
<input matInput name="post" [(ngModel)]="post"/>
</mat-form-field>
<button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<div *ngIf="blog$ | async as list">
...
...
@@ -24,6 +28,8 @@ import {CombineLatestBlogService} from "combining-streams/lib/exercises/combineL
encapsulation
:
ViewEncapsulation
.
None
})
export
class
SolutionCombineLatestComponent
{
post
=
'
my new post
'
;
blog$
:
Observable
<
BlogPost
[]
>
=
combineLatest
([
this
.
blogPostService
.
posts$
,
this
.
blogPostService
.
comments$
...
...
@@ -37,7 +43,7 @@ export class SolutionCombineLatestComponent {
}
addPost
()
{
this
.
blogPostService
.
addPost
({
title
:
'
New
post
'
});
this
.
blogPostService
.
addPost
({
title
:
this
.
post
});
}
}
projects/combining-streams/src/lib/exercises/combineLatest/start.combineLatest.component.ts
View file @
9a27759d
...
...
@@ -8,7 +8,11 @@ import {CombineLatestBlogService} from "./combine-latest-blog.service";
selector
:
'
combineLatest
'
,
template
:
`
<h1>combineLatest</h1>
<button mat-raised-button color="primary" (click)="addPost()">Add Post</button>
<mat-form-field>
<label>Name</label>
<input matInput name="post" [(ngModel)]="post"/>
</mat-form-field>
<button mat-raised-button color="primary">Add Post</button>
<div *ngIf="blog$ | async as list">
<mat-list>
...
...
@@ -21,6 +25,8 @@ import {CombineLatestBlogService} from "./combine-latest-blog.service";
`
})
export
class
StartCombineLatestComponent
{
post
=
'
my new post
'
;
blog$
:
Observable
<
BlogPost
[]
>
=
forkJoin
([
this
.
blogPostService
.
httpGetPosts
(),
this
.
blogPostService
.
httpGetComments
()
...
...
@@ -33,8 +39,5 @@ export class StartCombineLatestComponent {
}
addPost
()
{
this
.
blogPostService
.
addPost
({
title
:
'
new post
'
});
}
}
projects/combining-streams/src/lib/exercises/forkJoin/docs/forkJoin.exercise.md
View file @
9a27759d
...
...
@@ -14,30 +14,6 @@ You can find the methods `httpGetComments` and `httpGetPosts` in the `ForkJoinBl
After retrieving the 2 results from the
`forkJoin`
creation function,
we use the
`map`
operator to calculate the new list of
`BlogPost`
with
`toBlogPosts`
.
**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;
}
```
This
`Component`
will be the starting point for you. All needed dependencies are already included.
**Component**
...
...
projects/combining-streams/src/lib/exercises/withLatestFrom/blog.service.ts
0 → 100644
View file @
9a27759d
import
{
HttpClient
}
from
'
@angular/common/http
'
;
import
{
Injectable
}
from
'
@angular/core
'
;
import
{
BehaviorSubject
,
Observable
,
of
}
from
'
rxjs
'
;
import
{
catchError
,
map
}
from
'
rxjs/operators
'
;
import
{
Comment
,
Post
,
upsertEntities
}
from
'
shared
'
;
interface
BlogServiceState
{
posts
:
Post
[];
comments
:
Comment
[];
}
@
Injectable
({
providedIn
:
'
root
'
})
export
class
BlogService
{
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
[]))
);
}
}
projects/combining-streams/src/lib/exercises/withLatestFrom/docs/withLatestFrom.exercise.md
View file @
9a27759d
# Opt-in updates - Exercise
## Intro
Now that we've learned how to efficiently handle data coming from multiple HTTP Endpoints we can move on and
tackle more complex scenarios. Our current solution displays data whenever it arrives (_instant updates_).
However, there will be situations where you want to _stage_ incoming data before actually render them.
Imagine a message feed like _twitter_ where new arriving _tweets_ first inform the user about updates before actually updating
the feed.
In this scenario, users are able to _opt-in_ for updates.
_current solution: instant updates visualized_

_desired solution: opt-in updates visualized_

For this exercise we have to extend our
`Component`
with extra functionalities.
```
Typescript
@Component()
export class StartWithLatestFromComponent {
optInListClick$ = new Subject(); // performs the opt-in update on button click
numNewItems$: Observable<number>; // the derived number of new items available for display
feed$: Observable<BlogPost[]>; // the new feed to display. use blog$ and optIntListClick$ to calculate
}
```
## Exercise
Utilize the
`withLatestFrom`
combination operator and implement a _staging_ area for new arriving
`BlogPost`
.
Use the new introduced properties
`optInListClick$`
,
`numNewItems$`
and
`feed$`
to implement your solution. You should
end up replacing the view binding of
`blog$`
by
`feed$`
. Use
`blog$`
and
`optInListClick$`
to calculate the new feed.
projects/combining-streams/src/lib/exercises/withLatestFrom/docs/withLatestFrom.solution.md
View file @
9a27759d
# Opt-in updates - Solution
Staging new arriving data with
`withLatestFrom`
:
**Component**
```
Typescript
// solution.withLatestFrom.component.ts
blog$ = combineLatest([
this.blogService.posts$.pipe(filter(l => !!l.length)),
this.blogService.comments$.pipe(filter(l => !!l.length))
]).pipe(
map(([list, items]) => toBlogPosts(list, items)),
shareReplay(1)
);
feed$: Observable<BlogPost[]> = concat(
this.blog$.pipe(take(1)),
this.optInListClick$.pipe(
withLatestFrom(this.blog$),
map(([_, items]) => items)
),
shareReplay(1)
);
numNewItems$: Observable<number> = combineLatest([
this.blog$,
this.feed$
])
.pipe(
map(([a, b]) => Math.abs(a.length - b.length))
);
```
Great! ... we need some text here :)
projects/combining-streams/src/lib/exercises/withLatestFrom/docs/withLatestFrom.theory.md
View file @
9a27759d
# Opt-in updates - Theory
The previous example showcased how to combine multiple ongoing
`Observables`
to one result. However, there are situations
where it is necessary to access the latest value from an
`Observable`
after a specific event occurs.
`withLatestFrom`
combines the last _emitted_ value of the a provided source
`Observable`
to an active stream of data.
In this example we want to get the latest value of the
`timer$`
whenever a user clicks on the document.
```
Typescript
import { fromEvent, interval } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
const click$ = fromEvent(document, 'click'); // get the click event
const timer$ = interval(1000);
const result = click$.pipe(withLatestFrom(timer$));
result.subscribe(([clickEvent, latestTimerValue]) => console.log(latestTimerValue)); // logs the latest value of the timer
```



...
...
projects/combining-streams/src/lib/exercises/withLatestFrom/solution.withLatestFrom.component.ts
View file @
9a27759d
import
{
Component
}
from
'
@angular/core
'
;
import
{
combineLatest
,
concat
,
Observable
,
Subject
}
from
'
rxjs
'
;
import
{
filter
,
map
,
shareReplay
,
take
,
withLatestFrom
}
from
'
rxjs/operators
'
;
import
{
BlogBasicService
,
toBlogPosts
}
from
'
shared
'
;
import
{
BlogBasicService
,
BlogPost
,
toBlogPosts
}
from
'
shared
'
;
@
Component
({
selector
:
'
solution-opt-in-updates-basic
'
,
...
...
@@ -10,22 +10,21 @@ import {BlogBasicService, toBlogPosts} from 'shared';
<mat-form-field>
<label>Name</label>
<input matInput name="
commen
t" [(ngModel)]="
commen
t"/>
<input matInput name="
pos
t" [(ngModel)]="
pos
t"/>
</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)="addPost()">Add Post</button>
<ng-container *ngIf="(numNewItems$ | async) as numItems">
<button mat-raised-button color="accent"
*ngIf
="numItems
>
0"
[disabled]
="numItems
===
0"
(click)="optInListClick$.next($event)">
Update List
({{(
New posts:
({{(
numItems
)}})
</button>
</ng-container>
<div *ngIf="
acceptedItems
$ | async as blog">
<div *ngIf="
feed
$ | async as blog">
<mat-list>
<mat-list-item *ngFor="let post of blog">
<span mat-line>{{post.title}}</span>
...
...
@@ -37,36 +36,40 @@ import {BlogBasicService, toBlogPosts} from 'shared';
})
export
class
SolutionWithLatestFromComponent
{
commen
t
=
'
my new
commen
t
'
;
pos
t
=
'
my new
pos
t
'
;
optInListClick$
=
new
Subject
();
blog$
=
combineLatest
([
this
.
list
Service
.
posts$
.
pipe
(
filter
(
l
=>
!!
l
.
length
)),
this
.
list
Service
.
comments$
.
pipe
(
filter
(
l
=>
!!
l
.
length
))
this
.
blog
Service
.
posts$
.
pipe
(
filter
(
l
=>
!!
l
.
length
)),
this
.
blog
Service
.
comments$
.
pipe
(
filter
(
l
=>
!!
l
.
length
))
]).
pipe
(
map
(([
list
,
items
])
=>
toBlogPosts
(
list
,
items
)),
shareReplay
(
1
)
);
acceptedItems$
=
concat
(
feed$
:
Observable
<
BlogPost
[]
>
=
concat
(
this
.
blog$
.
pipe
(
take
(
1
)),
this
.
optInListClick$
.
pipe
(
withLatestFrom
(
this
.
blog$
),
map
(([
_
,
items
])
=>
items
)
)
),
shareReplay
(
1
)
);
numNewItems$
:
Observable
<
number
>
=
combineLatest
([
this
.
blog$
,
this
.
acceptedItems
$
this
.
feed
$
])
.
pipe
(
map
(([
a
,
b
])
=>
Math
.
abs
(
a
.
length
-
b
.
length
))
);
constructor
(
public
list
Service
:
BlogBasicService
)
{
this
.
list
Service
.
fetchPosts
();
this
.
list
Service
.
fetchComments
();
constructor
(
public
blog
Service
:
BlogBasicService
)
{
this
.
blog
Service
.
fetchPosts
();
this
.
blog
Service
.
fetchComments
();
}
addPost
()
{
this
.
blogService
.
addPost
({
title
:
this
.
post
});
}
}
projects/combining-streams/src/lib/exercises/withLatestFrom/start.withLatestFrom.component.ts
View file @
9a27759d
import
{
Component
}
from
'
@angular/core
'
;
import
{
combineLatest
,
of
,
Subject
}
from
'
rxjs
'
;
import
{
BlogService
}
from
'
./blog.service
'
;
import
{
combineLatest
,
Observable
,
of
,
Subject
}
from
'
rxjs
'
;
import
{
filter
,
map
,
shareReplay
}
from
'
rxjs/operators
'
;
import
{
Blog
BasicService
,
toBlogPosts
}
from
'
shared
'
;
import
{
Blog
Post
,
toBlogPosts
}
from
'
shared
'
;
@
Component
({
selector
:
'
with-latest-from
'
,
template
:
`
<h1>withLatestFrom</h1>
<mat-form-field>
<label>Name</label>
<input matInput name="
commen
t" [(ngModel)]="
commen
t"/>
</mat-form-field>
<button mat-raised-button color="primary" (click)="
listService.addComment({'text': comment, 'postId': 1}
)">Add
Item
</button>
<ng-container *ngIf="(numNewItems$ | async) as numItems">
<button mat-raised-button color="accent"
*ngIf
="numItems
>
0"
(click)="optInListClick$.next($event)">
Update List
({{(
numItems
)}})
</button>
</ng-container>
<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>
<mat-form-field>
<label>Name</label>
<input matInput name="
pos
t" [(ngModel)]="
pos
t"/>
</mat-form-field>
<button mat-raised-button color="primary" (click)="
addPost(
)">Add
Post
</button>
<ng-container *ngIf="(numNewItems$ | async) as numItems">
<button mat-raised-button color="accent"
[disabled]
="numItems
===
0"
(click)="optInListClick$.next($event)">
New posts:
({{(
numItems
)}})
</button>
</ng-container>
<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>
`
})
export
class
StartWithLatestFromComponent
{
commen
t
=
'
my new
commen
t
'
;
pos
t
=
'
my new
pos
t
'
;
optInListClick$
=
new
Subject
();
numNewItems$
=
of
(
0
);
numNewItems$
:
Observable
<
number
>
;
feed$
:
Observable
<
BlogPost
[]
>
;
// use optInListClick$ and blog$ to calculate new feed$
blog$
=
combineLatest
([
this
.
list
Service
.
posts$
.
pipe
(
filter
(
l
=>
!!
l
.
length
)),
this
.
list
Service
.
comments$
.
pipe
(
filter
(
l
=>
!!
l
.
length
))
this
.
blog
Service
.
posts$
.
pipe
(
filter
(
l
=>
!!
l
.
length
)),
this
.
blog
Service
.
comments$
.
pipe
(
filter
(
l
=>
!!
l
.
length
))
]).
pipe
(
map
(([
posts
,
comments
])
=>
toBlogPosts
(
posts
,
comments
)),
shareReplay
(
1
)
);
constructor
(
public
list
Service
:
Blog
Basic
Service
)
{
this
.
list
Service
.
fetchPosts
();
this
.
list
Service
.
fetchComments
();
constructor
(
public
blog
Service
:
BlogService
)
{
this
.
blog
Service
.
fetchPosts
();
this
.
blog
Service
.
fetchComments
();
}
addPost
()
{
this
.
blogService
.
addPost
({
title
:
this
.
post
});
}
}
projects/shared/src/lib/list-resource/blog-basic.service.ts
View file @
9a27759d
...
...
@@ -59,7 +59,7 @@ export class BlogBasicService {
this
.
httpPostPost
(
post
)
.
subscribe
((
v
)
=>
{
console
.
log
(
v
);
//
this.fetchPosts();
this
.
fetchPosts
();
},
console
.
log
);
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment