Reputation: 1058
I am working on upgrading a big AngularJS application to Angular 5+. This means using new Angular 5 components within a hybrid AngularJS application. In many cases there are forms nested inside other forms. The old AngularJS code has the parent form like this:
export default function PersonDirective() {
return {
restrict: "E",
template: `
<div ng-form="personForm">
<input type="text" name="surname" ng-model="person.surname" ng-required="true">
<address-form address="person.homeAddress"></address-form>
</div>`,
replace: true,
scope: {
person: "="
}
};
}
and the child form similar:
export default function AddressDirective() {
return {
restrict: "E",
template: `
<div ng-form="addressForm">
<input type="text" name="firstLine" ng-model="address.firstLine" ng-required="true">
<input type="text" name="city" ng-model="address.city" ng-required="true">
</div>`,
replace: true,
scope: {
address: "="
}
};
}
This results in a FormController for the PersonDirective which has the address form as a nested FormController field called addressForm
. Validation errors in the subform affect the validity of the parent form.
I have converted the address form to an Angular 5 component, replacing the AngularJS ng-form
and ng-required
directives with standard HTML:
@Component({
selector: 'address-form',
template: `
<div>
<form #addressForm="ngForm">
<input type="text" name="firstLine" [(ngModel)]="address.firstLine" required>
<input type="text" name="city" [(ngModel)]="address.city" required>
</div>`
})
export class AddressFormComponent {
@Input() address: any;
}
The new component is downgraded in index.ts
for use in AngularJS:
angular.module('common')
.directive("ng2AddressForm", downgradeComponent({component: AddressFormComponent}));
and the PersonDirective template modified to use the new component:
<div ng-form="personForm">
<input type="text" name="surname" ng-model="person.surname" ng-required="true">
<ng2-address-form address="person.homeAddress"></ng2-address-form>
</div>
The new component displays and validates as expected. The problem is that it no longer appears as a field in the parent form, and its validity and state are no longer propagated to the parent. It's impossible to convert all the forms at once. Can anyone suggest a solution?
Upvotes: 3
Views: 742
Reputation: 929
Recently ran into this same issue, and came up with this solution for template-drive forms
In the Angular template, wrap the input fields in an ngForm
<form #angularForm="ngForm" novalidate>
<div class="col-md-12">
<div class="col-md-6">
<label for="job">Job</label>
<input id="job" name="job" type="text" class="form-control"
[(ngModel)]="job"
#job="ngModel"
required>
</div>
<div>angularForm.form.valid: {{angularForm.form.valid}}</div>
</form>
In the Angular component, use ViewChild to get a handle on the form, subscribe to the changes of the form, then use an EventEmitter to emit the new status, as in @Chris's answer above
import { AfterViewInit, Component, EventEmitter, Output, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'angular-form',
templateUrl: './angularForm.component.html'
})
export class AngularFormComponent implements AfterViewInit {
@Output("status") statusEvent = new EventEmitter<string>();
@ViewChild('angularForm', { read: NgForm, static: false }) myForm: NgForm;
ngAfterViewInit() {
if (this.myForm) {
this.myForm.statusChanges?.subscribe(e => this.statusEvent.emit(e));
}
}
}
In the parent AngularJS form, add a hidden input element with a ng-model value that is set/unset by handling the event emitted from the Angular component
<form id="angularJSForm" name="angularJSForm" ng-submit="submit()" novalidate>
<div class="form-group">
<label for="myName">My Name</label>
<input id="myName" name="myName" type="text" class="form-control" ng-model="myName" required>
</div>
<angular-form (status)="angularFormValid = ($event === 'VALID' ? 'true' : null)"></angular-form>
<input id="angularFormValid" name="angularFormValid" type="hidden" class="form-control" ng-model="angularFormValid" required>
<div>angularJSForm.$valid: {{angularJSForm.$valid}}</div>
<div>
<button id="submitAngularJSForm" name="submitAngularJSForm" type="submit" class="btn btn-primary pull-right" title="Submit AngularJS Form"
ng-disabled="angularJSForm.$invalid">Submit</button>
</div>
</form>
When the status of the child Angular form changes, the value of angularFormValid is set, and, if valid, the submit button is enabled.
Upvotes: 0
Reputation: 1058
The best solution I have found is to create a child component that implements ControlValueAccessor and Validator. ControlValueAccessor allows the component to be bound to an arbitrary object (the address in my example above) using [(ngModel)] in new Angular 2+ templates and ng-model in old AngularJS templates.
In new templates, the validity status of the child component will automatically affect the validity of the parent form. In AngularJS I've had to use an event handler to report child status to the parent. So the relationship looks like this:
In the AngularJS parent form template:
<ng2-address-form
ng-model="myCtrl.person.homeAddress"
(status)="myCtrl.onAddressStatus($event)">
</ng2-address-form>
In the Angular 2+ component:
export class AddressFormComponent implements ControlValueAccessor, Validator {
@Output("status") statusEvent = new EventEmitter<ValidationErrors | null>();
addressForm: FormGroup;
...
this.addressForm.statusChanges.subscribe(s => this.statusEvent.emit(this.validate(null)));
...
validate(c: AbstractControl): ValidationErrors | null {
// validate the form
}
I've found this easier using a reactive form as it gives the component logic direct access to the form controls.
Upvotes: 1