Compare commits

...

7 Commits

6 changed files with 320 additions and 29 deletions

View File

@ -16,7 +16,7 @@ The [Base Line](apps/baseline) application implements the contact manangement fo
## Components Creating Own Form Controls
When searching the Internet for examples of nested forms in Angular, one popular approach is to have each sub-component be responsible for creating its own form controls. The parent form is passed in as an `@Input`, and the component adds its components to the form using the `addControl(name, control)` method.
Searching the Internet for examples of nested forms in Angular shows one popular approach is to have each sub-component be responsible for creating its own form controls. The parent form is passed in as an `@Input`, and the component adds its components to the form using the `addControl(name, control)` method. See, for example, [Nested Reactive Forms in Angular2](https://www.brophy.org/post/nested-reactive-forms-in-angular2/).
The [Parent Form](apps/parent-form) application explores this approach.

View File

@ -1 +1,89 @@
# Base Line - The Single component
The easiest way to create a simple Reactive Form is to use a single Angular component to build and display the form. In this example application, an asynchronous call to an API is simulated. The `Contact` object thus obtained is used to create and initialize a `FormGroup` using the Angular `FormBuilder`.
```typescript
public ngOnInit() {
this.subscription = this.service
.loadContact()
.subscribe((data: Contact) => {
this.form = this.createForm(data);
});
}
public createForm(model: Contact): FormGroup {
const name = model.name;
const addresses: FormArray = this.fb.array([]);
const group = this.fb.group({
name: this.fb.group({
firstName: [name ? name.firstName : ''],
lastName: [name ? name.lastName : ''],
middleName: [name ? name.middleName : ''],
prefix: [name ? name.prefix : ''],
suffix: [name ? name.suffix : ''],
}),
addresses: addresses,
});
if (model.addresses) {
model.addresses.forEach(addr => {
addresses.push(
this.fb.group({
line1: [addr ? addr.line1 : ''],
line2: [addr ? addr.line2 : ''],
city: [addr ? addr.city : ''],
state: [addr ? addr.state : ''],
postalCode: [addr ? addr.postalCode : ''],
}),
);
});
}
return group;
}
get addresses(): FormArray {
return this.form.get('addresses') as FormArray;
}
```
The component's template remders input controls for the Contact's name, and iterates over the array of addresses to render controls to edit each one.
```html
<form *ngIf="form" [formGroup]="form">
<ng-container formGroupName="name">
<div>
<label for="firstName">First Name: </label>
<input name="firstName" formControlName="firstName">
</div>
<!-- etc for each attribute in the Contact name -->
</ng-container>
<ng-container formArrayName="addresses">
<ng-container *ngFor="let addr of addresses.controls; let i=index">
<!-- Note: the form group is bound to the index variable -->
<ng-container [formGroupName]="i">
<div>
<label for="line1">Line 1: </label>
<input name="line1" formControlName="line1">
</div>
<!-- etc for each attribute in the Address -->
</ng-container>
</ng-container>
</ng-container>
</form>
```
## Pros
- All of the logic is in one place
- The potential complexity of multiple components is avoided
## Cons
- The component and its template will become ever larger and more complex as the form grows
- Little-to-no opportunity for code reuse

View File

@ -9,7 +9,6 @@ import { Subscription } from 'rxjs';
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit, OnDestroy {
contact: Contact;
form: FormGroup;
private subscription: Subscription;
@ -20,7 +19,6 @@ export class AppComponent implements OnInit, OnDestroy {
this.subscription = this.service
.loadContact()
.subscribe((data: Contact) => {
this.contact = data;
this.form = this.createForm(data);
});
}

View File

@ -1 +1,118 @@
# Parent Component Creates Form and Passes Form Controls Into Child Components
An alternative approach to refactoring a component into child sub-components is to make the parent component be responsible for creating the entire Reactive Form, and to pass the appropriate form controls into the children. By also refactoring the logic for creating the form into a builder method in a separate service, the parent control's logic and template become almost as simple as in the [Own Form Controls](../parent-form/README.md) application:
```typescript
export class AppComponent implements OnInit, OnDestroy {
contact: Contact;
form: FormGroup;
private subscription: Subscription;
constructor(
private service: ContactService,
private formService: ContactFormService,
) {}
public ngOnInit() {
this.subscription = this.service
.loadContact()
.subscribe((data: Contact) => {
this.contact = data;
this.form = this.formService.createForm(data);
});
}
}
```
```html
<form [formGroup]="form">
<nested-forms-name
[nameGroup]="form.get('name')"
></nested-forms-name>
<nested-forms-address-list
[addressArray]="form.get('addresses')"
></nested-forms-address-list>
</form>
```
While the templates for `nested-forms-name` and `nested-forms-address-list` using this approach are almost identical to the templates in the _Own Form Controls_ example, the typescript code is even simpler:
```typescript
@Component({
selector: 'nested-forms-name',
templateUrl: './name.component.html',
styleUrls: ['./name.component.css']
})
export class NameComponent {
@Input() nameGroup: FormGroup;
}
```
## Form Builder Service
When looking at Reactive Forms through the lens of the _Model, View, Controller_ pattern, the `FormGroup` is the _Model_, and the HTML templating is the _View_. By using factory service methods to create the overall form structure, one can gain the benefits of encapsulation and composition while maintaining a strong separation between the _Model_ and the _View_.
```typescript
import { Injectable } from '@angular/core';
import { FormArray, FormBuilder, FormGroup } from '@angular/forms';
import { Contact, Name, Address } from '@nested-forms/contact';
@Injectable({
providedIn: 'root',
})
export class ContactFormService {
constructor(private fb: FormBuilder) {}
public createForm(model: Contact): FormGroup {
return this.fb.group({
name: this.createContactNameForm(model.name),
addresses: this.createContactAddressListForm(model.addresses),
});
}
public createContactNameForm(name: Name): FormGroup {
return this.fb.group({
firstName: [name ? name.firstName : ''],
lastName: [name ? name.lastName : ''],
middleName: [name ? name.middleName : ''],
prefix: [name ? name.prefix : ''],
suffix: [name ? name.suffix : ''],
})
}
public createContactAddressListForm(addresses: Address[]): FormArray {
const list: FormArray = this.fb.array([]);
if (addresses) {
addresses.forEach(addr => {
list.push(this.createContactAddressForm(addr));
});
}
return list;
}
public createContactAddressForm(addr: Address): FormGroup {
return this.fb.group({
line1: [addr ? addr.line1 : ''],
line2: [addr ? addr.line2 : ''],
city: [addr ? addr.city : ''],
state: [addr ? addr.state : ''],
postalCode: [addr ? addr.postalCode : ''],
});
}
}
```
## Pros
- Clean separation between _Model_ and _View_
- The parent component is easy to understand and maintain
- The child components are likewise easy to understand and maintain
- Encapsulation and composition are provided by the factory methods
## Cons
- Having a service with factory methods adds complexity to the application

View File

@ -1 +1,83 @@
# Components Creating Own Form Controls
When a component becomes sufficiently complex, or the developer wishes to be able to reuse parts of it elsewhere, the component can be refactored into sub-components. One approach to binding such sub-components to their containing parent component is to pass the parent component's `FormGroup` in as an `@Input` parameter. Additionally, the data that each child sub-component needs is passed into an `@Input`. The children create their own `FormControls` as needed, and add them to the parent `FormGroup` provided to them. This approach greatly simplifies the code and template of the parent component:
```typescript
export class AppComponent implements OnInit, OnDestroy {
contact: Contact;
form: FormGroup;
private subscription: Subscription
constructor(private fb: FormBuilder, private service: ContactService) {
this.form = this.fb.group({});
}
public ngOnInit() {
this.subscription = this.service.loadContact().subscribe((data: Contact) => {
this.contact = data;
});
}
}
```
```html
<form [formGroup]="form">
<nested-forms-name
[name]="contact.name"
[parent]="form"
></nested-forms-name>
<nested-forms-address-list
[addresses]="contact.addresses"
[parent]="form"
></nested-forms-address-list>
</form>
```
The `nested-forms-name` component is responsible for creating the form controls binding to the Contact's name, and the `nested-forms-address-list` component is responsible for iterating over the Contact's addresses and binding to them using the `nested-forms-address` (singular) component. For example, the `nested-forms-name` would be implented as so:
```typescript
@Component({
selector: 'nested-forms-name',
templateUrl: './name.component.html',
styleUrls: ['./name.component.css']
})
export class NameComponent implements OnInit {
@Input() name: Name;
@Input() parent: FormGroup;
group: FormGroup;
constructor(private fb: FormBuilder) {
}
ngOnInit() {
this.group = this.fb.group({
firstName: new FormControl(this.name ? this.name.firstName : ''),
lastName: new FormControl(this.name ? this.name.lastName : ''),
middleName: new FormControl(this.name ? this.name.middleName : ''),
prefix: new FormControl(this.name ? this.name.prefix : ''),
suffix: new FormControl(this.name ? this.name.suffix : ''),
});
if (this.parent) {
this.parent.addControl('name', this.group);
}
}
}
```
Calling `this.parent.addControl(....)` is what ensures that the controls created in the child component are made part of the over-all form.
## Pros
- The parent component is easy to understand and maintain
- Each child component encapsulates its form controls and template
- The child components can easily be re-used in other "parent" components
## Cons
- The creation of the form controls is tightly coupled with the templates
- Since each child component encapsulates its form controls, the overall shape of the form data is not always clear

View File

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { FormArray, FormBuilder, FormGroup } from '@angular/forms';
import { Contact } from '@nested-forms/contact';
import { Contact, Name, Address } from '@nested-forms/contact';
@Injectable({
providedIn: 'root',
@ -9,35 +9,41 @@ export class ContactFormService {
constructor(private fb: FormBuilder) {}
public createForm(model: Contact): FormGroup {
const name = model.name;
const addresses: FormArray = this.fb.array([]);
const group = this.fb.group({
name: this.fb.group({
firstName: [name ? name.firstName : ''],
lastName: [name ? name.lastName : ''],
middleName: [name ? name.middleName : ''],
prefix: [name ? name.prefix : ''],
suffix: [name ? name.suffix : ''],
}),
addresses: addresses,
return this.fb.group({
name: this.createContactNameForm(model.name),
addresses: this.createContactAddressListForm(model.addresses),
});
}
if (model.addresses) {
model.addresses.forEach(addr => {
addresses.push(
this.fb.group({
line1: [addr ? addr.line1 : ''],
line2: [addr ? addr.line2 : ''],
city: [addr ? addr.city : ''],
state: [addr ? addr.state : ''],
postalCode: [addr ? addr.postalCode : ''],
}),
);
public createContactNameForm(name: Name): FormGroup {
return this.fb.group({
firstName: [name ? name.firstName : ''],
lastName: [name ? name.lastName : ''],
middleName: [name ? name.middleName : ''],
prefix: [name ? name.prefix : ''],
suffix: [name ? name.suffix : ''],
})
}
public createContactAddressListForm(addresses: Address[]): FormArray {
const list: FormArray = this.fb.array([]);
if (addresses) {
addresses.forEach(addr => {
list.push(this.createContactAddressForm(addr));
});
}
return group;
return list;
}
public createContactAddressForm(addr: Address): FormGroup {
return this.fb.group({
line1: [addr ? addr.line1 : ''],
line2: [addr ? addr.line2 : ''],
city: [addr ? addr.city : ''],
state: [addr ? addr.state : ''],
postalCode: [addr ? addr.postalCode : ''],
});
}
}