Skip to content

Proposal to separate concerns of child control construction and parent ControlContainer mapping. #10195

@EricABC

Description

@EricABC

I'm submitting a ... (check one with "x")

[ ] bug report
[X] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior
Reactive forms:
There is no built-in mechanism for separating the concerns of constructing a child control vs. mapping that hierarchy to a parent ControlContainer. I have two choices to handle this dynamic form mapping myself:

  • In the parent I can wire myself up to the proper lifecycle events to dynamically bind that child control to the correct parent member. I have to select the ViewChild, then implement OnChanges, tracking an initial wire-up flag.
  • In the child I can inject in the parent ControlContainer and receive the name/index of the parent.

Expected/desired behavior
A directive (or extension to existing __Name directives) which maps the value from the child control could remove this complexity and allow the child to specify the control while the directive (used in the parent component) would handle the mapping.

In other words, in the template I want to say this:

<address #homeAddress formControlName="homeAddress" [childControl]="homeAddress.control"></address>
<address #workAddress formControlName="workAddress" [childControl]="workAddress.control"></address>

Here the value associated with childControl would be set on the parent ControlContainer (either array or group) based on the formControlName.

What is the motivation / use case for changing the behavior?
Take a reusable address component for an example that may build up a FormGroup of line1, line2, city, state, and zip, as well as several validators. I may want to use this component in several locations, perhaps on a page with a parent FormGroup with 'home' and 'work' children, and then on another page that allows me to add any number of saved shipping addresses to a FormArray.

Implementation
This could be accomplished by adding an Input on the applicable directives (FormGroupName, FormControlName, NgControl). If set, then the control passed to it would be mapped to the parent ControlContainer. Here is a working hack I was toying with just for the sake of showing the interaction points and allowing me to use the above template syntax. The hack points are that it replaces those directives' ngOnChanges methods and accesses the private _parent.

@Directive({
    selector: '[abcFormControl]'
})
export class AbcFormControl implements OnChanges /*implements ControlValueAccessor*/ {
    constructor( @Optional() @Self() formGroupName: FormGroupName, @Optional() @Self() formArrayName: FormArrayName, @Optional() @Self() ngControl: NgControl) {
        this._otherDirective = <any>(formGroupName ? formGroupName : formArrayName ? formArrayName : ngControl);
        this._oldOnChanges = this._otherDirective.ngOnChanges;
        this._otherDirective.ngOnChanges = (changes: SimpleChanges) => this._newOnChanges(changes);
    }

    @Input() abcFormControl: AbstractControl;
    private _otherDirective: any;
    private _oldOnChanges: (changes: SimpleChanges) => any;
    private _delayedChanges: SimpleChanges | null;

    private _newOnChanges(changes: SimpleChanges) {
        if (!this.abcFormControl)
            this._delayedChanges = changes;
    }

    ngOnChanges(changes: SimpleChanges) {
        if (changes["abcFormControl"] && this._otherDirective._parent) {
            if (changes["abcFormControl"].previousValue instanceof AbstractControl)
                this._remove();
            this._add();
        }
        if (this._delayedChanges) {
            this._oldOnChanges.call(this._otherDirective, this._delayedChanges);
            this._delayedChanges = null;
        }
    }

    private _add(): void {
        if (this._otherDirective._parent.control instanceof FormGroup)
            (<FormGroup>this._otherDirective._parent.control).addControl(this._otherDirective.name, this.abcFormControl);
        else
            (<FormArray>this._otherDirective._parent.control).insert(parseInt(this._otherDirective.name), this.abcFormControl);
    }

    private _remove(): void {
        if (this._otherDirective._parent.control instanceof FormGroup)
            (<FormGroup>this._otherDirective._parent.control).removeControl(this._otherDirective.name);
        else
            (<FormArray>this._otherDirective._parent.control).removeAt(parseInt(this._otherDirective.name));
    }
}

Thanks for considering!

Please tell us about your environment:

  • Angular version: 2.0.0-rc.4
  • Browser: [Chrome 51]
  • Language: [TypeScript 2.0.0]

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions