<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development

Manage Form-Driven State with ngrx-forms (Part 1)

Kyle Nazario

Learn how to leverage NgRx, ngrx-forms and one-way data flow with your Angular forms.

posted in Open Source, javascript, tutorial, Angular, NgRx, Reactive Programming on May 21, 2021 by Kyle Nazario


Manage Form-Driven State with ngrx-forms (Part 1)

Kyle Nazario by Kyle Nazario

If your Angular application already uses NgRx, you know the value of writing good reducers and using one-way data flow. However, that knowledge may not be enough to keep the biggest form in your app from being a rat’s nest of FormGroups and related logic. Forget keeping components under 50 lines of code - your form logic alone blows past that.

Implementing the ngrx-forms library can help with this problem. This open-source library helps you move form logic into the global NgRx state. Form data flows from form components to the store and back to components.

Advantages:

  • One-way data flow like with NgRx, often easier to debug
  • Lets you reuse reducers and other store logic
  • Reduces component complexity
  • Useful for forms with a lot of inputs or heavy business logic

Having worked on enterprise applications with large form components thousands of lines long, I can attest that ngrx-forms is a viable way to move complex form logic somewhere else and let the component focus on rendering markup and dispatching actions to the store.

Disadvantages:

  • Storing local form state as global state
  • Like NgRx, adds a layer of indirection
  • Less popular package
  • Docs are terse and only somewhat helpful

Storing local form state in the global state can have negative effects. It breaks encapsulation. It can allow form data to leak to other parts of the application and cause confusing, undesirable data flow. 

When choosing third-party tools to assist with development, popularity does not automatically equal quality, but you’re less likely to encounter a novel bug. It means your question likely has an existing answer on Stack Overflow.

To give a sense of scale for this library’s popularity, @ngrx/store gets 378,000 downloads per week on NPM. ngrx-forms gets 4,000 a week.

How to Implement ngrx-forms

For this article, I’ll be using the Place My Order app built in Bitovi’s Learn Angular tutorial. I’ve created a fork with ngrx and ngrx-forms installed.

Place My Order is a simple sample app that lets users “order” from restaurants in one of a few cities. The version of the app built in the aforelinked tutorial uses Reactive Forms. While Reactive Forms are powerful, the order form component is too large. ngrx-forms will let us move that form logic and validation into the NgRx store.

Here is reducers.ts, a new file I made which declares our NgRx store and reducers:

// reducers.ts

export type GlobalState = {
  orders: Array<Order>
  mostRecentOrder?: Order;
}

export const initialState: GlobalState = {
  orders: [],
  mostRecentOrder: null,
};

export function reducer(
  state = initialState,
  action: any // normally this would be a union type of your action objects
): GlobalState {
  switch (action.type) {
    case ActionType.createOrderSuccess:
      const orders = [...state.orders, action.order];
      return {...state, orders, mostRecentOrder: action.order};
    case ActionType.getOrdersSuccess:
      return {...state, orders: action.orders};
    default:
      return state;
  }
}

First, to manage our form's state, we add it as a property on our existing NgRx state.

import { box, Boxed, createFormGroupState, FormGroupState } from 'ngrx-forms';

export type GlobalState = {
  orders: Array<Order>
  mostRecentOrder?: Order;
  orderForm: FormGroupState<OrderFormState>;
}

// shorthand to help TypeScript understand what we’re doing
type Override<T1, T2> = Omit<T1, keyof T2> & T2;
type OrderFormState = Override<Order, {items: Boxed<Array<Item>>}>;

const ORDER_FORM_ID = 'order_form_id';
const initialOrderFormState = createFormGroupState<OrderFormState>(ORDER_FORM_ID, {
  _id: '',
  name: null,
  address: null,
  phone: null,
  status: '',
  items: box([])
});

export const initialState: GlobalState = {
  orders: [],
  mostRecentOrder: null,
  orderForm: initialOrderFormState
};

First, we add a new property to GlobalState, orderForm.

The order form group will have all the same properties as an Order: _id, name, address, phone, status and items. The only difference is in the form group, items is Boxed<Array<Item>>. Here’s why.

The Place My Order application uses the pmo-menu-items component to select items. The form control attached to pmo-menu-items will receive an array of item objects. 

ngrx-forms works out of the box with form control values as long as those values are JavaScript primitives like strings or numbers. However, if you want to use an object or array for your form control value in ngrx-forms, you must provide an initial value that is “boxed.” That way when we provide an empty array, ngrx-forms knows the array is our form control value and not indicating a FormArray.

Next, we update the reducer to update our form group when the user inputs data. formGroupReducer updates the value of form controls in state if action is an update to any of them. If the form group has changed at all, formGroupReducer returns a new object. Otherwise, it returns the previous form group state.

// reducers.ts

import { formGroupReducer } from 'ngrx-forms';

export function reducer(
  state = initialState,
  action: any // normally this would be a union type of your action objects
): GlobalState {
  const orderForm = formGroupReducer(state.orderForm, action);
  if (orderForm !== state.orderForm) {
    state = {...state, orderForm};
  }
  switch (action.type) {
    case ActionType.createOrderSuccess:
      const orders = [...state.orders, action.order];
      return {...state, orders, mostRecentOrder: action.order};
    case ActionType.getOrdersSuccess:
      return {...state, orders: action.orders};
    case ActionType.clearOrderForm:
      return {...state, orderForm: initialOrderFormState};
    default:
      return state;
  }
}

Now, any time there is a change dispatched from the form or our TypeScript code, it will update the form group in the NgRx global state.

The next step is hooking up the global form state to our component. You select the desired form group from the global state, just like selecting non-form NgRx state properties.

// order.component.ts

...

  orderFormState$: Observable<FormGroupState<Order>>;

  constructor(
    private route: ActivatedRoute,
    private restaurantService: RestaurantService,
    private store: Store<GlobalState>
  ) {
    this.createdOrder = store.pipe(
      select('order'),
      select('mostRecentOrder')
    );
    // ngrx-forms FormGroup below
    this.orderFormState$ = store.pipe(
      select('order'),
      select('orderForm')
    );
  }

Select the ngrx property for your form group and assign it orderFormState$. Now we bind it to our template:

<!-- order.component.html -->

<ng-container *ngIf="(orderFormState$ | async) as formState">
      <h2>Order here</h2>
      <form *ngIf="restaurant" [ngrxFormState]="formState" (ngSubmit)="onSubmit()">
        <tabset>
          <tab heading="Lunch Menu">
            <ul class="list-group" >
              <pmo-menu-items [data]="restaurant.menu.lunch" [ngrxFormControlState]="formState.controls.items"
              ></pmo-menu-items>
            </ul>
          </tab>
          <tab heading="Dinner menu">
            <ul class="list-group" >
              <pmo-menu-items [data]="restaurant.menu.dinner" [ngrxFormControlState]="formState.controls.items"></pmo-menu-items>
            </ul>
          </tab>
        </tabset>
        <div class="form-group">
          <label class="control-label">Name:</label>
          <input name="name" type="text" class="form-control" [ngrxFormControlState]="formState.controls.name">
          <p>Please enter your name.</p>
        </div>
        <div class="form-group">
          <label class="control-label">Address:</label>
          <input name="address" type="text" class="form-control" [ngrxFormControlState]="formState.controls.address">
          <p class="help-text">Please enter your address.</p>
        </div>
        <div class="form-group">
          <label class="control-label">Phone:</label>
          <input name="phone" type="text" class="form-control" [ngrxFormControlState]="formState.controls.phone">
          <p class="help-text">Please enter your phone number.</p>
        </div>
        

      ...


      </form>
    </ng-container>

First, we grab the value of the form group using the async pipe and assign it an alias for easier reference. Next, attach form controls to template elements with the verbose ngrxFormControlState directive. You can log formState to see the object shape, it’s similar-ish to Reactive Forms FormGroups.

And that’s it for basic setup! Typing in the template or selecting items will update the global state.

ngrx-forms-1

In Part 2, we'll cover synchronous and asynchronous validation, as well as custom validators. 

Create better web applications. We’ll help. Let’s work together.