3

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?

DaggeJ
  • 2,094
  • 1
  • 10
  • 22
Chris
  • 1,038
  • 10
  • 18

1 Answers1

1

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.

Chris
  • 1,038
  • 10
  • 18