Angular ControlValueAccessor using a nested form fails to update

Discussão em 'Angular' iniciado por pop, Outubro 31, 2024.

  1. pop

    pop Guest

    I have a component, which implements ControlValueAccessor (via an abstract class which I used for other CVAs in my app) but it also uses a form, which has two more controls, one of which uses a CVA (via the same abstract class), too.

    This is how it is supposed to work:

    Parent form
    <-> Filter CVA
    <-> Input CVA


    Right now I can use the input CVA with a parent form and it works fine (can set from form value and can change its value), so:

    Parent form
    <-> Input CVA


    However when I use the intermediate layer, the Filter CVA component is only being updated from Input, but not from the parent form:

    Parent form
    <- Filter CVA
    <- Input CVA


    but not like this

    Parent form
    -> Filter CVA
    -> Input CVA


    After trying different ideas with the abstract class implementations and failing with all of them, I am now stuck. Why would a control value accessor fail to be patched from the parent form?

    Here is the setup:

    export class ParentComponent extends BaseComponent implements OnInit {

    constructor(
    private fb: UntypedFormBuilder
    ){
    super();

    this.form = this.fb.group({
    id: null,
    label: null,
    description: null,
    assignedTo: null,
    assignedTags: {tags:[45], state: 'or'}, // does not update underlying components!
    dueDate: [null, Validators.required],
    isTemplate: false,
    templateId: null,
    canComplete: true,
    notifyOnCompletion: false,
    isPrivate: false
    });
    }

    /** patches the form value ok, but will not update underlying components */
    public changeForm(): void {
    this.form.get('assignedTags').patchValue(null);
    }
    }


    Here is the filter component which seems to fail when setting the value from the outside form, but updates its value when the inner form is being updated

    @Component({
    selector: 'tag-filter',
    templateUrl: './tag-filter.component.html',
    styleUrls: ['./tag-filter.component.scss'],
    providers: [
    {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef( ()=> TagFilterComponent ),
    multi: true
    },
    {
    provide: NG_VALIDATORS,
    useExisting: forwardRef( ()=> TagFilterComponent ),
    multi: true
    }
    ],
    changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class TagFilterComponent extends AbstractInputComponent implements OnInit {

    @Output() public changed: EventEmitter<TagFilter> = new EventEmitter();

    public tags: Array<any> = [];

    public form: UntypedFormGroup;
    public STATES = {
    truthy: 'or',
    falsy: 'and'
    }

    private firstChange = true;

    constructor(
    private fb: UntypedFormBuilder,
    protected cdr: ChangeDetectorRef
    ) {
    super(cdr);
    this.form = this.fb.group({
    tags: [[]],
    state: 'or'
    });
    }

    public ngOnInit(): void {

    this.form.valueChanges
    .subscribe((res: TagFilter) => {

    // this never gets triggered from outside, only from the inner form
    if ( res.tags.length < 2 ) {
    res.state = 'or';
    this.form.patchValue({state: 'or'}, {emitEvent: false});
    }
    const v = {tags: res.tags, state: res.state}
    if ( !this.firstChange || v.tags.length > 0 ) {
    this.changed.emit(v);
    }
    this.firstChange = false;
    this.value = v;
    });

    if ( this.initialValue ) {
    this.form.patchValue(JSON.parse(JSON.stringify(this.initialValue)) );
    }
    }
    }


    Here is the HTML markup:

    <div class="tag-filter">
    <form [formGroup]="form">
    <tag-input
    formControlName="tags"></tag-input>
    <custom-toggle
    [state]="STATES.truthy"
    [stateFalse]="STATES.falsy"
    formControlName="state"></custom-toggle>
    </form>
    </div>


    And here is the Input component, which has a slightly different implementation for a CVA and works both ways:

    @Component({
    selector: 'tag-input',
    templateUrl: './tag-input.component.html',
    styleUrls: ['./tag-input.component.scss'],
    providers: [
    {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef( ()=> TagInputComponent ),
    multi: true
    },
    {
    provide: NG_VALIDATORS,
    useExisting: forwardRef( ()=> TagInputComponent ),
    multi: true
    }
    ]
    })
    export class TagInputComponent extends AbstractInputComponent implements ControlValueAccessor, Validator, OnInit, OnChanges {

    @Input() public formControlName: string;
    @Output() public changed: EventEmitter<any> = new EventEmitter();

    public allTags: Array<LabelValue> = [];

    constructor(
    protected cdr: ChangeDetectorRef,
    private tags: TagService,
    private controlContainer: ControlContainer
    ) {
    super(cdr);
    }

    /** somehow I have this implementation here */
    public get control() {
    return this.controlContainer.control.get(this.formControlName);
    }

    public ngOnInit(): void {
    TagService.newTags = [];
    this.readTags();

    this.control.valueChanges
    .subscribe(() => {
    /** triggers as expected */
    if ( this.control.pristine ) {
    this.readTags();
    }
    });
    }
    }


    Any ideas, why the Filter Component is failing to be updated from outside?

    Continue reading...

Compartilhe esta Página