ngrx store, safe two-way bound or one-way bound template forms

ngrx store, safe two-way bound or one-way bound template forms

This article builds on the prior series showing how to create an application using Angular 2 or later, and leverage ngrx store for application state.  The previous article demonstrated how to add ngrx-store-freeze to verify your angular components do not improperly modify state directly without your reducers. This article will continue from a working app containing both a one-way bound template form for search criteria and a two-way bound template form working with a copy of the store data and explore the option of converting the two-way bound edit form into a one-way bound form and the trade-offs for both options.

Angular Two-way bound template form – how it works with Ngrx Store

A common example Angular app uses two-way bindings, “banana-in-a-box” [(ngModel)], to show an edit form and have those values automatically update the bound property in the component. This technique can work with ngrx store, however you need to bind the form to a copy of the input, if that input value is from the ngrx store. This prevents accidental skipping of the reducer and direct modification of the store.

Take a common input of a component for example:

@Input() personHolder: PersonHolder;

Let’s convert that public field into a property for which on set makes a copy of the supplied value. Now the two-way bound form will update the copy and not the original in the store.

// person-detail.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Person, PersonHolder } from '../person.model';
import { PersonService } from '../person.service';

@Component({
    selector: 'person-detail',
    providers: [PersonService],
    templateUrl: '/dist/js/person/detail/person-detail.component.html',
})
export class PersonDetailComponent {
    private _personHolder: PersonHolder;
    get personHolder() { return this._personHolder; }
    @Input() set personHolder(personHolder: PersonHolder) {
        this._personHolder = JSON.parse(JSON.stringify(personHolder)) //_.cloneDeep(personHolder);
    }
    @Output() saveClicked: EventEmitter<PersonHolder> = new EventEmitter<PersonHolder>();

    get person() {
        return this._personHolder.Person;
    }
    public save(): void {
        this.saveClicked.emit(this.personHolder);
    }
}

Then when the save button is pressed in the view, which calls this save function, this copy of the original object modified by the two-way binding will be emitted to the parent component that handles the save.

The person property getter in this example only exists as a shortcut to reduce the code in the view template. This view template is your typical two-way bound form, with a button click triggering the action in the backing component above.

// person-detail.component.html
<div *ngIf="loading">
    <div class="text-center"><span class="glyphicon glyphicon-refresh"> </span></div>
</div>
<div *ngIf="!loading" class="panel">
    <div class="panel-body">
        <div class="form-horizontal">
            <div class="row">
                <div class="col-md-6">
                    <div class="form-group">
                        <label>
                            <span>First Name</span>
                            <input type="text" class="form-control" 
                                [(ngModel)]="person.FirstName">
                        </label>
                    </div>
                    <div class="form-group">
                        <label>
                            <span>Last Name</span>
                            <input type="text" class="form-control" 
                                [(ngModel)]="person.LastName">
                        </label>
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="form-group">
                        <label>
                            <span>Phone Number</span>
                            <input type="tel" class="form-control" 
                                [(ngModel)]="person.ContactPhoneNumber">
                        </label>
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="col-sm-12">
                    <button class="btn btn-primary" (click)="save()">Save</button>
                </div>
            </div>
        </div>
    </div>
</div>

Angular One-way bound template alternative

Let’s see how one-way bound template forms differ when using ngrx. First, we can get back to the original simple input decorator for the value passed in from the parent, which the parent loaded directly from the store. This input value is a direct reference to the original store value. Let’s be careful not to modify it.

One-way forms are similar to two-way bound forms, but we are telling angular to only initialize the form from the model and not to update the bound item when the form updates. From the component code, you can’t be sure if it is one-way bound or two-way bound. That is, until you look at the save function.

// person-detail.component.ts
import { Component, Input, Output, EventEmitter, ViewChild, AfterViewInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Person, PersonHolder } from '../person.model';
import { PersonService } from '../person.service';

@Component({
    selector: 'person-detail',
    providers: [PersonService],
    templateUrl: '/dist/js/person/detail/person-detail.component.html',
})
export class PersonDetailComponent {
    @Input() personHolder: PersonHolder;    
    @Output() saveClicked: EventEmitter<PersonHolder> = new EventEmitter<PersonHolder>();

    get person() {
        return this.personHolder.Person;
    }
    save(person: Person) {
        let v = Object.assign({}, this.personHolder, {
            Person: Object.assign({}, this.personHolder.Person, person)
        });
        this.saveClicked.emit(v);
    }
}

Notice the save code is taking the original input value combined with the updated form values into a new object. Essentially this is a copy of the original object, plus changes from the form. If you debug through this code, you’ll notice the supplied person instance to the function only has some of the person class properties, and look closely and you’ll notice its just the ones that represent a form value in the view. PersonId property of Person does not even exist on the value passed into the save function.

// person-detail.component.html
<div *ngIf="loading">
    <div class="text-center"><span class="glyphicon glyphicon-refresh"> </span></div>
</div>
<div *ngIf="!loading" class="panel">
    <div class="panel-body">
        <form id="personSaveForm" #ngForm="ngForm" class="form-horizontal" 
              (submit)="save(ngForm.form.value)">
            <div class="row">
                <div class="col-md-6">
                    <div class="form-group">
                        <label>
                            <span>First Name</span>
                            <input type="text" class="form-control" 
                                   [ngModel]="person.FirstName" name="FirstName">
                        </label>
                    </div>
                    <div class="form-group">
                        <label>
                            <span>Last Name</span>
                            <input type="text" class="form-control" 
                                   [ngModel]="person.LastName" name="LastName">
                        </label>
                    </div>
                </div>
                <div class="col-md-6">
                    <div class="form-group">
                        <label>
                            <span>Phone Number</span>
                            <input type="tel" class="form-control" 
                                   [ngModel]="person.ContactPhoneNumber" name="ContactPhoneNumber">
                        </label>
                    </div>
                </div>
            </div>
            <div class="row">
                <div class="col-sm-12">
                    <button class="btn btn-primary" type="submit">Save</button>
                </div>
            </div>
        </form>
    </div>
</div>

Notice on line 8, the NgForm values are passed into the save function call. The form as defined on line 7, only knows about properties attached to an ngModel binding on this form. The ngModel bindings on lines 15,22,31 are defined with the one-way plan box wrapped [ngModel] declarations. The button on line 38 now doesn’t have a click, and instead is of type submit. The call to save is handled by line 8 with the ‘submit’ event.

Which is better?

The two-way template form has the advantage in the component in which you know you have a copy, and it doesn’t matter if the view is really one-way or two-way.

It’s really up to you which is better for your project. For now, I’m going to stick with the one-way template forms when using ngrx store.

Other options to explore

The other major option available is angular reactive forms. Angular.io has no opinion on which is better overall. It would be a worthy test to see if reactive forms makes more sense than template forms when using ngrx, but really the choice is up to you.

There’s another option in the works by the ngrx store team, so watch that for any traction.



Further reading

Full latest source code for this article series
https://github.com/michael-lang/sample-ng2-mvc

Ngrx core: Improve Reactive Forms Experience
https://github.com/ngrx/core/issues/12

Ngrx store freeze reveals problems with shared state

About the Author

Michael Lang

Co-Founder and CTO of Watchdog Creative, business development, technology vision, and more since 2013, Developer, and Mentor since 1999. See the about page for more details.