use the api
Karine Samyn, Benjamin Vertonghen, Thomas Aelbrecht, Pieter Van Der
Helst
overview.
-
intro
what is an async call?
-
callbacks
using callbacks to handle async
-
promises
how and why do promises handle async better
-
observables
the new kid in town to handle async
-
recipe filter
using a Subject observable to create a recipe filter
-
HttpClientModule
how to make http calls in Angular
-
RecipeDataService
using the http module in our service
-
async pipe
display async data using the async pipe
-
error handling
how to handle observable errors
-
http post
how to make a http post call
recipe app startpoint
if you spent last class on facebook (or at the pub), branch the repo
at the correct commit and follow along!
~$ git clone https://github.com/Pieter-hogent/recipeapp.git (or git pull)
~$ cd recipeapp
~/recipeapp$ npm install
~/recipeapp$ git checkout -b mybranch 4b40342
Observable
-
Connecting with our (or any) API is done using Angular's
HttpClientModule
-
it neatly wraps performing an XHR: easily set headers, cope with
errors, response types, progress indicators, and so on
-
the async response is handled using an
Observable, which we'll explain first
-
(note that before Angular 4.3, this was handled by HttpModule, not
HttpClientModule, don't include the wrong one)
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
synchronous programming is calling a function, and waiting for
the response before you call the next function
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
the first function is called
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
the program waits for the function to finish and assigns the
result
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
and only then, the next function starts executing
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
what is async?
let notAMoon = buildDeathStar();
let alderaan = destroyPlanet(notAMoon);
let disturbedForce = aMillionVoicesCriedOut(alderaan);
until the program is finished, it's easy to follow along and
reason about synchronous code
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
but sometimes, completing a function takes a long time, and the
next function is not dependent on the result of the previous
one
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
we'd like to start creating the clone army while the death star
is still being built
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
that's what asynchronous programming is (in a nutshell), not
waiting for a function to finish before you start the next
one
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
this leads to a couple of difficulties...
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
when can you start using the result of an asynchronous
function?
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
remember, we're not waiting for the result of
buildADeathStarASync
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
so it's possible notAMoon doesn't
hold the result yet, while we're trying to use it
what is async?
let notAMoon = buildADeathStarAsync();
let bobbaFett = createACloneArmyAsync();
...
let alderaan = destroyPlanet(notAMoon);
solving this is what async programming is all about
and deceptively difficult
async solutions
-
there are multiple ways to cope with this, you can simply 'wait'
for the other function to finish, using mutexes, barriers, ...
-
we don't do that in javascript though, we use higher level
constructs: you've already learned about callbacks and promises
-
let's do a quick recap of how they work, and then introduce
another way to handle async functionality: observables
callbacks
function longAsyncOperation(callbackParameter) {
return callbackParameter(calculation);
}
let result = longAsyncOperation(doSomethingWithResult);
doSomethingWithResult(result);
doOtherStuffThatDoesntNeedResult();
so our problem is that we don't want to wait (a long time) for
the result but already start processing other stuff
callbacks
function longAsyncOperation(callbackParameter) {
return callbackParameter(calculation);
}
let result = longAsyncOperation(doSomethingWithResult);
doSomethingWithResult(result);
doOtherStuffThatDoesntNeedResult();
one way to do this is using callbacks
i.s.o. returning the result and processing the returned result,
we pass the processing function as a parameter to the long
operation!
callbacks
function longAsyncOperation(callbackParameter) {
return callbackParameter(calculation);
}
let result = longAsyncOperation(doSomethingWithResult);
doSomethingWithResult(result);
doOtherStuffThatDoesntNeedResult();
so we change the long operation to receive a processing
parameter
it no longer returns a result, it calls the processing
parameter on the result
callbacks
function longAsyncOperation(callbackParameter) {
return callbackParameter(calculation);
}
let result = longAsyncOperation(doSomethingWithResult);
doSomethingWithResult(result);
doOtherStuffThatDoesntNeedResult();
functions are first class citizens of javascript, so all that's
left is simply passing the function name of the processing
function to this long operation
callbacks
function longAsyncOperation(callbackParameter) {
return callbackParameter(calculation);
}
let result = longAsyncOperation(doSomethingWithResult);
doSomethingWithResult(result);
doOtherStuffThatDoesntNeedResult();
now other functions can be called while the long operation is
processing, and as soon as the result is available, the callback
will be called to process the result
multi threaded javascript
-
javascript (for now) is always single threaded, concurrency is
handled using an event loop and a message queue
-
every still-needs-processing (async) function become messages on
the message queue
-
the event loop will always pop the oldest message, process it, pop
the oldest message, process it, ...
-
(to make it slightly confusing, it's javascript after all, the
browser it's own API methods can run in parallel)
- let's look at a small example to illustrate this
callback hell
-
the callback idiom works fine if the callback is a small,
isolated, function
-
but if the callback basically boils down to "the rest of the
program", it will have callbacks itself...
- ...whom have callbacks themselves, and so on, and so on
-
leading to 'callback hell', the name already suggests this is not
the most fun thing in the world
callback hell
createGalacticEmpire(function (error, empire) {
if (!error) {
empire.buildADeathStar(function (notAMoon) {
notAMoon.destroyPlanet(function (error, alderaan) {
if (error) {
throw new Exception(error.msg);
} else {
alderaan.eliminateRebels(function (disturbance) {
....
})
}
})
})
} else {
console.log(error.message());
}
})
even for this relatively small example you can already tell
code like this gets complicated fast
callback hell
createGalacticEmpire(function (error, empire) {
if (!error) {
empire.buildADeathStar(function (notAMoon) {
notAMoon.destroyPlanet(function (error, alderaan) {
if (error) {
throw new Exception(error.msg);
} else {
alderaan.eliminateRebels(function (disturbance) {
....
})
}
})
})
} else {
console.log(error.message());
}
})
what is the flow of this function? is every error
handled?
you need to 'dig in' to understand what's happening (and what's
not)
callback problems
function slowAsync(callback) {
}
function anotherAsync(callback) {
}
let processAfterBoth = ...
slowAsync( ??? )
anotherAsync( ??? )
there are other problems with callbacks, what if you only want
to continue after multiple parallel functions have
finished?
callback problems
function slowAsync(callback) {
}
function anotherAsync(callback) {
}
let processAfterBoth = ...
slowAsync( ??? )
anotherAsync( ??? )
this can be solved, but it requires keeping track of all
functions and their state
it makes for even harder to read and reason about code
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
you've learned about promises in web2
the idea is to represent the result of an asynchronous
operation, which you can pass around and with which you can
interact at any time
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
so you start an async operation, and immediately get a
result, while the async operation runs
obviously it doesn't hold the final value yet, it's a
placeholder
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
so if other function calls follow the
buildADeathStarAsync, they are
immediately started, there's no waiting for the
buildADeathStarAsync to finish
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
you interact with this future value by calling a
then function with a callback, this
callback will be called as soon as the
buildADeathStarAsync is
finished
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
so a then() is not blocking either,
processing continues until the
buildADeathStarAsync is completed,
and then the callback is called
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
you can also provide a
catch() function, which will be
called if an error occurred during
buildADeathStarAsync
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
this looks a lot like callbacks (you pass a function that gets
called when the async function is finished), the difference
might look subtle, but is important
promises
let deathStarPromise = buildADeathStarAsync();
let army = buildCloneArmyAsync();
deathStarPromise.then(notAMoon => notAMoon.destroyPlanet());
let luke = meetYourDad();
deathStarPromise.catch(error => console.log(error));
by adding an extra level of indirection, you can 'capture' any
point in the chain (by keeping the result in a variable), and
diverge in multiple directions from that point
code is also more readable, as you don't end up with 20 levels
of indentation
callback hell to promise heaven
let empire = createGalacticEmpire(function (error, empire) {);
if (!error) {
let notAMoon = empire.then(e => e.buildADeathStar(function (notAMoon) {));
let alderaan = notAMoon.then(moon => moon.destroyPlanet(function (error, alderaan) {));
if (error) {
throw new Exception(error.msg);
} else {
let disturbance = alderaan.then(al => al.eliminateRebels(function (disturbance) {));
....
})
}
})
})
} else {
console.log(error.message());
}
})
disturbance.then( () => { ... } );
empire.catch(err => console.log(err.message()));
notAMoon.catch(err => throw new Exception(err.msg));
remember this example? let's rewrite it with promises to show
what we mean
callback hell to promise heaven
let empire = createGalacticEmpire(function (error, empire) {);
if (!error) {
let notAMoon = empire.then(e => e.buildADeathStar(function (notAMoon) {));
let alderaan = notAMoon.then(moon => moon.destroyPlanet(function (error, alderaan) {));
if (error) {
throw new Exception(error.msg);
} else {
let disturbance = alderaan.then(al => al.eliminateRebels(function (disturbance) {));
....
})
}
})
})
} else {
console.log(error.message());
}
})
disturbance.then( () => { ... } );
empire.catch(err => console.log(err.message()));
notAMoon.catch(err => throw new Exception(err.msg));
every error handling becomes a
catch, regular callbacks become a
then
callback hell to promise heaven
let empire = createGalacticEmpire(function (error, empire) {);
if (!error) {
let notAMoon = empire.then(e => e.buildADeathStar(function (notAMoon) {));
let alderaan = notAMoon.then(moon => moon.destroyPlanet(function (error, alderaan) {));
if (error) {
throw new Exception(error.msg);
} else {
let disturbance = alderaan.then(al => al.eliminateRebels(function (disturbance) {));
....
})
}
})
})
} else {
console.log(error.message());
}
})
disturbance.then( () => { ... } );
empire.catch(err => console.log(err.message()));
notAMoon.catch(err => throw new Exception(err.msg));
you end up with smaller and more readable code
callback hell to promise heaven
let empire = createGalacticEmpire(function (error, empire) {);
if (!error) {
let notAMoon = empire.then(e => e.buildADeathStar(function (notAMoon) {));
let alderaan = notAMoon.then(moon => moon.destroyPlanet(function (error, alderaan) {));
if (error) {
throw new Exception(error.msg);
} else {
let disturbance = alderaan.then(al => al.eliminateRebels(function (disturbance) {));
....
})
}
})
})
} else {
console.log(error.message());
}
})
disturbance.then( () => { ... } );
empire.catch(err => console.log(err.message()));
notAMoon.catch(err => throw new Exception(err.msg));
no matter how 'deep' you are in the hierarchy, you can keep
applying the same conversions
callback hell to promise heaven
let empire = createGalacticEmpire(function (error, empire) {);
if (!error) {
let notAMoon = empire.then(e => e.buildADeathStar(function (notAMoon) {));
let alderaan = notAMoon.then(moon => moon.destroyPlanet(function (error, alderaan) {));
if (error) {
throw new Exception(error.msg);
} else {
let disturbance = alderaan.then(al => al.eliminateRebels(function (disturbance) {));
....
})
}
})
})
} else {
console.log(error.message());
}
})
disturbance.then( () => { ... } );
empire.catch(err => console.log(err.message()));
notAMoon.catch(err => throw new Exception(err.msg));
callback hell to promise heaven
let empire = createGalacticEmpire(function (error, empire) {);
if (!error) {
let notAMoon = empire.then(e => e.buildADeathStar(function (notAMoon) {));
let alderaan = notAMoon.then(moon => moon.destroyPlanet(function (error, alderaan) {));
if (error) {
throw new Exception(error.msg);
} else {
let disturbance = alderaan.then(al => al.eliminateRebels(function (disturbance) {));
....
})
}
})
})
} else {
console.log(error.message());
}
})
disturbance.then( () => { ... } );
empire.catch(err => console.log(err.message()));
notAMoon.catch(err => throw new Exception(err.msg));
and again, until you have eliminated the 'callback hell'
callback hell to promise heaven
let empire = createGalacticEmpire(function (error, empire) {);
if (!error) {
let notAMoon = empire.then(e => e.buildADeathStar(function (notAMoon) {));
let alderaan = notAMoon.then(moon => moon.destroyPlanet(function (error, alderaan) {));
if (error) {
throw new Exception(error.msg);
} else {
let disturbance = alderaan.then(al => al.eliminateRebels(function (disturbance) {));
....
})
}
})
})
} else {
console.log(error.message());
}
})
disturbance.then( () => { ... } );
empire.catch(err => console.log(err.message()));
notAMoon.catch(err => throw new Exception(err.msg));
des goûts et des couleurs on ne discute pas
but I think we can all agree the flow became a lot easier to
grasp
promises
- there's a lot more to say about promises
- how to create them?
-
what happens when you return a promise from inside a promise?
-
what about Promise.all and
Promise.race?
-
but we already did that back in web2, and since you all made it
into web4, I'm sure you're all very versed in these matters
already
promises perfectly tackle async?
promises - problems?
- promises solve async handling pretty well
-
nothing is ever perfect though, there is room for some improvement
-
once a promise is created it will complete and call the resolve
(then) or reject (catch) callback, there's no easy way to cancel
it
-
if a promise failed, given only the promise object there's no
easy way to 'retry' the promise
observables
-
reactive programming, which uses observables, is a fairly new way
of dealing with asynchronous functions
-
angular uses the RxJS library for this (ºMicrosoft)
-
it's not just javascript, reactive extension exist for pretty much
every language you'd want to use (java, .net, c++, kotlin, swift,
python, ...)
observables
-
put simply: reactive programming is programming with asynchronous
data streams
-
we said a Promise is the future result of an operation
-
well, an observable are ALL future results of an operation, and an
immense toolbox to work with them
-
imperative code 'pulls' data where reactive code 'pushes' data,
you subscribe to get notified of changes, and those changes are
pushed to you
observables
-
you could say an array is a collection of data you get handed (and
which you'll loop over yourselves)
-
and an observable gives you a collection of data, one by one, with
certain time intervals in between. This is often visualized using
'marble diagrams', a timeline with some values (shown as circles)
-
RxJS' test framework
is designed around ASCII drawings of such marble diagrams
observables
-
this is a lot like responding to user events, e.g. click events
can be seen as an async event stream, which you observe and
respond to, a 'stream of clicks'
-
the streams are composable, think of streams as a pipeline of
operations over your data, you can subscribe to any part of the
stream or combine them to make new streams
-
working with observables requires a different way of thinking, you
subscribe to streams, and update your app based on these changes.
There is very little imperative thinking left
-
more reading about what this is all about?
"the introduction to reactive programming you've been
missing"
observables - operators
-
the real power of RxJS comes from the available operators to
combine and manipulate streams.
-
Just like can apply map,
filter,
reduce to arrays to use 'converted'
arrays, there are operators available to do the same with
observable streams
-
but there are many (many) more, let's introduce a few to show what
I mean
observables - map
-
just like with an array, there's also a
map operator, similar to an array, you
define a function to convert T to
U and your
Observable<T> becomes an
Observable<U>
observables - delay
-
there are operators who work on the timing of your stream, rather
than the values, e.g. delay will,
well, delay the values being fired
observables - merge
-
then there are operators which allow you to combine multiple
streams together, the simplest one is
merge, which simply creates a new
stream firing everything all the other streams do
pipeable operators
-
this is just the tip of the iceberg, there are many (many) more,
allowing you to sometimes do really cool stuff with little code
-
they are called
pipeable operators
-
(until 12 januari 2018 they were called 'lettable operators', if
you google)
-
RxMarbles.com has a great
visual overview of many of these
-
but there are many more resources to learn these, and more pop up
every day, reactive programming is on the rise, and not just in
the web world
recipe filter
src/app/recipe/recipe-list/recipe-list.component.ts import { Subject } from 'rxjs';
export class RecipeListComponent {
public filterRecipeName: string;
public filterRecipe$ = new Subject<string>();
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$.subscribe(
val => this.filterRecipeName = val);
}
}
}
as an example, let's update our recipe filter from chapter 2 to
become a 'live' filter, that responds as you type (and not only
after clicking a button)
recipe filter
src/app/recipe/recipe-list/recipe-list.component.ts import { Subject } from 'rxjs';
export class RecipeListComponent {
public filterRecipeName: string;
public filterRecipe$ = new Subject<string>();
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$.subscribe(
val => this.filterRecipeName = val);
}
}
}
we'll store the input field value in an observable, we use a
Subject
for this
recipe filter
src/app/recipe/recipe-list/recipe-list.component.ts import { Subject } from 'rxjs';
export class RecipeListComponent {
public filterRecipeName: string;
public filterRecipe$ = new Subject<string>();
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$.subscribe(
val => this.filterRecipeName = val);
}
}
}
Subjects can function both as an observer and as an observable,
here we'll only use it to emit new values
every letter that is typed, is a new 'event' on the
stream
recipe filter
src/app/recipe/recipe-list/recipe-list.component.ts import { Subject } from 'rxjs';
export class RecipeListComponent {
public filterRecipeName: string;
public filterRecipe$ = new Subject<string>();
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$.subscribe(
val => this.filterRecipeName = val);
}
}
}
by convention, observable variables' names end in a $ (stream),
similar to the _ at the start of private properties
recipe filter
src/app/recipe/recipe-list/recipe-list.component.ts import { Subject } from 'rxjs';
export class RecipeListComponent {
public filterRecipeName: string;
public filterRecipe$ = new Subject<string>();
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$.subscribe(
val => this.filterRecipeName = val);
}
}
}
so the observable will 'fire' new values in a 'stream', if you
want to act on those you have to subscribe
recipe filter
src/app/recipe/recipe-list/recipe-list.component.ts import { Subject } from 'rxjs';
export class RecipeListComponent {
public filterRecipeName: string;
public filterRecipe$ = new Subject<string>();
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$.subscribe(
val => this.filterRecipeName = val);
}
}
}
unlike promises, observables only 'start' when someone
subscribes (explictly or implicitly)
recipe list
src/app/recipe/recipe-list/recipe-list.component.html <mat-form-field>
<input matInput (keyup)='filterRecipe$.next($event.target.value)'
type='text'
placeholder='filter recipe name...' #filter>
</mat-form-field>
<button (click)="applyFilter(filter.value)" mat-raised-button>
filter
</button>
on the html side, we'll remove the button and add a keyup event
listener
recipe list
src/app/recipe/recipe-list/recipe-list.component.html <mat-form-field>
<input matInput (keyup)='filterRecipe$.next($event.target.value)'
type='text'
placeholder='filter recipe name...' #filter>
</mat-form-field>
<button (click)="applyFilter(filter.value)" mat-raised-button>
filter
</button>
on the html side, we'll remove the button and add a keyup event
listener
recipe list
src/app/recipe/recipe-list/recipe-list.component.html <mat-form-field>
<input matInput (keyup)='filterRecipe$.next($event.target.value)'
type='text'
placeholder='filter recipe name...' #filter>
</mat-form-field>
<button (click)="applyFilter(filter.value)" mat-raised-button>
filter
</button>
everytime a keyup happens, we 'next' a new value in our stream,
triggering all subscribers their subscribe functions
recipe list
src/app/recipe/recipe-list/recipe-list.component.html <mat-form-field>
<input matInput (keyup)='filterRecipe$.next($event.target.value)'
type='text'
placeholder='filter recipe name...' #filter>
</mat-form-field>
<button (click)="applyFilter(filter.value)" mat-raised-button>
filter
</button>
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
this works, but the real power comes from applying the pipeable
operators
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
you do this by passing one or more function to the pipe function
on an observable
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
let's say you only want to get called when the input changes
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
and then only once every 400 milliseconds
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
and you want to convert the filter to lowercase before passing
it
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
and then only pass those not starting with an 's'
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
and so on, and so on, these are really powerful things
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
they can all be found in 'rxjs/operators'
recipe list
src/app/recipe/recipe-list/recipe-list.component.ts import { distinctUntilChanged, debounceTime,
map, filter } from 'rxjs/operators';
constructor(private _recipeDataService: RecipeDataService) {
this.filterRecipe$
.pipe(
distinctUntilChanged(),
debounceTime(400),
map(val => val.toLowerCase()),
filter(val => !val.startsWith('s'))
)
.subscribe(val => (this.filterRecipeName = val));
}
87dd96b
http - observables
-
in Angular, http request will always return an observable
-
this is done so you can easily cancel and retry requests (http
responses are not real 'streams' of data, there is always just one
result)
HttpClientModule
src/app/recipe/recipe.module.ts @NgModule({
declarations: [
RecipeComponent,
IngredientComponent,
RecipeListComponent,
AddRecipeComponent,
RecipeFilterPipe
],
imports: [
CommonModule,
HttpClientModule,
MaterialModule
],
exports: [RecipeListComponent]
})
export class RecipeModule {}
first we need to add the
HttpClientModule
HttpClientModule
src/app/recipe/recipe.module.ts @NgModule({
declarations: [
RecipeComponent,
IngredientComponent,
RecipeListComponent,
AddRecipeComponent,
RecipeFilterPipe
],
imports: [
CommonModule,
HttpClientModule,
MaterialModule
],
exports: [RecipeListComponent]
})
export class RecipeModule {}
start by adding HttpClientModule to
the app module (remember: NOT HttpModule)
CORS
-
before we can use our API from Angular we have to make sure our
cross domain calls work (http on port 4200 to https on port 5001
doesn't work out of the box)
-
for security reasons, web browsers do not allow Ajax requests to
servers other than the site you're visiting ('same-origin policy')
-
while developing it's easiest to set up a proxy server (for our
webpack) to handle this
Proxy
./proxy.conf.json {
"/api": {
"target": {
"host": "localhost",
"protocol": "https:",
"port": 5001
},
"secure": false,
"changeOrigin": true,
"logLevel": "info"
}
}
create a new file proxy.conf.json,
inside the root of our app (next to
package.json) with the following
contents
Proxy
./proxy.conf.json {
"/api": {
"target": {
"host": "localhost",
"protocol": "https:",
"port": 5001
},
"secure": false,
"changeOrigin": true,
"logLevel": "info"
}
}
this means that every call to /api will be redirected to
https://localhost:5001/api
Proxy
./proxy.conf.json {
"/api": {
"target": {
"host": "localhost",
"protocol": "https:",
"port": 5001
},
"secure": false,
"changeOrigin": true,
"logLevel": "info"
}
}
so e.g. http://localhost:4200/api/recipes becomes
https://localhost:5001/api/recipes
but http://localhost:4200/recipes remains
http://localhost:4200/recipes, exactly what we want
Proxy
~$ ng serve --proxy-config proxy.conf.json
-
if you want to use this you have to pass it as a command line
parameter
-
let's add it to the package.json scripts so we don't have to type
this every time
package.json"scripts": {
"start": "ng serve --proxy-config proxy.conf.json",
}
-
so from now on, we no longer start our environment with 'ng
serve' but with 'npm start'
environment.
-
we'd rather not have our backend url hardcoded in the source code
-
it's also not unlikely we'd use a different backend server during
production and when developing, Angular uses the environment
mechanism for this
-
by default you have an
environments folder in the root folder
with two files, one regular and one for production
-
we'll add a new variable apiUrl to
both, pointing to '/api', which is where our frontend can find our
backend
environments/environment.tsexport const environment = {
production: false,
apiUrl: '/api'
};
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
we'll update the data service to do all communication with our
API
you'll typically keep API access contained in your
services
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
we start by injecting the
HttpClient
making it available as
this.http
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
we no longer use the mock object, nor will we keep a copy of
the recipes in our service
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
we'll return the result of a
http.get request, this is an async
operation, and it returns an Observable
notice how we use the environment variable here
when the request is completed it will 'push' a json array to
the stream
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
so our get recipes() will become
async as well, we will return an Observable to an array of
Recipes
now we still need to convert the json array from the API, to an
array of our Recipe model objects
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
you can perform all kind of conversions by piping the result
through various RxJS operators
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
for example, the map operator,
which you supply a function to, to convert the object wrapped in
the observable
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
in our case, we got an
Observable<any[]> from
http.get, and want an
Observable<Recipe[]>
so we supply a function that converts an
any[] to a
Recipe[]
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
we achieve this by calling the javascript map function on the
any array, converting each element using the
fromJSON static method we created
before
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
adding explicit types to the arrow function signature is of
course not required, but this is typically one of those spots
where static type checking can really help you
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
a usefull operator when dealing with observables it
tap
tap will simply pass everything it gets to the next operator,
leaving it untouched, but you get a change to 'tap into' the
stream
recipedataservice.
src/app/recipe.data.service.ts import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class RecipeDataService {
private _recipes = RECIPES;
constructor(private http: HttpClient) {}
get recipes$(): Observable< Recipe[] > {
return this._recipes;
return this.http.get(`${environment.apiUrl}/recipes/`)).pipe(
tap(console.log),
map(
(list: any[]): Recipe[] => {}list.map(Recipe.fromJSON)
)
);
}
}
that's it, not a lot of code, but a lot is happening here; it's
important to really understand this
now all that's left is subscribing to this service where we
need the data
http.
src/app/recipe/recipe-list/recipe-list.component.html export class RecipeListComponent {
get recipes$(): Observable<Recipe[]> {
return this._recipeDataService.recipes$;
}
}
our app component will have a compile error
http.
src/app/recipe/recipe-list/recipe-list.component.html export class RecipeListComponent {
get recipes$(): Observable<Recipe[]> {
return this._recipeDataService.recipes$;
}
}
so we adapt the new property name (with '$'), and change the
return type to an observable
http - observable
- if we have a look in the browser, nothing will be shown
- the console has an error that tells us what's going wrong
-
it basically says ngFor only works for
Iterables such as
Arrays
-
inside our html, we loop over our 'recipes' property, but it
became on Observable of Recipe[], we can no longer simply do that
http - observable
-
there's more, it's not just that we can't simply loop over the
result of an Observable, there *is* no result
-
if we would have used Promises, the http call would have already
happened, we'd only need to adapt the way we show the result
-
but here nothing has happened yet, if you look in the backend
logs, there's nothing there
-
Observables are 'cold' constructs, as long as nobody subscribes,
nothing happens
-
so we need to subscribe, wait for the async result, and then
capture it and loop over the list
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
so we could adapt our AppComponent to subscribe, copy the
results and show those
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
so add a variable to cache the results
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
subscribe to our services, and as the results come in, store
them in our cache
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
and change the get recipes to
return our cache list
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
this works, and it's not necessarily bad
but we are needlessly caching, keeping state
always avoid keeping state if you can
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
this can be solved better using the
AsyncPipe
by adding | async in the html, we
tell the system to subscribe asynchronously, and return the
result as soon as it comes in, and move on as normal afterwards
so, exactly what we want
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
we add a new variable with a reference to the recipes stream of
our data service
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
http - observable
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeComponent {
private _recipes: Recipes[];
private _fetchRecipes$: Observable<Recipe[]>
= this._recipeDataService.recipes$;
constructor(private _recipeDataService: RecipeDataService) {
this._recipeDataService.recipe$.subscribe(
res => this._recipes = res
);
}
get recipes$(): Observable<Recipe[]> {
return this._fetchRecipes$this._recipesthis._recipeDataService.recipes$;
}
and we simply return this new stream
now all that's left is adding the async pipe in our html
http - observable
src/app/app.component.html <div>
<div fxLayout="row" [...] >
<div fxFlex="0 0 calc(25%-0.5%)"
*ngFor="
let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)
"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
97466f4
in our html code we simply add the
| async to the the recipes list
this will subscribe to the observable, and process the results
as they arrive
http - observable
src/app/app.component.html <div>
<div fxLayout="row" [...] >
<div fxFlex="0 0 calc(25%-0.5%)"
*ngFor="
let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)
"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
97466f4
you might wonder why we copied the observable in our class, and
didn't just pass the result of the recipe dataservice to our
async pipe?
async pipe
-
change detection! An async pipe whose input resolves triggers a
change detection cycle
-
so the (recipes$ | async) would
trigger get recipes() in
RecipeDataService, which triggers a
http get call
-
when the http get call resolves, async pipe signals to the change
detection system that something changed, on this new cycle
(recipes$ | async) are requested
again, triggering get recipes() on
RecipedataService once more, and hence
a new http get call is triggered
-
which returns a new Observable as well, which returns new
results, so the async pipe has changed again, and triggers a new
cycle!
-
and so on and so on, you end up in an endless loop, because a http
get always creates a new observable, so we have to avoid this
- let's try it
ingredient model
-
so that mostly works, but we get all these
[Object object]'s for our ingredients
-
this is because our frontend still stores ingredients as strings,
while the API returns proper Ingredient objects
-
this can be solved by adding an Ingredient model class, adapting
the ingredient component, and using the model when converting the
recipe
-
this is extremely similar to what we've done for Recipe, I
consider this an exercise
-
(if you have trouble with this, it's probably advised to start
studying for this course)
-
(and if you reeeaally can't figure it out: commit
837cc7c)
loading...
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loading">
<div
fxLayout="row wrap"
fxLayout.xs="column"
fxLayoutWrap
fxLayoutGap="0.5%"
fxLayoutAlign="left"
>
<div
*ngFor="let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loading><mat-spinner></mat-spinner></ng-template>
our recipes load asynchronously, depending on the speed of the
network (and server) this could take a while
it would be better if we could show a 'loading' message (or
animation)
loading...
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loading">
<div
fxLayout="row wrap"
fxLayout.xs="column"
fxLayoutWrap
fxLayoutGap="0.5%"
fxLayoutAlign="left"
>
<div
*ngFor="let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loading><mat-spinner></mat-spinner></ng-template>
to achieve this, first we add an
ng-template with a material design
spinner which we'll show while loading
ng-template's define html building
blocks, which are not shown until you explicitly include
them
loading...
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loading">
<div
fxLayout="row wrap"
fxLayout.xs="column"
fxLayoutWrap
fxLayoutGap="0.5%"
fxLayoutAlign="left"
>
<div
*ngFor="let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loading><mat-spinner></mat-spinner></ng-template>
to show either the list of recipes, or our loading message, we
use an *ngIf
just like *ngFor loops,
*ngIf gives us a conditional
expression
loading...
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loading">
<div
fxLayout="row wrap"
fxLayout.xs="column"
fxLayoutWrap
fxLayoutGap="0.5%"
fxLayoutAlign="left"
>
<div
*ngFor="let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loading><mat-spinner></mat-spinner></ng-template>
all children of the tag containing the
*ngIf are shown if the condition is
met
(in this case, if your
AsyncPipe resolves, and the results
are put in recipes)
loading...
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loading">
<div
fxLayout="row wrap"
fxLayout.xs="column"
fxLayoutWrap
fxLayoutGap="0.5%"
fxLayoutAlign="left"
>
<div
*ngFor="let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loading><mat-spinner></mat-spinner></ng-template>
and if not (else case) we show an
ng-template defined elsewhere
mat-spinner can be found in the
MatProgressSpinnerModule
loading...
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loading">
<div
fxLayout="row wrap"
fxLayout.xs="column"
fxLayoutWrap
fxLayoutGap="0.5%"
fxLayoutAlign="left"
>
<div
*ngFor="let localRecipe of (recipes$ | async | recipeFilter: filterRecipeName)"
>
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loading><mat-spinner></mat-spinner></ng-template>
testing this is bit hard, you're probably not fast enough to
see if this works (unless if your computer is really old)
loading...
src/app/recipe.data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
delay(2000),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
}
eb23d85
RxJS to the rescue
you can pipe through the delay operator first
2 seconds in this example, should be enough to alt-tab and see
the loading message
loading...
src/app/recipe.data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
delay(2000),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
}
eb23d85
don't forget to remove this delay again after you've finished
testing...
error handling
-
it's important to realize any given stream can only
error out once
-
streams can complete, ending their lifecycle without any error,
the stream will no longer produce values
-
an alternative to completion is erroring out, the stream's
lifecycle ended with an error, it will no longer emit values
-
notice that there is no obligation for a stream to complete or
error out, both are optional, but they
are mutually exclusive
error handling
-
just as Promises have a catch function, you can specify a second
function when subscribing to Observables to process any errors
that happened
- (and a third, to be called when a stream completes)
-
but we only subscribe implicitly by using our AsyncPipe, adding
error handling at the point of our AsyncPipe would be messy
-
once more, we'll rely on an RxJS operator to help us out:
catchError
error handling - service
src/app/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
handleError(err: any): Observable<never> {
let errorMessage: string;
if (err instanceof HttpErrorResponse) {
errorMessage = `'${err.status} ${err.statusText}' when accessing '${err.url}'`;
} else {
errorMessage = `an unknown error occurred ${err}`;
}
console.error(err);
return of([]);
return throwError(errorMessage);
}
}
we add a catchError operator to the
pipe
like any pipe operator, this one
gets an input observable and returns an output observable
error handling - service
src/app/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
handleError(err: any): Observable<never> {
let errorMessage: string;
if (err instanceof HttpErrorResponse) {
errorMessage = `'${err.status} ${err.statusText}' when accessing '${err.url}'`;
} else {
errorMessage = `an unknown error occurred ${err}`;
}
console.error(err);
return of([]);
return throwError(errorMessage);
}
}
as long as the original produces no error,
catchError simply passes the values
from its input observable to its output observable
error handling - service
src/app/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
handleError(err: any): Observable<never> {
let errorMessage: string;
if (err instanceof HttpErrorResponse) {
errorMessage = `'${err.status} ${err.statusText}' when accessing '${err.url}'`;
} else {
errorMessage = `an unknown error occurred ${err}`;
}
console.error(err);
return of([]);
return throwError(errorMessage);
}
}
when an error occurs however, the function passed to
catchError is called to deal with
this error
error handling - service
src/app/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
handleError(err: any): Observable<never> {
let errorMessage: string;
if (err instanceof HttpErrorResponse) {
errorMessage = `'${err.status} ${err.statusText}' when accessing '${err.url}'`;
} else {
errorMessage = `an unknown error occurred ${err}`;
}
console.error(err);
return of([]);
return throwError(errorMessage);
}
}
the original stream no longer emits values (an error occurred),
but we still need a stream to pass in the pipe, so after dealing
with the error we create a stream ourselves
this is called the
catch and replace strategy
error handling - service
src/app/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
handleError(err: any): Observable<never> {
let errorMessage: string;
if (err instanceof HttpErrorResponse) {
errorMessage = `'${err.status} ${err.statusText}' when accessing '${err.url}'`;
} else {
errorMessage = `an unknown error occurred ${err}`;
}
console.error(err);
return of([]);
return throwError(errorMessage);
}
}
we could simply return an observable created from an empty
area, which will immediately complete without emitting
values
error handling - service
src/app/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
handleError(err: any): Observable<never> {
let errorMessage: string;
if (err instanceof HttpErrorResponse) {
errorMessage = `'${err.status} ${err.statusText}' when accessing '${err.url}'`;
} else {
errorMessage = `an unknown error occurred ${err}`;
}
console.error(err);
return of([]);
return throwError(errorMessage);
}
}
or we'll use the static
throwError function from RxJS, which
will never return a value but immediatelly error out
itself
now let's update the component so we can present this error to
the user
error handling - component
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeListComponent implements OnInit {
private _fetchRecipes$: Observable<Recipe[]> = this._recipeDataService
.recipes$;
public errorMessage: string = '';
ngOnInit(): void {
this._fetchRecipes$ = this._recipeDataService.recipes$.pipe(
catchError(err => {
this.errorMessage = err;
return EMPTY;
})
);
}
}
we'll change how we deal with the fetchRecipes$ stream
error handling - component
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeListComponent implements OnInit {
private _fetchRecipes$: Observable<Recipe[]> = this._recipeDataService
.recipes$;
public errorMessage: string = '';
ngOnInit(): void {
this._fetchRecipes$ = this._recipeDataService.recipes$.pipe(
catchError(err => {
this.errorMessage = err;
return EMPTY;
})
);
}
}
we'll pipe it through a catchError,
to capture the error we rethrow with
throwError
and assign that one to a new variable that will hold the error
message
error handling - component
src/app/recipe/recipe-list/recipe-list.component.ts export class RecipeListComponent implements OnInit {
private _fetchRecipes$: Observable<Recipe[]> = this._recipeDataService
.recipes$;
public errorMessage: string = '';
ngOnInit(): void {
this._fetchRecipes$ = this._recipeDataService.recipes$.pipe(
catchError(err => {
this.errorMessage = err;
return EMPTY;
})
);
}
}
so if we never get an error, nothing changes, but if any error
occurs the this.errorMessage will
hold it
error handling - component
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loadingloadingOrError">
<div
fxLayout="row"
>
<div *ngFor="let localRecipe of (recipes | recipeFilter: filterRecipeName)">
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loadingOrError>
<mat-card class="error" *ngIf="errorMessage; else loading">
Error loading the recipe list: {{ errorMessage }}. <br/>
Please try again later.
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
</mat-card>
</ng-template>
4fd078b
here we do pretty much the same thing we did for loading
earlier
error handling - component
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loadingloadingOrError">
<div
fxLayout="row"
>
<div *ngFor="let localRecipe of (recipes | recipeFilter: filterRecipeName)">
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loadingOrError>
<mat-card class="error" *ngIf="errorMessage; else loading">
Error loading the recipe list: {{ errorMessage }}. <br/>
Please try again later.
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
</mat-card>
</ng-template>
4fd078b
we change the template shown if the recipes are not loaded
(it's because they're either still loading, or because an error
occurred)
error handling - component
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loadingloadingOrError">
<div
fxLayout="row"
>
<div *ngFor="let localRecipe of (recipes | recipeFilter: filterRecipeName)">
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loadingOrError>
<mat-card class="error" *ngIf="errorMessage; else loading">
Error loading the recipe list: {{ errorMessage }}. <br/>
Please try again later.
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
</mat-card>
</ng-template>
4fd078b
the template is very similar to before, if there is no error
reported on the stream, we're still loading
error handling - component
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loadingloadingOrError">
<div
fxLayout="row"
>
<div *ngFor="let localRecipe of (recipes | recipeFilter: filterRecipeName)">
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loadingOrError>
<mat-card class="error" *ngIf="errorMessage; else loading">
Error loading the recipe list: {{ errorMessage }}. <br/>
Please try again later.
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
</mat-card>
</ng-template>
4fd078b
and as soon as an error occurred, we show the
errorMessage that was signaled on
the stream
error handling - component
src/app/recipe/recipe-list/recipe-list.component.html <div *ngIf="(recipes$ | async) as recipes; else loadingloadingOrError">
<div
fxLayout="row"
>
<div *ngFor="let localRecipe of (recipes | recipeFilter: filterRecipeName)">
<app-recipe [recipe]="localRecipe"></app-recipe>
</div>
</div>
</div>
<ng-template #loadingOrError>
<mat-card class="error" *ngIf="errorMessage; else loading">
Error loading the recipe list: {{ errorMessage }}. <br/>
Please try again later.
<ng-template #loading>
<mat-spinner></mat-spinner>
</ng-template>
</mat-card>
</ng-template>
4fd078b
the easiest way to test this, is to adapt the REST API to
return a StatusCode(500) when we
request our recipes
http post
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError), map(Recipe.fromJSON))
.subscribe();
}
}
next we'll tackle http POST requests
luckily this is all very similar to GET
http post
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError), map(Recipe.fromJSON))
.subscribe();
}
}
i.s.o. get we do
post to the same url we used to
retrieve recipes
but now we also need to pass a body when performing the
HttpRequest, with a json representation of our recipe
http post
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError), map(Recipe.fromJSON))
.subscribe();
}
}
so we'll simply add a toJSON method
to recipe and pass that
(not shown, i'm sure you can manage)
http post
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
constructor(private http: HttpClient) {}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError), map(Recipe.fromJSON))
.subscribe();
}
}
that's it for the data service, but if we'd try now, nothing
would happen when we click the add recipe button
as said before, Observables are cold, as long as nobody
subscribes, they do not start
http post
-
the recipe gets added, but it doesn't get added to the list of
recipes automatically, we need to manually refresh
-
while this makes sense (the methods retrieving the recipes can't
automatically know something was added in the backend) this is not
what we want
- there are essentially two ways around this
-
you either make sure the list gets refreshed when needed (so a
GET of all recipes happens everytime a recipe was succesfully
added or removed)
-
you keep a local cache of the recipes and update it when you add
or remove a recipe
- both have their advantages / disadvantages and their uses
local cache
advantage
-
less http requests, meaning a faster webapp, and you need less
server resources
- user action feedback is immediatelly reflected in the UI
disadvantage
- what if multiple people manipulate the data?
-
even if it's just you, keeping state in sync correctly is HARD
(see lesson 9)
local cache
-
it greatly depends on the use case as well, when designing a stock
market ticker, or an orderpage for tomorrowland tickets you most
definitely do not want to much caching
-
we'll have a lot more to say about state management later, for now
we'll create a local array with a copy of the recipes and provide
observable access to that array
-
I'd like to stress again that this is FAR from a good solution to
this problem, you just haven't learned enough rxjs to do this
properly
- we'll get back to this...
local copy of the recipes
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
private _recipes$ = new BehaviorSubject<Recipe[]>([]);
private _recipes: Recipe[];
constructor(private http: HttpClient) {
this.recipes$.subscribe((recipes: Recipe[]) => {
this._recipes = recipes;
this._recipes$.next(this._recipes);
});
}
get allRecipes$(): Observable<Recipe[]> {
return this._recipes$;
}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(this.handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError),map(Recipe.fromJSON))
.subscribe((rec: Recipe) => {
this._recipes = [...this._recipes, rec];
this._recipes$.next(this._recipes);
});
}
we'll keep a copy of all recipes in a local array
while we could simply grant access to this array to the
components, we want to keep our public interface using
observables (we'll change the inner workings of this class
later)
local copy of the recipes
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
private _recipes$ = new BehaviorSubject<Recipe[]>([]);
private _recipes: Recipe[];
constructor(private http: HttpClient) {
this.recipes$.subscribe((recipes: Recipe[]) => {
this._recipes = recipes;
this._recipes$.next(this._recipes);
});
}
get allRecipes$(): Observable<Recipe[]> {
return this._recipes$;
}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(this.handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError),map(Recipe.fromJSON))
.subscribe((rec: Recipe) => {
this._recipes = [...this._recipes, rec];
this._recipes$.next(this._recipes);
});
}
we'll initialize the array with the original recipes from the
backend
and update the list when a recipe is added
local copy of the recipes
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
private _recipes$ = new BehaviorSubject<Recipe[]>([]);
private _recipes: Recipe[];
constructor(private http: HttpClient) {
this.recipes$.subscribe((recipes: Recipe[]) => {
this._recipes = recipes;
this._recipes$.next(this._recipes);
});
}
get allRecipes$(): Observable<Recipe[]> {
return this._recipes$;
}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(this.handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError),map(Recipe.fromJSON))
.subscribe((rec: Recipe) => {
this._recipes = [...this._recipes, rec];
this._recipes$.next(this._recipes);
});
}
we'll then provide observable access to this list using a new
subject
local copy of the recipes
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
private _recipes$ = new BehaviorSubject<Recipe[]>([]);
private _recipes: Recipe[];
constructor(private http: HttpClient) {
this.recipes$.subscribe((recipes: Recipe[]) => {
this._recipes = recipes;
this._recipes$.next(this._recipes);
});
}
get allRecipes$(): Observable<Recipe[]> {
return this._recipes$;
}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(this.handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError),map(Recipe.fromJSON))
.subscribe((rec: Recipe) => {
this._recipes = [...this._recipes, rec];
this._recipes$.next(this._recipes);
});
}
and resend the array of recipes whenever it's updated through
this subjectarray
local copy of the recipes
src/app/recipe/recipe-data.service.ts export class RecipeDataService {
private _recipes$ = new BehaviorSubject<Recipe[]>([]);
private _recipes: Recipe[];
constructor(private http: HttpClient) {
this.recipes$.subscribe((recipes: Recipe[]) => {
this._recipes = recipes;
this._recipes$.next(this._recipes);
});
}
get allRecipes$(): Observable<Recipe[]> {
return this._recipes$;
}
get recipes$(): Observable<Recipe[]> {
return this.http.get(`${environment.apiUrl}/recipes/`).pipe(
catchError(this.handleError),
map((list: any[]): Recipe[] => list.map(Recipe.fromJSON))
);
}
addNewRecipe(recipe: Recipe) {
return this.http
.post(`${environment.apiUrl}/recipes/`, recipe.toJSON())
.pipe(catchError(this.handleError),map(Recipe.fromJSON))
.subscribe((rec: Recipe) => {
this._recipes = [...this._recipes, rec];
this._recipes$.next(this._recipes);
});
}
all that's left now is updating the component to use
allRecipes i.s.o.
recipes
that's all folks
-
this will do for now, we'll tackle this for real when we learn
about more advanced rxjs operators
-
so that's it for today, we covered a bit of reactive programming
and how to perform http requests
-
as an exercise try to add a trashcan button to each recipe and
call a http delete when clicked (solution: see
ffec8a5)
-
note that you know most things you need for your task now, we
still need to cover forms and routing, but if you ignore forms for
now, and show everything in one big html (so you don't need
routing yet), you can do pretty much everything needed
- so no excuses! start working
1
use the api
Karine Samyn, Benjamin Vertonghen, Thomas Aelbrecht, Pieter Van Der
Helst
"What one programmer can do in one month, two programmers can do in
two months." - Fred Brooks