13

Saving, Deleting, and Updating with NgRx

In the previous chapter, we learned about the concepts and features of NgRx. We learned the importance of state management as it provides a single source for the application to have a unidirectional data flow and reduces the responsibility of components. We also learned the building blocks of NgRx, which are actions, effects, reducers, and selectors. Lastly, we implemented the getting and displaying of the anti-heroes list feature using NgRx in our application.

In this chapter, we will now complete our application’s missing features – saving, deleting, and updating data by still using NgRx.

In this chapter, we will cover the following topics:

  • Removing an item without side effects using NgRx
  • Removing an item with side effects using NgRx
  • Adding an item with side effects using NgRx
  • Updating an item with side effects using NgRx

Technical requirements

The link to the finished version of the code is https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-13/superheroes.

Removing an item without a side effect using NgRx

In this section, we will first see how to delete items without using side effects in NgRx. As we learned in the previous chapter, side effects are used to call external APIs to retrieve data. This means that without using effects, we will delete the data by dispatching an action to invoke a reducer base on the dispatched type. This section will help us see the difference in the flow and behavior of using effects in the application.

Creating the delete action

The first step is to create the action for the delete feature. In our project, in the anti-hero/state/anti-hero.actions.ts file, we will add a new action interface and a new function for deletion.

Let’s have a look at the implementation of the following code:

export enum AntiHeroActions {
  GET_ANTI_HERO_LIST = '[Anti-Hero] Get Anti-Hero list',
  SET_ANTI_HERO_LIST = '[Anti-Hero] Set Anti-Hero list',
  REMOVE_ANTI_HERO_STATE =
   '[Anti-Hero] Remove ALL Anti-Hero (STATE)',
}
export const removeAntiHeroState = createAction(
    AntiHeroActions.REMOVE_ANTI_HERO_STATE,
  props<{ antiHeroId: string }>()
);

In the preceding code example, we can see that we have added a new action named REMOVE_ANTI_HERO_STATE. We have also created an action with the newly created type, which has a props parameter that accepts an anti-hero ID. The ID is needed for the reducer to identify what data we should delete from our store.

Creating the delete reducer

Now, let’s create the reducer for deleting data from our store. The first thing we need to think of is what our reducer would look like if it could remove a single piece of data from an array using the provided ID. One way we can implement this is by using the filter() function to extract the data in the array.

Let’s add the following code in the anti-hero/state/anti-hero.reducers.ts file:

export const antiHeroReducer = createReducer(
  initialState,
  on(setAntiHeroList, (state, { antiHeroes }) => { return
    {...state, antiHeroes}}),
  on(removeAntiHeroState, (state, { antiHeroId }) => {
    return {...state, antiHeroes:
      state.antiHeroes.filter(data => data.id !=
                              antiHeroId)}
  }),
);

In the preceding code example, we can see that we have added a new reducer for our delete feature. This accepts the anti-hero ID coming from the removeAntiHeroState action and returns the new state with the modified antiHeroes value where the anti-hero data that has the given ID is already filtered. If the reducer successfully modifies the value of the antiHeroes state, any selectors subscribed to the changes of this state will emit the new value in the component.

Dispatching the action

The last step we need to do is to dispatch the action in our component. To implement this step, we need to call a dispatch when the Delete button for each piece of anti-hero data is clicked.

In the anti-hero/components/anti-hero-list.component.ts file, we have added emittethatch, which passes the selected anti-hero object and TableAction, based on the button clicked by the user.

Let’s have a recap of the code we have implemented for this feature in the following files:

anti-hero-list.component.ts

// See full code on https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-13 /
export class AntiHeroListComponent implements OnInit {
   // other code for component not displayed
  @Output() antiHero = new EventEmitter<{antiHero:   AntiHero, action :TableActions}>();
  selectAntiHero(antiHero: AntiHero, action: TableActions)  {
    this.antiHero.emit({antiHero, action});
 }
}

anti-hero-list.component.html

// See full code on https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-13
<button (click)="selectAntiHero(element, 1)" mat-raised-button color="warn">
           <mat-icon>delete</mat-icon> Delete
</button>

table-actions.enum.ts

export enum TableActions {
  View,
   Delete
}

In the preceding code example, we can see that if the Delete button is clicked, this will emit the whole anti-hero object and the value 1, which represents the value for Delete enum.

Now, we need to dispatch the REMOVE_ANTI_HERO_STATE action in the list component when the Delete button has emitted an event. To implement this part, we will add the following code in the following files:

list.component.ts

  selectAntiHero(data: {antiHero: AntiHero, action:
    TableActions}) {
    switch(data.action) {
      case TableActions.Delete: {
        this.store.dispatch({type:
          AntiHeroActions. REMOVE_ANTI_HERO_STATE,
          antiHeroId: data.antiHero.id});
        return;
      }
      default: ""
    }
  }

list.component.html

// See full code on https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-13
<!-- Dumb component anti hero list -->
<app-anti-hero-list [antiHeroes]="antiHeroes" (antiHero)="selectAntiHero($event)" [headers]="headers"></app-anti-hero-list>

In the preceding code example, we created a function that checks the action triggered by the user with the TableActions value. If TableActions has a delete value, we will dispatch REMOVE_ANTI_HERO_STATE and pass the ID of the anti-hero object that will be used by the reducer we have created.

We have now successfully implemented the delete feature of our application with NgRx, but in this case, we are only deleting the items in our UI, and we are not syncing the changes in the database.

In the next section, we will implement the use of side effects in the deleting of data.

Removing an item with side effects using NgRx

In this section, we will improve the delete functionality by adding effects in our state. Our current delete feature only removes the data in the store but does not sync the changes in the database. This means that if we refresh our application, the data that we have deleted will be available again.

To sync the changes in the database, what we should do is create an effect that will invoke the delete API. Let’s have a look at the step-by-step changes in our code in the following sections.

Creating a new action type

The first step we need to do is create a new action type. The effects in NgRx will use the new action type for deleting feature later.

We will add REMOVE_ANTI_HERO_API in the AntiHeroActions enum under the anti-hero/state/anti-hero.actions.ts file.

Let’s have a look at the added action in the following code:

export enum AntiHeroActions {
  GET_ANTI_HERO_LIST = '[Anti-Hero] Get Anti-Hero list',
  SET_ANTI_HERO_LIST = '[Anti-Hero] Set Anti-Hero list',
  REMOVE_ANTI_HERO_API =
    '[Anti-Hero] Remove Anti-Hero (API)',
  REMOVE_ANTI_HERO_STATE =
    '[Anti-Hero] Remove Anti-Hero (STATE)',
}

We can see in the preceding code example that a new action type was added for our actions. Take note that we do not need to create a new action for this type as we will be calling an effect instead of an action once this action type is dispatched.

Creating the delete effect

The next step we need to do is to create the effect for the delete feature. In the anti-hero/state/anti-hero.effect.ts file, we will add the following code:

 // See full code on https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-13
 removeAntiHero$ = createEffect(() => {
    return this.actions$.pipe(
        ofType(AntiHeroActions.REMOVE_ANTI_HERO_API),
        mergeMap((data: { payload: string}) =>
          this.antiHeroService.deleteAntiHero(data.payload)
          .pipe(
            map(() => ({ type:
              AntiHeroActions.REMOVE_ANTI_HERO_STATE,
              antiHeroId: data.payload })),
            catchError(() => EMPTY)
          ))
        )
    }, {dispatch: true}
  );

In the preceding code example, we can see that we have created a new effect for our delete action; this has a type of REMOVE_ANTI_HERO_API, which calls the deleteAntiHero() function in AntiHeroService for the data deletion based on the passed ID, and once the API is successful.

The effect will dispatch another action, REMOVE_ANTI_HERO_STATE, which we created in the previous section, which removes the anti-hero from the store. This means that the data we delete from the database will also be deleted from our NgRx store.

Modifying the dispatch

The last step for this feature is to modify the action being dispatched in the list.component.ts file. In the previous section, we call the REMOVE_ANTI_HERO_STATE action directly in our component; we will change this into REMOVE_ANTI_HERO_API as we should now call the effect, which will invoke the API and at the same time will call the REMOVE_ANTI_HERO_STATE action.

Let’s have a look at the following code example:

 selectAntiHero(data: {antiHero: AntiHero, action: TableActions}) {
    switch(data.action) {
      case TableActions.Delete: {
        this. store.dispatch({type:
          AntiHeroActions.REMOVE_ANTI_HERO_API,
          payload: data.antiHero.id});
        return;
      }
      default: ""
    }
  }

In the preceding code example, we are now dispatching the effect in our list component. This will call the API first before updating our store in the application; the changes in our store and database are synced.

In the next section, we will implement the addition of data with side effects to our application.

Adding an item with side effects using NgRx

In this section, we will implement the add functionality with side effects in NgRx. The steps are similar to how we implemented the delete feature. We will create the building blocks step by step and create the dispatch logic in our component.

Creating the actions

The first step we need to do is create the required action types and actions for our add feature. To implement the actions, we can think of how we created the actions for the delete feature.

The concept is the same. There are two action types that we need to create, and these are ADD_ANTI_HERO_API and ADD_ANTI_HERO_STATE. The first type will be used by the effect that will call the API, and the second type will be used by the reducer that will modify the state by adding the newly created data.

After creating the two action types, we also need to create an action using the createAction() function for the ADD_ANTI_HERO_STATE type. The effect will dispatch this once the API has been successfully called.

Let’s have a look at the following code implementation:

 // See full code on https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-13
export enum AntiHeroActions {
  GET_ANTI_HERO_LIST = '[Anti-Hero] Get Anti-Hero list',
  SET_ANTI_HERO_LIST = '[Anti-Hero] Set Anti-Hero list',
  ADD_ANTI_HERO_API = '[Anti-Hero] Add Anti-Hero (API',
  ADD_ANTI_HERO_STATE = '[Ant
    i-Hero] Add Anti-Hero (STATE)',
  REMOVE_ANTI_HERO_API =
    '[Anti-Hero] Remove Anti-Hero (API)',
  REMOVE_ANTI_HERO_STATE =
    '[Anti-Hero] Remove Anti-Hero (STATE)',
}
export const addAntiHeroState = createAction(
  AntiHeroActions.ADD_ANTI_HERO_STATE,
  props<{ antiHero: AntiHero }>()
)

In the preceding code example, we can see that we have added the two new types in AntiHeroActions. We have also created a new action with the ADD_ANTI_HERO_STATE type, which accepts an antiHero property that will be pushed as a new item in the anti-hero state.

Creating the effect

The next step we need to do is to create the effect for the add feature. In the anti-hero/state/anti-hero.effect.ts file, we will add the following code:

// add anti-heroes to the database
  addAntiHero$ = createEffect(() =>{
    return this.actions$.pipe(
        ofType(AntiHeroActions.ADD_ANTI_HERO_API),
        mergeMap((data: {type: string, payload: AntiHero})
          => this.antiHeroService.addAntiHero(data.payload)
          .pipe(
            map(antiHeroes => ({ type:
              AntiHeroActions.ADD_ANTI_HERO_STATE,
              antiHero: data.payload })),
            tap(() =>
              this.router.navigate(["anti-heroes"])),
            catchError(() => EMPTY)
          ))
        )
    }, {dispatch: true})

In the preceding code example, we can see that we have created an effect similar to the effect for the delete feature. This effect uses the ADD_ANTI_HERO_API type and invokes the addAntiHero() function from antiHeroService to call the POST API to add new data to the database.

After successfully calling the POST API, the effect will dispatch the ADD_ANTI_HERO_STATE action and pass the new anti-hero data coming from the API response to be added by the reducer. We have also added a tap operator, which calls a navigate function that will navigate to the list page after creating the new anti-hero.

Creating the reducer

After creating the effects, we need to sync the changes implemented in the database with our store, and the reducer will do this.

Let’s have a look at the following code implementation:

export const antiHeroReducer = createReducer(
  initialState,
  on(setAntiHeroList, (state, { antiHeroes }) => { return
    {...state, antiHeroes}}),
  on(removeAntiHeroState, (state, { antiHeroId }) => {
    return {...state, antiHeroes:
     state.antiHeroes.filter(data => data.id !=
       antiHeroId)}
  }),
  on(addAntiHeroState, (state, {antiHero}) => {
    return {...state, antiHeroes: [...state.antiHeroes,
            antiHero]}
  }),
);

In the preceding code example, we can see that we have added a new reducer for our add feature. This accepts the new anti-hero data coming from the addAntiHeroState action and returns the new state with the modified antiHeroes value where the new anti-hero is already added in the array.

If the reducer successfully modifies the value of the antiHeroes state, any selectors subscribed to the changes of this state will emit the new value in the component.

Dispatching the action

The last step we need to do is to dispatch the action in our component. To implement this step, we will invoke the dispatch action once the Create button in the anti-hero form is clicked. In the anti-hero/components/anti-hero-form.component.ts file, we have added an emitter that passes the value of the form and the button label to identify if the action is created or updated.

Let’s have a recap of the code we have implemented for this anti-hero form:

export class AntiHeroFormComponent implements OnInit {
  @Input() actionButtonLabel: string = 'Create';
  @Output() action = new EventEmitter();
  form: FormGroup;
  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      id: [''],
      firstName: [''],
      lastName: [''],
      house: [''],
      knownAs: ['']
    })
   }
  emitAction() {
    this.action.emit({value: this.form.value,
                      action: this.actionButtonLabel})
  }
}

In the preceding code example, we can see that the anti-hero form emits the form value as an anti-hero object that will be passed to the effect.

This also gives the current action, as we will also be using this anti-hero form component for the update. Once the button is clicked, we need to have a function in the form.component.ts file that will dispatch the effect.

Let’s have a look at the following code example:

// form.component.html
<app-anti-hero-form [selectedAntiHero]="antiHero" (action)="formAction($event)"></app-anti-hero-form>
// form.component.ts
 formAction(data: {value: AntiHero, action: string}) {
    switch(data.action) {
      case "Create" : {
        this.store.dispatch({type:
          AntiHeroActions.ADD_ANTI_HERO_API,
          payload: data.value});
        return;
      }
      default: ""
    }
  }

In the preceding code example, we can see that we have created the formAction() function, which dispatches an action based on the passed value from the anti-hero form component.

This uses a switch statement, as this will also be called when the action is update. We have now successfully created the add feature for our application using the building blocks of NgRx.

In the next section, we will implement the modification of data with side effects.

Updating an item with a side effect using NgRx

In this last section, we will implement the final missing feature, which is the update functionality, where we will create the building blocks step by step and the dispatch logic in our component as we did for the add and delete features.

Creating the actions

The first step we need to do is to create the required action types and actions for our update feature. We will first create the two action types we need, which are MODIFY_ANTI_HERO_API and MODIFY_ANTI_HERO_STATE. The first type will be used by the effect that will call the API, and the second type will be used by the reducer that will modify the state by changing the data based on the new anti-hero object.

After creating the two action types, we also need to create an action using the createAction() function for the MODIFY_ANTI_HERO_STATE type. The effect will dispatch this once the API has been successfully called.

Let’s have a look at the following code implementation:

 // See full code on https://github.com/PacktPublishing/Spring-Boot-and-Angular/tree/main/Chapter-13
export enum AntiHeroActions {
  GET_ANTI_HERO_LIST = '[Anti-Hero] Get Anti-Hero list',
  SET_ANTI_HERO_LIST = '[Anti-Hero] Set Anti-Hero list',
  ADD_ANTI_HERO_API = '[Anti-Hero] Add Anti-Hero (API',
  ADD_ANTI_HERO_STATE =
    '[Anti-Hero] Add Anti-Hero (STATE)',
  REMOVE_ANTI_HERO_API =
    '[Anti-Hero] Remove Anti-Hero (API)',
  REMOVE_ANTI_HERO_STATE =
    '[Anti-Hero] Remove Anti-Hero (STATE)',
  MODIFY_ANTI_HERO_API =
    '[Anti-Hero] Modify Anti-Hero (API)',
  MODIFY_ANTI_HERO_STATE =
    '[Anti-Hero] Modify Anti-Hero (STATE)',
}
export const modifyAntiHeroState = createAction(
    AntiHeroActions.MODIFY_ANTI_HERO_STATE,
    props<{ antiHero: AntiHero }>()
);

In the preceding code example, we can see that we have added the two new types in AntiHeroActions. We have also created a new action with the MODIFY_ANTI_HERO_STATE type, which accepts an antiHero property that will be used to modify the current values in the store.

Creating the effect

The next step we need to do is to create the effect for the add feature. In the anti-hero/state/anti-hero.effect.ts file, we will add the following code:

// modify anti-heroes in the database
   modifyAntiHero$ = createEffect(() =>{
    return this.actions$.pipe(
        ofType(AntiHeroActions.MODIFY_ANTI_HERO_API),
        mergeMap((data: {type: string, payload: AntiHero})
          => this.antiHeroService.updateAntiHero(
          data.payload.id, data.payload)
          .pipe(
            map(antiHeroes => ({ type:
                AntiHeroActions.MODIFY_ANTI_HERO_STATE,
                antiHero: data.payload })),
            tap(() =>
              this.router.navigate(["anti-heroes"])),
            catchError(() => EMPTY)
          ))
        )
    }, {dispatch: true})

In the preceding code example, we can see that we have created an effect similar to the effect for the add and delete features. This effect uses the MODIFY_ANTI_HERO_API type and invokes the updateAntiHero() function from antiHeroService to call the PUT API to modify the anti-hero with the ID parameter in the database.

After successfully calling the PUT API, the effect will dispatch the MODIFY_ANTI_HERO_STATE action and pass the modified anti-hero data coming from the API response to be added by the reducer, and the same as with the add effect, we have also added a tap operator, which calls a navigate function that will navigate to the list page after modifying the anti-hero.

Creating the reducer

After creating the effects, we need to sync the changes implemented in the database with our store, and the reducer will do this.

Let’s have a look at the following code implementation:

export const antiHeroReducer = createReducer(
  initialState,
  on(setAntiHeroList, (state, { antiHeroes }) => {
    return {...state, antiHeroes}}),
  on(removeAntiHeroState, (state, { antiHeroId }) => {
    return {...state, antiHeroes:
      state.antiHeroes.filter(data => data.id !=
                              antiHeroId)}
  }),
  on(addAntiHeroState, (state, {antiHero}) => {
    return {...state, antiHeroes: [...state.antiHeroes,
                                   antiHero]}
  }),
  on(modifyAntiHeroState, (state, {antiHero}) => {
    return {...state, antiHeroes: state.antiHeroes.map(data
      => data.id === antiHero.id ? antiHero : data)}
  }),
);

In the preceding code example, we can see that we have added a new reducer for our update feature. This accepts the modified anti-hero data coming from the addAntiHeroState action and returns the new state with the modified antiHeroes value, where we replace the anti-hero with the given ID with the new object using the map() operator.

If the reducer successfully modifies the value of the antiHeroes state, any selectors subscribed to the changes of this state will emit the new value in the component.

Dispatching the action

The last step we need to do is to dispatch the action to our component. To implement this step, we will do the same steps as we did for the add feature. We will still use the anti-hero/components/anti-hero-form.component.ts file for updating the data.

The only difference here is that we will bind the selected anti-hero value in our form; the anti-hero form component should accept an anti-hero object and should patch the value in the form group.

Let’s have a look at the following code example:

export class AntiHeroFormComponent implements OnInit {
  @Input() actionButtonLabel: string = 'Create';
  @Input() selectedAntiHero: AntiHero | null = null;
  @Output() action = new EventEmitter();
  form: FormGroup;
  constructor(private fb: FormBuilder) {
    this.form = this.fb.group({
      id: [''],
      firstName: [''],
      lastName: [''],
      house: [''],
      knownAs: ['']
    })
   }
  ngOnInit(): void {
    this.checkAction();
  }
  checkAction() {
    if(this.selectedAntiHero) {
      this.actionButtonLabel = "Update";
      this.patchDataValues()
    }
  emitAction() {
    this.action.emit({value: this.form.value,
      action: this.actionButtonLabel})
  }
}

In the preceding code example, we can see that we have added the checkAction() function, which checks whether we have passed an anti-hero object in the anti-hero form component.

This indicates that if the object is not null, this will be an Update action, and we must display the selected anti-hero details in each field by binding the form using the patchValue() method.

Now let’s have the code implementation for the form component:

// form.component.html
<app-anti-hero-form [selectedAntiHero]="antiHero" (action)="formAction($event)"></app-anti-hero-form>
// form.component.ts
antiHero$: Observable<AntiHero | undefined>;
  antiHero: AntiHero | null = null;
  constructor(private router: ActivatedRoute,
    private store: Store<AppState>) {
    const id = this.router.snapshot.params['id'];
    this.antiHero$ = this.store.select(selectAntiHero(id));
    this.antiHero$.subscribe(d => {
      if(d) this.antiHero = d;
    });
   }
 formAction(data: {value: AntiHero, action: string}) {
    switch(data.action) {
      case "Create" : {
        this.store.dispatch({type:
          AntiHeroActions.ADD_ANTI_HERO_API,
          payload: data.value});
        return;
      }
     case "Update" : {
        this.store.dispatch({type:
          AntiHeroActions.MODIFY_ANTI_HERO_API,
          payload: data.value});
        return;
      }
      default: ""
    }
  }

In the preceding code example, we can see that we have added a new case in the formAction() function, which also dispatches an action but of type MODIFY_ANTI_HERO_API.

We have also used the selectAntiHero() selector to select the anti-hero using the ID in our URL route that will be passed in our anti-hero-form.component.ts file.

Summary

With this, we have reached the end of this chapter. Let’s have a recap of the valuable things we have learned; we have completed the CRUD features of applications using the building blocks of NgRx, and we have learned the difference between using and not using side effects in state management. Side effects are essential for our changes in the store to be synced with the database.

We have also learned, step by step, how to create the building blocks of NgRx with the different actions we need for our application.

In the next chapter, we will learn how to apply security features in Angular, such as adding user login and logout, retrieving user profile information, protecting application routes, and calling an API with protected endpoints.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset