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

overview.

  1. intro
    what is an async call?
  2. callbacks
    using callbacks to handle async
  3. promises
    how and why do promises handle async better
  4. observables
    the new kid in town to handle async
  5. recipe filter
    using a Subject observable to create a recipe filter
  6. HttpClientModule
    how to make http calls in Angular
  7. RecipeDataService
    using the http module in our service
  8. async pipe
    display async data using the async pipe
  9. error handling
    how to handle observable errors
  10. 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 the first function is called the program waits for the function to finish and assigns the result and only then, the next function starts executing and so on 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 we'd like to start creating the clone army while the death star is still being built that's what asynchronous programming is (in a nutshell), not waiting for a function to finish before you start the next one this leads to a couple of difficulties... when can you start using the result of an asynchronous function? remember, we're not waiting for the result of buildADeathStarASync so it's possible notAMoon doesn't hold the result yet, while we're trying to use it 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) {
              // loooong operation
              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 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! 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 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 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 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 = ...

            // How can I call processAfterBoth ONLY if both have finished??
            slowAsync( ??? )
            anotherAsync( ??? )
            
            
there are other problems with callbacks, what if you only want to continue after multiple parallel functions have finished? 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 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 so if other function calls follow the buildADeathStarAsync, they are immediately started, there's no waiting for the buildADeathStarAsync to finish 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 so a then() is not blocking either, processing continues until the buildADeathStarAsync is completed, and then the callback is called you can also provide a catch() function, which will be called if an error occurred during buildADeathStarAsync 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 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 every error handling becomes a catch, regular callbacks become a then you end up with smaller and more readable code no matter how 'deep' you are in the hierarchy, you can keep applying the same conversions again and again, until you have eliminated the 'callback hell' 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) we'll store the input field value in an observable, we use a Subject for this 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 by convention, observable variables' names end in a $ (stream), similar to the _ at the start of private properties so the observable will 'fire' new values in a 'stream', if you want to act on those you have to subscribe 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 everytime a keyup happens, we 'next' a new value in our stream, triggering all subscribers their subscribe functions that's it, let's try this out

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 you do this by passing one or more function to the pipe function on an observable let's say you only want to get called when the input changes and then only once every 400 milliseconds and you want to convert the filter to lowercase before passing it and then only pass those not starting with an 's' and so on, and so on, these are really powerful things they can all be found in 'rxjs/operators' let's try this

pipeable operators

  • one last remark before we get to http, the pipe() syntax where you pass other functions is rather new
  • so on google, stackoverflow... you'll often find code with the following syntax
    
          import 'rxjs/add/operator/map'
          import 'rxjs/add/operator/distinctUntilChanged'
          
          this.filterRecipe$
            .distinctUntilChanged()
            .debounceTime(400)
            .map(val => val.toLowerCase());
                      
  • DON'T DO THAT ANYMORE, it's always easily converted to the new syntax

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 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 this means that every call to /api will be redirected to https://localhost:5001/api 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.ts
                    export 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 we start by injecting the HttpClient making it available as this.http we no longer use the mock object, nor will we keep a copy of the recipes in our service 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 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 you can perform all kind of conversions by piping the result through various RxJS operators for example, the map operator, which you supply a function to, to convert the object wrapped in the observable 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[] we achieve this by calling the javascript map function on the any array, converting each element using the fromJSON static method we created before 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 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 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 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 so add a variable to cache the results subscribe to our services, and as the results come in, store them in our cache and change the get recipes to return our cache list this works, and it's not necessarily bad but we are needlessly caching, keeping state always avoid keeping state if you can 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 we add a new variable with a reference to the recipes stream of our data service we no longer subscribe 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 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) 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 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 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) and if not (else case) we show an ng-template defined elsewhere mat-spinner can be found in the MatProgressSpinnerModule 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 don't forget to remove this delay again after you've finished testing... let's try this out

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 as long as the original produces no error, catchError simply passes the values from its input observable to its output observable when an error occurs however, the function passed to catchError is called to deal with this error 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 we could simply return an observable created from an empty area, which will immediately complete without emitting values 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 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 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 we change the template shown if the recipes are not loaded (it's because they're either still loading, or because an error occurred) the template is very similar to before, if there is no error reported on the stream, we're still loading and as soon as an error occurred, we show the errorMessage that was signaled on the stream the easiest way to test this, is to adapt the REST API to return a StatusCode(500) when we request our recipes let's try this out

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 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 so we'll simply add a toJSON method to recipe and pass that (not shown, i'm sure you can manage) 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) we'll initialize the array with the original recipes from the backend and update the list when a recipe is added we'll then provide observable access to this list using a new subject and resend the array of recipes whenever it's updated through this subjectarray 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