Search code examples
angulartypescriptangular-reactive-formsangular-controlvalueaccessor

Bind content child to form


Is there a way to bind a projected template (ContentChild) to the form defined on the child i.e. add formControlName once rendered?

Struggling to know what to search for online as I may be using the wrong search terms.

Non projected example - this works

I have a form array (a particular example) below hooked up to a control value accessor,
The component has form group of type FormGroup<{ formArray: FormArray<{ formControl : FormControl }> }
but on value changes I emit just the array of values i.e. formValue.formArray.map(v => v.formControl)
So formArray and formControl are just holding names internally on the component.

Particular example

<fieldset [formGroup]="formGroup">
    <legend>Paving runs (minimum 1)</legend>

    <ng-container formArrayName="formArray">
        <fieldset *ngFor="let frmGrp of formGroup.controls.formArray.controls; let i = index">
            <legend>Paving run</legend>
            
            <ng-container [formGroup]="frmGrp">
                <!-- what I want to project in a reusable version-->
                <paving-run-form
                    formControlName="formControl">
                </paving-run-form> 
            </ng-container>

            <button (click)="remove(i)">Delete run</button>
        </fieldset>
    </ng-container>
    
    <button (click)="add()">Add run</button>
</fieldset>

This is fairly standard and works but now I'm trying to create a reuseable component version of this.

So that I can do something like:

<generic-form-array formControlName="paved_runs" [labels]="labels">
    <ng-template editMode>
        <paving-run-form></paving-run-form>
    </ng-template>
</generic-form-array>

where labels is in this case would be

@Input() labels: GenericFormArrayOptions = {
    legendMany: "Paving runs (minimum 1)",
    legendOne: "Paving run",
    deleteOne: "Delete run",
    addOne: "Add run",
}

Projected example - what I've tried

<fieldset [formGroup]="formGroup">
    <legend>{{ labels.legendMany }}</legend>

    <ng-container formArrayName="formArray">
        <fieldset *ngFor="let frmGrp of formGroup.controls.formArray.controls; let i = index">
            <legend>{{ labels.legendOne }}</legend>
                
                    <ng-container [formGroup]="frmGrp">
                        <!-- project the form to edit one of array elements -->
                        <ng-container
                          [ngTemplateOutlet]="editModeTpl.templateRef">
                        </ng-container>
                    </ng-container>

            <button (click)="remove(i)">{{ labels.deleteOne }}</button>
        </fieldset>
    </ng-container>
    
    <button (click)="add()">{{ labels.addOne }}</button>
</fieldset>

With the following defined on the class:

@ContentChild(EditModeDirective) editModeTpl!: EditModeDirective

i.e. uses the directive

import { Directive, TemplateRef } from '@angular/core';

@Directive({
    selector: '[editMode]'
})
export class EditModeDirective {
    constructor(public templateRef: TemplateRef<any>) {}
}

In this case the content is projected as expected but <paving-run-form></paving-run-form> isn't bound to the child form. Trying <paving-run-form formControlName="formControl"></paving-run-form> doesn't work because this is expecting formControl on the parent.

Is there some way to render the component and then add it to the child form?


Solution

  • Very typical that I solve just after asking the question...

    In the reuseable component I need to pass the form group via the context

    <ng-container
        [ngTemplateOutlet]="editModeTpl.templateRef"
        [ngTemplateOutletContext]="{formGroup: frmGrp}">
    </ng-container>
    

    and when using as a customer I need to do the following:

    <generic-form-array formControlName="paved_runs">
        <ng-template editMode let-formGroup="formGroup">
            <paving-run-form [formControl]="formGroup.get('formControl')"></paving-run-form>
        </ng-template>
    </generic-form-array>
    

    Thanks to @Eliseo for leaving a helpful comment on a question I can no longer find :)