Ethan Kent
Ethan Kent

Reputation: 551

Angular 2: Idiomatic way to compose forms from multiple components

To try to learn Angular 2, I am making a diet tracker to go along with the latest fad diet my wife and I are trying. The book has several questions that can be answered with buttons (How hungry were you—Starving, Very Hungry, etc.) plus an optional text input. There are also checkbox-based questions, plus a date.

I am trying to create a form composed of these multiple components—one for the button questions, one for the checkbox questions. See this plunker.

What I'm curious about is the idiomatic way to compose components—each with its own form elements—into the Daily form. Each subcomponent has form elements, also using *ngFor to loop over data in an external file (question-data.ts). At the moment, each component has an event emitter that the Daily form subscribes to.

Here is the Daily form template (src/daily.component.ts):

  <form>
  <h1>Daily Tracker</h1>
  <br>
  <legend>Date</legend>
  <date-picker (onDataEntered)="dateDataEntered($event)"></date-picker>
  <br>
  <br>
  <button-questions
    *ngFor="#b of buttonQuestions"
    [btn]="b"
    (onDataEntered)="buttonDataEntered($event)">
  </button-questions>
  <checkbox-questions
    *ngFor="#c of checkboxQuestions"
    [cbox]="c"
    (onDataEntered)="checkboxDataEntered($event)">
  </checkbox-questions>
  <br>
  <input type="submit" value="Submit" class="btn btn-primary">
</form>

Then, for example, the buttonQuestions are rendered like so (src/button-questions.component.ts):

<form>
  <h1>Daily Tracker</h1>
  <br>
  <legend>Date</legend>
  <date-picker (onDataEntered)="dateDataEntered($event)"></date-picker>
  <br>
  <br>
  <button-questions
    *ngFor="#b of buttonQuestions"
    [btn]="b"
    (onDataEntered)="buttonDataEntered($event)">
  </button-questions>
  <checkbox-questions
    *ngFor="#c of checkboxQuestions"
    [cbox]="c"
    (onDataEntered)="checkboxDataEntered($event)">
  </checkbox-questions>
  <br>
  <input type="submit" value="Submit" class="btn btn-primary">
</form>

The dataEntered() methods do this (this method is also called on the click of any button):

private dataEntered(): void {
  this.btn.inputText = this.inputTextControl.value;
  this.onDataEntered.emit(this.btn);
}

The Daily component then subscribes to the event emitters on its sub-components and will handle final validation / database interface (I’m still trying to learn that part).

But I feel like the use of raw event listeners on the sub-components is the wrong approach: for example, I don’t know how to wire together validators on my sub-components to validation on the form as a whole. I also don’t think I’m using built-in Angular 2 capabilities fully. And I feel as though this does not follow the principle of loose coupling, as I understand it.

My inclination is that I should somehow gather up the sub-components into a master ControlGroup in the Daily component, but I am not sure how to do that.

Thanks!

Upvotes: 3

Views: 788

Answers (1)

Eleomosynator
Eleomosynator

Reputation: 90

I don't know whether this approach is preferred, but this is one way. Child components can use ControlContainer from @angular/forms to access their parent control's FormGroup.

Parent component TypeScript file:

// imports go here

@Component({
  selector: "app-parent",
  templateUrl: "./parent.component.html",
  styleUrls: ["./parent.component.css"]
})
export class ParentComponent implements OnInit {
    // Just create a typical FormGroup.
    fg: FormGroup;

    // Instantiate the FormGroup using your preferred method.
    constructor(fb: FormBuilder) {
        this.fg = this.fb.group({ FirstName: "John", LastName: "Doe" });
    }
}

Parent component template file:

<!-- Bind a form in the parent component to your FormGroup. -->
<form [formGroup]="fg">
    <!-- Reference the child component.  No need for @Inputs or fancy bindings. -->
    <app-child></app-child>
</form>

Child component TypeScript file:

import { Component } from "@angular/core";
import { ControlContainer } from "@angular/forms";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent {
  // Ask the framework for a ControlContainer whose control property has the goods.
  constructor(public controlContainer: ControlContainer) { }

  ngOnInit(): void {
  }
}

Child component template file:

<!-- Finally, bind some top-level element to the imported FormGroup. -->
<fieldset [formGroup]="controlContainer.control">
    <legend>Reusable Controls Go Here</legend>

    <div>
        First Name
        <!-- Now, use the FormGroup per normal. -->
        <input formControlName="FirstName" type="text" />
    </div>
    <div>
        Last Name
        <input formControlName="LastName" type="text" />
    </div>
    <div>
        Accessing the FormGroup
        <!-- You can even get fancy. -->
        {{ controlContainer.control.get('FirstName')?.errors | json }}
    </div>
</fieldset>

Upvotes: 1

Related Questions