I'm trying to create a custom form control in Angular (v5). The custom control is essentially a wrapper around an Angular Material component, but with some extra stuff going on.
I've read various tutorials on implementing ControlValueAccessor
, but I can't find anything that accounts for writing a component to wrap an existing component.
Ideally, I want a custom component that displays the Angular Material component (with some extra bindings and stuff going on), but to be able to pass in validation from the parent form (e.g. required
) and have the Angular Material components handle that.
Example:
Outer component, containing a form and using custom component
<form [formGroup]="myForm">
<div formArrayName="things">
<div *ngFor="let thing of things; let i = index;">
<app-my-custom-control [formControlName]="i"></app-my-custom-control>
</div>
</div>
</form>
Custom component template
Essentially my custom form component just wraps an Angular Material drop-down with autocomplete. I could do this without creating a custom component, but it seems to make sense to do it this way as all the code for handling filtering etc. can live within that component class, rather than being in the container class (which doesn't need to care about the implementation of this).
<mat-form-field>
<input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
<mat-autocomplete #thingInput="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
So, on the input
changing, that value should be used as the form value.
Things I've tried
I've tried a few ways of doing this, all with their own pitfalls:
Simple event binding
Bind to keyup
and blur
events on the input
, and then notify the parent of the change (i.e. call the function that Angular passes into registerOnChange
as part of implementing ControlValueAccessor
).
That sort of works, but on selecting a value from the dropdown it seems the change events don't fire and you end up in an inconsistent state.
It also doesn't account for validation (e.g. if it's "required", when a value isn;t set the form control will correctly be invalid, but the Angular Material component won't show as such).
Nested form
This is a bit closer. I've created a new form within the custom component class, which has a single control. In the component template, I pass in that form control to the Angular Material component. In the class, I subscribe to valueChanges
of that and then propagate the changes back to the parent (via the function passed into registerOnChange
).
This sort of works, but feels messy and like there should be a better way.
It also means that any validation applied to my custom form control (by the container component) is ignored, as I've created a new "inner form" that lacks the original validation.
Don't use ControlValueAccessor
at all, and instead just pass in the form
As the title says... I tried not doing this the "proper" way, and instead added a binding to the parent form. I then create a form control within the custom component as part of that parent form.
This works for handling value updates, and to an extent validation (but it has to be created as part of the component, not the parent form), but this just feels wrong.
Summary
What's the proper way of handling this? It feels like I'm just stumbling through different anti-patterns, but I can't find anything in the docs to suggest that this is even supported.