NgRx Course – Finishing the CRUD Functionality

In the last video, we introduced the NgRx entity library and started using it in our application. In this video, we’re going to finish the CRUD for the customer component.

For the CRUD the initial action being dispatched by the component is going to be taken by the effect that will communicate with the server. Then this is going to dispatch a success action that will be taken by the reducer to create the new state.

So let’s start creating the actions in our customer.actions.ts file. Import the Update type and add the necessary actions to the enum type:

// customers/state/customer.reducer.ts
import { Action } from "@ngrx/store";
import { Update } from "@ngrx/entity";

import { Customer } from "../customer.model";

export enum CustomerActionTypes {
  LOAD_CUSTOMERS = "[Customer] Load Customers",
  LOAD_CUSTOMERS_SUCCESS = "[Customer] Load Customers Success",
  LOAD_CUSTOMERS_FAIL = "[Customer] Load Customers Fail",
  LOAD_CUSTOMER = "[Customer] Load Customer",
  LOAD_CUSTOMER_SUCCESS = "[Customer] Load Customer Success",
  LOAD_CUSTOMER_FAIL = "[Customer] Load Customer Fail",
  CREATE_CUSTOMER = "[Customer] Create Customer",
  CREATE_CUSTOMER_SUCCESS = "[Customer] Create Customer Success",
  CREATE_CUSTOMER_FAIL = "[Customer] Create Customer Fail",
  UPDATE_CUSTOMER = "[Customer] Update Customer",
  UPDATE_CUSTOMER_SUCCESS = "[Customer] Update Customer Success",
  UPDATE_CUSTOMER_FAIL = "[Customer] Update Customer Fail",
  DELETE_CUSTOMER = "[Customer] Delete Customer",
  DELETE_CUSTOMER_SUCCESS = "[Customer] Delete Customer Success",
  DELETE_CUSTOMER_FAIL = "[Customer] Delete Customer Fail"
}
…
// (to be continued)

Then, define these actions for the CRUD:

// customers/state/customer.reducer.ts (continued)
...
// LOAD ONE CUSTOMER
export class LoadCustomer implements Action {
  readonly type = CustomerActionTypes.LOAD_CUSTOMER;

  constructor(public payload: number) {}
}
export class LoadCustomerSuccess implements Action {
  readonly type = CustomerActionTypes.LOAD_CUSTOMER_SUCCESS;

  constructor(public payload: Customer) {}
}
export class LoadCustomerFail implements Action {
  readonly type = CustomerActionTypes.LOAD_CUSTOMER_FAIL;

  constructor(public payload: string) {}
}

// CREATE CUSTOMER
export class CreateCustomer implements Action {
  readonly type = CustomerActionTypes.CREATE_CUSTOMER;

  constructor(public payload: Customer) {}
}
export class CreateCustomerSuccess implements Action {
  readonly type = CustomerActionTypes.CREATE_CUSTOMER_SUCCESS;

  constructor(public payload: Customer) {}
}
export class CreateCustomerFail implements Action {
  readonly type = CustomerActionTypes.CREATE_CUSTOMER_FAIL;

  constructor(public payload: string) {}
}

// UPDATE CUSTOMER
export class UpdateCustomer implements Action {
  readonly type = CustomerActionTypes.UPDATE_CUSTOMER;

  constructor(public payload: Customer) {}
}
export class UpdateCustomerSuccess implements Action {
  readonly type = CustomerActionTypes.UPDATE_CUSTOMER_SUCCESS;

  constructor(public payload: Update<Customer>) {}
}
export class UpdateCustomerFail implements Action {
  readonly type = CustomerActionTypes.UPDATE_CUSTOMER_FAIL;

  constructor(public payload: string) {}
}

// DELETE CUSTOMER
export class DeleteCustomer implements Action {
  readonly type = CustomerActionTypes.DELETE_CUSTOMER;

  constructor(public payload: number) {}
}
export class DeleteCustomerSuccess implements Action {
  readonly type = CustomerActionTypes.DELETE_CUSTOMER_SUCCESS;

  constructor(public payload: number) {}
}
export class DeleteCustomerFail implements Action {
  readonly type = CustomerActionTypes.DELETE_CUSTOMER_FAIL;

  constructor(public payload: string) {}
}
...
// (to be continued)

And last, update the Action type:

// customers/state/customer.reducer.ts (continued)
export type Action =
  | LoadCustomers
  | LoadCustomersSuccess
  | LoadCustomersFail
  | LoadCustomer
  | LoadCustomerSuccess
  | LoadCustomerFail
  | CreateCustomer
  | CreateCustomerSuccess
  | CreateCustomerFail
  | UpdateCustomer
  | UpdateCustomerSuccess
  | UpdateCustomerFail
  | DeleteCustomer
  | DeleteCustomerSuccess
  | DeleteCustomerFail;

With the actions ready, we can go and start working on the effects and the reducer. Some of these actions will be handled by an effect, others will be taken by the reducer. Let’s go and create the effects for our application.

Below the Load Customer effect, add the following one for the Load Customer action:

// customers/state/customer.effects.ts
…
// LOAD CUSTOMERS
  @Effect()
  loadCustomers$: Observable<Action> = this.actions$.pipe(
    ofType<customerActions.LoadCustomers>(
      customerActions.CustomerActionTypes.LOAD_CUSTOMERS
    ),
    mergeMap((action: customerActions.LoadCustomers) =>
      this.customerService.getCustomers().pipe(
        map(
          (customers: Customer[]) =>
            new customerActions.LoadCustomersSuccess(customers)
        ),
        catchError(err => of(new customerActions.LoadCustomersFail(err)))
      )
    )
  );

// LOAD CUSTOMER
  @Effect()
  loadCustomer$: Observable<Action> = this.actions$.pipe(
    ofType<customerActions.LoadCustomer>(
      customerActions.CustomerActionTypes.LOAD_CUSTOMER
    ),
    mergeMap((action: customerActions.LoadCustomer) =>
      this.customerService.getCustomerById(action.payload).pipe(
        map(
          (customer: Customer) =>
            new customerActions.LoadCustomerSuccess(customer)
        ),
        catchError(err => of(new customerActions.CreateCustomerFail(err)
      );
// (to be continued)

The logic is the same as we talked about in the previous video about effects where we created the load customers effect. The only difference here is that we’re going to be passing the id of the customer from the action.payload to the server. Then we’re going to dispatch a new LoadCustomerSuccess action. The reducer will take to load the customer returned by the server into our store.

Now, below the load customer effect, let’s go and add the effect for the create customer action:

// customers/state/customer.effects.ts (continued)
…
  // CREATE CUSTOMER
  @Effect()
  createCustomer$: Observable<Action> = this.actions$.pipe(
    ofType<customerActions.CreateCustomer>(
      customerActions.CustomerActionTypes.CREATE_CUSTOMER
    ),
    map((action: customerActions.CreateCustomer) => action.payload),
    mergeMap((customer: Customer) =>
      this.customerService.createCustomer(customer).pipe(
        map(
          newCustomer =>
            new customerActions.CreateCustomerSuccess(newCustomer)
        ),
        catchError(err => of(new customerActions.CreateCustomerFail(err)))
      )
    )
  );
…
// (to be continued)

As we can see, we’re taking the payload from the CreateCustomer action. We are then passing that payload, which is the new customer, to the server. Then we’re dispatching a new CreateCustomerSuccess action. Passing in the new customer we just created, so the reducer will then be able to update the state.

Let’s move on and add the effect for the UpdateCustomer action below the previous one:

// customers/state/customer.effects.ts (continued)
...
  @Effect()
  updateCustomer$ = this.actions$.pipe(
    ofType<customerActions.UpdateCustomer>(
      customerActions.CustomerActionTypes.UPDATE_CUSTOMER
    ),
    map((action: customerActions.UpdateCustomer) => action.payload),
    mergeMap((customer: Customer) =>
      this.customerService.updateCustomer(customer).pipe(
        map(
          (updatedCustomer: Customer) =>
            new customerActions.UpdateCustomerSuccess({
              id: updatedCustomer.id,
              changes: updatedCustomer
            })
        )
        catchError(err => of(new customerActions.UpdateCustomerFail(err)))
      )
    )
  );
…
// (to be continued)

Here we’re doing the same thing we did with the createCustomer effect. The only difference being, that we’re dispatching the CreateCustomerSuccess passing in both the customer and the customer id. This is so the reducer can update the customer in the state.

And the last effect will be deleteCustomer:

// customers/state/customer.effects.ts (continued)
…
  @Effect()
  deleteCustomer$: Observable<Action> = this.actions$.pipe(
    ofType(customerActions.CustomerActionTypes.DELETE_CUSTOMER),
    map((action: customerActions.DeleteCustomer) => action.payload),
    mergeMap((id: number) =>
      this.customerService.deleteCustomer(id).pipe(
        map(() => new customerActions.DeleteCustomerSuccess(id)),
        catchError(err => of(new customerActions.DeleteCustomerFail(err)))
      )
    )
);

Here we’ll pass the id of the customer we want to delete to the customer service. Then dispatch a DeleteCustomerSuccess action passing, in the id of the customer, that the reducer will have to remove from the state.

Let’s go ahead and add the actions that the reducer will be responding to inside the customerReducer function:

// customers/state/customer.reducer.ts (updated)
…
export function customerReducer(
  state = initialState,
  action: customerActions.Action
): CustomerState {
  switch (action.type) {
    // LOAD CUSTOMERS
    case customerActions.CustomerActionTypes.LOAD_CUSTOMERS_SUCCESS: {
      return customerAdapter.addAll(action.payload, {
        ...state,
        loading: false,
        loaded: true
      });
    }
    case customerActions.CustomerActionTypes.LOAD_CUSTOMERS_FAIL: {
      return {
        ...state,
        entities: {},
        loading: false,
        loaded: false,
        error: action.payload
      };
    }

    // LOAD CUSTOMER
    case customerActions.CustomerActionTypes.LOAD_CUSTOMER_SUCCESS: {
    return customerAdapter.addOne(action.payload, {
        ...state,
        selectedCustomerId: action.payload.id
      });
    }
    case customerActions.CustomerActionTypes.LOAD_CUSTOMER_FAIL: {
      return {
        ...state,
        entities: {},
        loading: false,
        loaded: false,
        error: action.payload
      };
    }

    // CREATE CUSTOMER
    case customerActions.CustomerActionTypes.CREATE_CUSTOMER_SUCCESS: {
      return customerAdapter.addOne(action.payload, state);
    }
    case customerActions.CustomerActionTypes.CREATE_CUSTOMER_FAIL: {
      return {
        ...state,
        entities: {},
        loading: false,
        loaded: false,
        error: action.payload
      };
    }

    // UPDATE CUSTOMER
    case customerActions.CustomerActionTypes.UPDATE_CUSTOMER_SUCCESS: {
      return customerAdapter.updateOne(action.payload, state);
    }
    case customerActions.CustomerActionTypes.UPDATE_CUSTOMER_FAIL: {
      return {
        ...state,
        entities: {},
        loading: false,
        loaded: false,
        error: action.payload
      };
    }

    // DELETE CUSTOMER
    case customerActions.CustomerActionTypes.DELETE_CUSTOMER_SUCCESS: {
      return customerAdapter.removeOne(action.payload, state);
    }
    case customerActions.CustomerActionTypes.DELETE_CUSTOMER_FAIL: {
      return {
        ...state,
        entities: {},
        loading: false,
        loaded: false,
        error: action.payload
      };
    }

    default: {
      return state;
    }
  }
}
…
// (to be continued)

As we can see here, we’re using the methods provided by the entity adapter that we saw previously to do the logic for us and manage the state. These methods take two arguments, the payload that we’re passing in and the state that we’re returning.

If we need to add properties, e.g. in the case of the LoadCustomerSuccess where we need to add a selected CustomerId property and need to load the customer to the edit component later. We can do this by passing an object as the second argument and use the spread operator and add the property that we need.

And for the error, we’re just returning some custom properties and the error from the payload.

Finally, we’re going to add two selectors at the end of the file that we’ll need to load the selected customer in the edit component.

// customers/state/customer.reducer.ts (continued)
...
export const getCurrentCustomerId = createSelector(
  getCustomerFeatureState,
  (state: CustomerState) => state.selectedCustomerId
);
export const getCurrentCustomer = createSelector(
  getCustomerFeatureState,
  getCurrentCustomerId,
  state => state.entities[state.selectedCustomerId]
);

Now, we’re going to update the components. Let’s begin with the add-customer component. Open the customer-add.html file and update the HTML:

// customers/customer-add/customer-add.component.html
<h3>Add Customer</h3>

<form class="form-inline mb-4">
  <form [formGroup]="customerForm" (submit)="createCustomer()" class="form-inline mb-4">
    <label class="sr-only" for="Name">Name</label>
    <div class="input-group mb-2 mr-sm-2">
      <input type="text" class="form-control" formControlName="name" placeholder="name">
    </div>

    <label class="sr-only" for="Phone">Phone</label>
    <div class="input-group mb-2 mr-sm-2">
      <input type="text" class="form-control" formControlName="phone" placeholder="Phone">
    </div>

    <label class="sr-only" for="Address">Address</label>
    <div class="input-group mb-2 mr-sm-2">
      <input type="text" class="form-control" formControlName="address" placeholder="Address">
    </div>

    <label class="sr-only" for="membership">Membership</label>
    <div class="input-group mb-2 mr-sm-2">
      <select formControlName="membership" class="form-control">
        <option>Basic</option>
        <option>Pro</option>
        <option>Platinum</option>
      </select>
    </div>
    <button type="submit" class="btn btn-primary mb-2">Add Customer</button>
  </form>

We’re setting up the form and calling the createCustomer() function on submit. Now, open the add-customer.component.ts file and add the following:

// customers/customer-add/customer-add.component.ts
import { Component, OnInit } from "@angular/core";

import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { Store, State, select } from "@ngrx/store";
import * as customerActions from "../state/customer.actions";
import * as fromCustomer from "../state/customer.reducer";
import { Customer } from "../customer.model";

@Component({
  selector: "app-customer-add",
  templateUrl: "./customer-add.component.html",
  styleUrls: ["./customer-add.component.css"]
})
export class CustomerAddComponent implements OnInit {
  // 2 form group and constructor
  customerForm: FormGroup;

  constructor(
    private fb: FormBuilder,
    private store: Store<fromCustomer.AppState>
  ) {}

  ngOnInit() {
    // 3 - initialize the form with default values on init
    this.customerForm = this.fb.group({
      name: ["", Validators.required], // Set initial value to empty string
      phone: ["", Validators.required],
      address: ["", Validators.required],
      membership: ["", Validators.required]
    });
  }

  // 4 - create customer function
  createCustomer() {
    const newCustomer: Customer = {
      name: this.customerForm.get("name").value,
      phone: this.customerForm.get("phone").value,
      address: this.customerForm.get("address").value,
      membership: this.customerForm.get("membership").value
    };

    this.store.dispatch(new customerActions.CreateCustomer(newCustomer));
    this.customerForm.reset();
  }
}

Here we’re importing the dependencies that we need. Then we inject the store in the constructor, we initialize the form with some default values and we create the createCustomer() function. In this function, we declare the newCustomer object with the values from the form. We dispatch an action CreateCustomer passing in the newCustomer. Finally, we reset the form once it’s been submitted.

So now, when we refresh the page, we’ll see that if we add a new customer from our form it will be added in the back-end server. If we inspect the store, we’ll see that the actions have been dispatched and the new state now contains the new entity.

The customer-edit component is going to have a similar behaviour. Let’s open the customer-edit.component.html file and make sure it contains the HTML for the form:

// customers/customer-edit/customer-edit.component.html
<div class="mt-4">
  <h3>Edit Customer</h3>
  <form [formGroup]="customerForm" (submit)="updateCustomer()" class="form-inline mb-4">

    <label class="sr-only" for="Name">Name</label>
    <div class="input-group mb-2 mr-sm-2">
      <input type="text" class="form-control" formControlName="name" placeholder="name">
    </div>

    <label class="sr-only" for="Phone">Phone</label>
    <div class="input-group mb-2 mr-sm-2">
      <input type="text" class="form-control" formControlName="phone" placeholder="Phone">
    </div>

    <label class="sr-only" for="Address">Address</label>
    <div class="input-group mb-2 mr-sm-2">
      <input type="text" class="form-control" formControlName="address" placeholder="Address">
    </div>

    <label class="sr-only" for="membership">Membership</label>
    <div class="input-group mb-2 mr-sm-2">
      <select formControlName="membership" class="form-control">
        <option>Basic</option>
        <option>Pro</option>
        <option>Platinum</option>
      </select>
    </div>

    <button type="submit" class="btn btn-primary mb-2">Update Customer</button>

  </form>
</div>

Now, to move to the update and delete components. We’re going to make a small modification in our customer list. This is because the buttons to update and delete customers are inside the html table. So let’s go ahead and update the customer-list.component.html file:

// customers/customer-list/customer-list.component.html
...
  <tbody>
    <tr>
      <!-- > -->
      <tr *ngFor="let customer of (customers$ | async)">
        <th scope="row">{{customer.name}}</th>
        <td>{{customer.phone}}</td>
        <td>{{customer.address}}</td>
        <td>{{customer.membership}}</td>
        <th>
          <a (click)=editCustomer(customer)>edit</a>
          <br>
          <a (click)=deleteCustomer(customer)>delete</a>
        </th>
      </tr>
  </tbody>
…

Now that we’re calling the respective actions when the user clicks on the links, let’s add these functions in the customer-list.component.ts file:

// customers/customer-list/customer-list.component.html
import { Component, OnInit } from "@angular/core";

import { Store, State, select } from "@ngrx/store";
import { Observable } from "rxjs";

import * as customerActions from "../state/customer.actions";
import * as fromCustomer from "../state/customer.reducer";
import { Customer } from "../customer.model";

@Component({
  selector: "app-customer-list",
  templateUrl: "./customer-list.component.html",
  styleUrls: ["./customer-list.component.css"]
})
export class CustomerListComponent implements OnInit {
  customers$: Observable<Customer[]>;
  error$: Observable<String>;

  constructor(private store: Store<fromCustomer.AppState>) {}

  ngOnInit() {
    this.store.dispatch(new customerActions.LoadCustomers());
    this.customers$ = this.store.pipe(select(fromCustomer.getCustomers));
    this.error$ = this.store.pipe(select(fromCustomer.getError));
  }

  deleteCustomer(customer: Customer) {
    if (confirm("Are You Sure You want to Delete the User?")) {
      this.store.dispatch(new customerActions.DeleteCustomer(customer.id));
    }
  }

  editCustomer(customer: Customer) {
    this.store.dispatch(new customerActions.LoadCustomer(customer.id));
  }
}

As we can see, we’re adding two functions, one to delete the customer and another one to edit the customer. Here we’re going to dispatch the actions, passing in the id of the clicked item. We’re also subscribing any error that we may get.

With that in place, the delete functionality should be working now. We can then move on to finish our last component to edit the customers. So let’s open the edit-customer.component.html file and make sure we have the HTML we need:

// customers/customer-edit/customer-edit.component.html
<div class="mt-4">
  <h3>Edit Customer</h3>
  <form [formGroup]="customerForm" (submit)="updateCustomer()" class="form-inline mb-4">

    <label class="sr-only" for="Name">Name</label>
    <div class="input-group mb-2 mr-sm-2">
      <input type="text" class="form-control" formControlName="name" placeholder="name">
    </div>

    ...

    <button type="submit" class="btn btn-primary mb-2">Update Customer</button>

  </form>
</div>

In the edit-customer.component.ts, we’re going to have to do two things. First, we need to subscribe to the selected customer to fill in the form. Then we’re going to have to dispatch an action. This updates the customer with the values of the edit customer form:

// customers/customer-edit/customer-edit.component.ts
import { Component, OnInit } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import * as customerActions from "../state/customer.actions";
import * as fromCustomer from "../state/customer.reducer";
import { Customer } from "../customer.model";
import { Store, State, select } from "@ngrx/store";
import { Observable } from "rxjs";

@Component({
  selector: "app-customer-edit",
  templateUrl: "./customer-edit.component.html",
  styleUrls: ["./customer-edit.component.css"]
})
export class CustomerEditComponent implements OnInit {
  customerForm: FormGroup;

  constructor(
    private fb: FormBuilder,
    private store: Store<fromCustomer.AppState>
  ) {}

  ngOnInit() {
    this.customerForm = this.fb.group({
      name: ["", Validators.required],
      phone: ["", Validators.required],
      address: ["", Validators.required],
      membership: ["", Validators.required],
      id: null
    });

    const customer$: Observable<Customer> = this.store.select(
      fromCustomer.getCurrentCustomer
    );

    customer$.subscribe(currentCustomer => {
      if (currentCustomer) {
        this.customerForm.patchValue({
          name: currentCustomer.name,
          phone: currentCustomer.phone,
          address: currentCustomer.address,
          membership: currentCustomer.membership,
          id: currentCustomer.id
        });
      }
    });
  }

  updateCustomer() {
    const updatedCustomer: Customer = {
      name: this.customerForm.get("name").value,
      phone: this.customerForm.get("phone").value,
      address: this.customerForm.get("address").value,
      membership: this.customerForm.get("membership").value,
      id: this.customerForm.get("id").value
    };

    this.store.dispatch(new customerActions.UpdateCustomer(updatedCustomer));
  }
}

Here, we first import the dependencies, we initialize the customer form and inject the formBuilder and the store in the constructor. When the component is initialized we first assign some default values to the form. We then declare the customer observable. We subscribe the currently selected customer in the store, passing its values to the form in the template. That way, any time a new customer is selected the form will display the values.

Finally, we define the updateCustomer() function, in which we declare the updatedCustomer object. We do this with the values from the form in the template. Then we need to dispatch the UpdateCustomer action passing in the updatedCustomer.

If we refresh the page now, we’ll see that we can edit the customers and the changes will be saved both in the server and in the store.

And with that the CRUD is complete. We can now read, create, update and remove customers in our application.

In the next video, we’ll look at another library that we haven’t used in this application for the sake of simplicity. But you might need to use in your future projects, that is the NgRx/router-store library.