Control Value Accessor (CVA)
The ControlValueAccessor interface is what you are looking for.
Why? This interface decouples the DOM from the Angular Form allowing the display drop down and input to differ from the value that is actually used by the form.
You can implement a custom input as a separate component* and pass a FormControl in.
The following is an untested Working Stackblitz
*Edit 3 - I believe it may also possible to implement this as a directive. See - Angular 2 Directive implementing ControlValueAccessor doesn't update 'touched' property on change
Outside the black box
Your app.component.html will end up looking like.
<form class="example-form">
<app-auto-special [users]="options" [formControl]="myControl"></app-auto-special>
</form>
app-auto-special acts like a blackbox where it cares only about the User id.
We can patchValue or setValue and it will do it's thing (call writeValue internally). If we interact with this component we get User id's for the FormControl value.
Edit - nothing is stopping you passing the whole User object around instead. I'm assumed OP wanted id based on the question.
Edit 2 - Example passing User object instead Working Stackbliz
Inside the black box
We need to register the app-auto-special component as a provider for the controlValueAccessor using NG_VALUE_ACCESSOR. This is done via:
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutoSpecialComponent),
multi: true
}]
So inside the black box we implement the interface comprising of 4 methods:
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
This usually means the following boilerplate:
export class AutoSpecialComponent implements ControlValueAccessor {
public _value: number;
public disabled: boolean;
onChanged: any = () => {};
onTouched: any = () => {};
/*
* ControlValueAccessor boilerplate
*
*/
writeValue(value): void {
this._value = value
}
registerOnChange(fn: any): void {
this.onChanged = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled
}
}
We make a copies of the functions provided by registerOnChange
and registerOnTouched
and call these copies (this.onChanged
and this.onTouched
) when we want to update the formControl value or it's on touched property. setDisabledState
is optional and writeValue
is called on initialization or when we call patchValue
or setValue
from the parent.
To set the FormControl we call this.onChanged(some_value);
and we can hook into various events input
, focusin
, blur
, optionSelected
and decide what happens separately to:
- The FormControl value
- Which options to show
- What display string should be in the input
This answer comes with the caveat that this is one of the first CVA implementations I've done so I'm shaky on the foundations.
Additional benefits
- Unit testing - isolated DOM display versus form
- Separation of logic - parent no longer saturated
Resources
Learn more from the following youtube video The Control Value Accessor | Jennifer Wadella
This is accompanied by the following slides