How to set Angular formControlName on a component wrapping a custom form control (CVA)

Discussão em 'Angular' iniciado por Vincent, Dezembro 4, 2024.

  1. Vincent

    Vincent Guest

    In a Angular 19 app with a reactive form, I am developing a root component containing a form whose unique and generic control is wrapped in another component that fixes default values for some of its inputs. Actually, I cannot find an elegant way allowing the root form to define the formControlName and propagate it to the child form control through the wrapping component.

    My actual HTML tree looks like this:

    <form [formGroup]="formGroup"> <!-- RootComponent -->
    <!--
    IDEALLY, I WOULD LIKE TO USE DIRECTIVE 'formControlName' INSTEAD OF A CUSTOM INPUT 'name',
    BUT THIS CHILD COMPONENT IS A NOT ITSELF A FORM CONTROL
    -->
    <app-image-file-chooser name="files"> <!-- ImageFileChooserComponent (not a form control) -->
    <app-file-chooser [formControlName]="name()" accept="image/jpeg, image/png"> <!-- FileChooserComponent (custom form control implementing CVA) -->
    <input type="file" [accept]="accept" ...>
    </app-file-chooser>
    </app-image-file-chooser>
    </form>


    Ideally, I would like to get this result:

    <form [formGroup]="formGroup"> <!-- RootComponent -->
    <app-image-file-chooser formControlName="files"> <!-- ImageFileChooserComponent (not a form control) -->
    <app-file-chooser accept="image/jpeg, image/png"> <!-- FileChooserComponent (custom form control implementing CVA) -->
    <input type="file" [accept]="accept" ...>
    </app-file-chooser>
    </app-image-file-chooser>
    </form>


    Here are the different component implementations:

    ////////////////////////////// ROOT FORM COMPONENT //////////////////////////////
    import { Component, inject } from '@angular/core';
    import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
    import { ImageFileChooserComponent } from './image-file-chooser.component';

    @Component({
    selector: 'app-root',
    changeDetection: ChangeDetectionStrategy.OnPush,
    imports: [ImageFileSelectorComponent, ReactiveFormsModule],
    template: `
    <form [formGroup]="formGroup">
    <!--
    IDEALLY, I WOULD LIKE TO USE DIRECTIVE 'formControlName' INSTEAD OF A CUSTOM INPUT 'name',
    BUT THIS CHILD COMPONENT IS A NOT ITSELF A FORM CONTROL
    -->
    <app-image-file-chooser name="files"></app-image-file-chooser>
    </form>
    `
    })
    export class RootComponent {

    private readonly formBuilder = inject(FormBuilder);

    readonly formGroup = this.formBuilder.group({
    files: this.formBuilder.control<FileList | null>(null)
    });
    }

    ////////////////////////////// IMAGE FILE CHOOSER //////////////////////////////
    import { Component, inject } from '@angular/core';
    import { ControlContainer, FormGroup, ReactiveFormsModule } from '@angular/forms';
    import { FileChooserComponent } from './file-chooser.component';

    @Component({
    selector: 'app-image-file-chooser',
    changeDetection: ChangeDetectionStrategy.OnPush,
    imports: [FileChooserComponent, ReactiveFormsModule],
    template: `
    <ng-container [formGroup]="formGroup!">
    <app-file-chooser [formControlName]="name()" accept="image/jpeg, image/png"></app-file-chooser>
    </ng-container>
    `
    })
    export class ImageFileChooserComponent {
    private readonly controlContainer = inject(ControlContainer);

    name = input.required<string>();

    formGroup: FormGroup | null = null;

    ngOnInit(): void {
    this.formGroup = this.controlContainer.control as FormGroup;
    }
    }

    ////////////////////////////// FILE CHOOSER //////////////////////////////
    import { booleanAttribute, ChangeDetectionStrategy, Component, forwardRef, inject, input } from '@angular/core';
    import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

    @Component({
    selector: 'app-file-chooser',
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => FileChooserComponent),
    multi: true
    }],
    template: `<input type="file" [accept]="accept">`
    })
    export class FileChooserComponent implements ControlValueAccessor {

    accept = input('');

    // Implementation hidden for clarity
    }



    Question: is there a way to keep only a formControlName directive on a non-form control child element and avoid progagating the name with inputs to the nested form control component?

    Continue reading...

Compartilhe esta Página