Skip to content

Instantly share code, notes, and snippets.

@jehugaleahsa
Created June 23, 2017 20:46
Show Gist options
  • Save jehugaleahsa/c40fb64d8613cfad8f1e1faa4c2a7e33 to your computer and use it in GitHub Desktop.
Save jehugaleahsa/c40fb64d8613cfad8f1e1faa4c2a7e33 to your computer and use it in GitHub Desktop.
Angular 2- Nested Model-Driven Forms

Angular 2 - Supporting Nested Model-Driven Forms

Introduction - It worked in AngularJS

If you build real business applications, then you know most of your development effort goes into forms. AngularJS struck me right away as an amazing improvement over the previous generation of HTML libraries, simply because model binding was really well done. Integrating binding with validation made it a first-class web framework.

Business applications of any moderate complexity often have reusable user controls and AngularJS directives were great for that purpose. AngularJS had this amazing feature, whereby placing an ng-form inside of another ng-form the state of the child form was automatically reflected in the parent! You could build up directives with their own validation completely independent of what page they belonged to. Simply by dropping them on your page, all of your validation was auto-wired up. Brilliant!

It doesn't work in Angular2

It is a little surprising, then, that Angular2 did not follow in this tradition. If you search the Internet for "Angular2 nested forms" you'll find a dizzying array of StackOverflow questions and blogs that basically tell you "you can't do it that way anymore". Whaaat? It's a little counter-intuitive at first because you'll find the ngModelGroup class whose purpose it seems is to group together form elements and validation. But the docs explicit say they must appear directly under an ngForm. Boo...

Instead, the Angular2 team decided, for these more advanced scenarios, we developers would be better off using the Reactive style form builder classes. While this might be solid advice for some scenarios, I would guess most developers want to keep working in something similar to AngularJS and prefer the more declarative style of form building. Working mostly in HTML gives me a closer picture of what my page is going to look like. It is much easier to switch between "designer mode" and "coder mode".

A workaround presents itself!

Credit goes to Johannes Rudolph for his brilliant StackOverflow post suggesting simply giving each child component it's own ngForm and exposing that member (via ViewChild) as a public property. Note you should only have one <form> element per page, so just put the ngForm on a <div> instead.

Let's start with this simple example:

@Component({
    selector: "account-name-panel",
    template: `
<div #nameForm="ngForm" ngForm="nameForm">
    <label for="textName">Account Name</label>
    <input #textName="ngModel"
            id="textName"
            name="textName"
            type="text"
            maxlength="100"
            [(ngModel)]="accountName"
            required />
</div>`
})
export class AccountNamePanelComponent {
    @ViewChild("nameForm") public form: NgForm = null;
    public accountName: string = null;
}

Basically, ViewChild, will look at the template and assign the element with the given name to the backing member form. Since we set #nameForm=ngForm, Angular2 will take care to make sure form is an instance of NgForm. This is a great start!

Now when we add <account-name-panel> to another form, we can use ViewChild again to grab an instance of the AccountNamePanelComponent class. From there, we can access the form member to ask it if it is valid or not.

NgForm provides functions for modifying the state of the form: onSubmit($event) and onReset(). Additionally, there are flags to see the current state of the form: submitted, dirty, pristine, valid and invalid. Look at the docs for NgForm to see additional options.

Some drawbacks

Unfortunately, this approach has some drawbacks:

For one, it means exposing a bit about the internal state of your components. You might realize a really complex component needs to have its own child forms that would then also need registered somehow. You'd have to modify every parent form using that child.

If you place a child within an *ngIf, the component will go in and out of existence with your logic; there's no convenient component life-time hook to detect when a child component is created/destroyed either.

Another drawback is that you have to manually check every child form at submission time. Personally, I was adding NgForm objects to an array and then using lodash to check to see if there were any invalid forms:

const forms = [this.mainForm, this.childForm1, this.childForm2];
const isInvalid = _(forms).some((form: NgForm) => form.invalid);

I would often place a forms.filter((f) => f != null) in there, too, just to be safe. After my second form, I created a simple helper class to do this logic for me, call it FormHelper.

export class FormHelper {
    private readonly forms: NgForm[] = [];

    public add(form: NgForm): void {
        if (form == null) {
            return;
        }
        this.forms.push(form);
    }

    public get isValid(): boolean {
        return !_(this.forms).some((f: NgForm) => f.invalid);
    }

    // ... etc.
}

How to auto-wire

I was working on my fourth or fifth form when I finally got tired of building up FormHelpers. My forms were sporting 5+ child forms and I had to wrap each add call with a null check to make sure the child component wasn't null. Worse, I had to build upa new FormHelper everytime I used it because my child components came in and out of existence as data was loading asynchronously.

Fortunately, an idea had already been brewing in my mind for a couple weeks. What set me over the edge was an article that a friend of mine had recently forwarded to me about Heirarchical Dependency Injection in Angular2. Basically, if you explicitly list out a type in the providers section of a component, it gets its own copy of the dependency. Any child components will inherit the parent's copy.

I also recently searched around for ways to detect when a child component became accessible to the parent. Like I said, there is no convenient life-time hook to tap into. I found this answer that, in short, suggests using a property "setter" to detect the change:

@ViewChild(ChildComponent)
private set childSetter(child: ChildComponent) {
    // Do something...
}

An alternative would be to create a separate EventEmitter for the parent to listen to, but then the parent would need to wire in explicitly. I'm trying to avoid the parent needing to do anything.

Putting it altogether

So how do I combine all these seemingly random tricks?

First, I went to my child components and changed them to look like this:

@Component({
    selector: "account-name-panel",
    template: `
<div #nameForm="ngForm" ngForm="nameForm">
    <label for="textName">Account Name</label>
    <input #textName="ngModel"
            id="textName"
            name="textName"
            type="text"
            maxlength="100"
            [(ngModel)]="accountName"
            required />
</div>`
})
export class AccountNamePanelComponent {
    public constructor(private formHelper: FormHelper) {
    }

    @ViewChild("nameForm")
    private set formSetter(form: NgForm) {
        this.formHelper.add(form);
    }

    public accountName: string = null;
}

Notice that I am using a setter property formSetter to add the NgForm in the component to the FormHelper. I have to pass that FormHelper to the constructor of the child component so it has access to it. In order to pass FormHelper, I had to make it Injectable():

@Injectable()
export class FormHelper {
    // ...the rest stays the same
}

Then, in the top-most parent component, I add the providers section so that each page gets its own FormHelper; you wouldn't want to share the same instance across you entire application! You'll need to pass this FormHelper to the constructor to have access to it again:

@Component({
    selector: "parent-form",
    template: `<div>...</div>`,
    providers: [FormHelper]
})
export class ParentComponent {
    public constructor(private formHelper: FormHelper) {
    }
}

Basically, FormHelper becomes a shared service that the parent and children use to report their statuses. The @ViewChild setters in the child components take care of wiring up their forms.

One final step is to make sure you don't re-register the same child form multiple times and that you remove forms when null gets passed to the setter functions. To handle that, I switched to using a Map<string, NgForm> rather than just a simple array and renamed add to register. Here's what my final FormHelper looked like in my application:

@Injectable()
export class FormHelper {
    private formLookup = new Map<string, NgForm>();
    private _isSubmitted: boolean = false;

    public register(name: string, form: NgForm): void {
        if (form == null) {
            this.formLookup.delete(name);
        } else {
            this.formLookup.set(name, form);
        }
    }

    public get isSubmitted(): boolean {
        return this._isSubmitted;
    }

    public get isValid(): boolean {
        const forms: NgForm[] = Array.from(this.formLookup.values());
        const isInvalid = _(forms).some((f: NgForm) => f.invalid);
        return !isInvalid;
    }

    public submit(): void {
        this.formLookup.forEach((form) => {
            form.onSubmit(null);
        });
        this._isSubmitted = true;
    }

    public reset(): void {
        this.formLookup.forEach((form) => {
            form.onReset();
        });
        this._isSubmitted = false;
    }
}

With these simple changes, you can add a child component to any page and it will automatically wire itself into the FormHelper, giving you access to the overall state of your page. This works just as well for grandchild components, too. Woo!

Summary

In summary, it would be nice if model-driven forms just worked like they used to. However, the approach I show here is pretty straight-forward and easy to extend. It doesn't involve knowing much about Angular2, either. For me, a declarative approach just seems like the better alternative and I am glad I was able to stick to it without too much code. Hopefully this will find its way out to other developers.

@squadwuschel
Copy link

You use the " this.formHelper.add(form);" but in your new Helper there is no add function anymore

@Abe-Froman
Copy link

Abe-Froman commented Jul 31, 2017

Looks like this example takes us 90% of the way to a solution.

I'm missing how does formSetter() in the child component get called? Also, where/how do you register the main form from the parent component and what does the HTML look like in the parent where you insert the child component?

I'd also be curious if this solution could be adapted to a child component which has, for example, two fields where one is not required. Or for a child component which is used multiple times in the same parent component and not all are required.

@Poitrin
Copy link

Poitrin commented Aug 3, 2017

This workaround could also be helpful.

@martinobordin
Copy link

@Neutrino-Sunset
Copy link

Neutrino-Sunset commented Dec 8, 2017

Just checked out the approach linked by @martinobordin which only requires a single line of code to specify a viewProvider on the subcomponent, and it works perfectly with validation and one and two way model binding. It should really be added to the official documentation.

@stamminator
Copy link

This gist is missing something very important... the final step of actually consuming the FormHelper you created! Do you have a simple demo app that implements your solution with a parent-child-grandchild hierarchy?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment