docs(forms): update API reference for value accessors (#26946)

PR Close #26946
This commit is contained in:
Brandon Roberts 2018-11-01 15:23:01 -05:00 committed by Andrew Kushnir
parent e5c9f7a507
commit 99c5db1fb1
7 changed files with 439 additions and 99 deletions

View File

@ -17,17 +17,26 @@ export const CHECKBOX_VALUE_ACCESSOR: any = {
}; };
/** /**
* The accessor for writing a value and listening to changes on a checkbox input element. * @description
* A `ControlValueAccessor` for writing a value and listening to changes on a checkbox input
* element.
* *
* @usageNotes * @usageNotes
* ### Example
* *
* ``` * ### Using a checkbox with a reactive form.
* <input type="checkbox" name="rememberLogin" ngModel> *
* The following example shows how to use a checkbox with a reactive form.
*
* ```ts
* const rememberLoginControl = new FormControl();
* ```
*
* ```
* <input type="checkbox" [formControl]="rememberLoginControl">
* ``` * ```
* *
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi * @publicApi
*/ */
@Directive({ @Directive({
@ -37,17 +46,50 @@ export const CHECKBOX_VALUE_ACCESSOR: any = {
providers: [CHECKBOX_VALUE_ACCESSOR] providers: [CHECKBOX_VALUE_ACCESSOR]
}) })
export class CheckboxControlValueAccessor implements ControlValueAccessor { export class CheckboxControlValueAccessor implements ControlValueAccessor {
/**
* @description
* The registered callback function called when a change event occurs on the input element.
*/
onChange = (_: any) => {}; onChange = (_: any) => {};
/**
* @description
* The registered callback function called when a blur event occurs on the input element.
*/
onTouched = () => {}; onTouched = () => {};
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
/**
* Sets the "checked" property on the input element.
*
* @param value The checked value
*/
writeValue(value: any): void { writeValue(value: any): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'checked', value); this._renderer.setProperty(this._elementRef.nativeElement, 'checked', value);
} }
/**
* @description
* Registers a function called when the control value changes.
*
* @param fn The callback function
*/
registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; } registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; }
/**
* @description
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn: () => {}): void { this.onTouched = fn; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
/**
* Sets the "disabled" property on the input element.
*
* @param isDisabled The disabled value
*/
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
} }

View File

@ -32,18 +32,28 @@ function _isAndroid(): boolean {
export const COMPOSITION_BUFFER_MODE = new InjectionToken<boolean>('CompositionEventMode'); export const COMPOSITION_BUFFER_MODE = new InjectionToken<boolean>('CompositionEventMode');
/** /**
* The default accessor for writing a value and listening to changes that is used by the * @description
* `NgModel`, `FormControlDirective`, and `FormControlName` directives. * The default `ControlValueAccessor` for writing a value and listening to changes on input
* elements. The accessor is used by the `FormControlDirective`, `FormControlName`, and
* `NgModel` directives.
* *
* @usageNotes * @usageNotes
* ### Example
* *
* ``` * ### Using the default value accessor
* <input type="text" name="searchQuery" ngModel> *
* The following example shows how to use an input element that activates the default value accessor
* (in this case, a text field).
*
* ```ts
* const firstNameControl = new FormControl();
* ```
*
* ```
* <input type="text" [formControl]="firstNameControl">
* ``` * ```
* *
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi * @publicApi
*/ */
@Directive({ @Directive({
@ -61,7 +71,16 @@ export const COMPOSITION_BUFFER_MODE = new InjectionToken<boolean>('CompositionE
providers: [DEFAULT_VALUE_ACCESSOR] providers: [DEFAULT_VALUE_ACCESSOR]
}) })
export class DefaultValueAccessor implements ControlValueAccessor { export class DefaultValueAccessor implements ControlValueAccessor {
/**
* @description
* The registered callback function called when an input event occurs on the input element.
*/
onChange = (_: any) => {}; onChange = (_: any) => {};
/**
* @description
* The registered callback function called when a blur event occurs on the input element.
*/
onTouched = () => {}; onTouched = () => {};
/** Whether the user is creating a composition string (IME events). */ /** Whether the user is creating a composition string (IME events). */
@ -75,14 +94,37 @@ export class DefaultValueAccessor implements ControlValueAccessor {
} }
} }
/**
* Sets the "value" property on the input element.
*
* @param value The checked value
*/
writeValue(value: any): void { writeValue(value: any): void {
const normalizedValue = value == null ? '' : value; const normalizedValue = value == null ? '' : value;
this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue); this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
} }
/**
* @description
* Registers a function called when the control value changes.
*
* @param fn The callback function
*/
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; } registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
/**
* @description
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn: () => void): void { this.onTouched = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; }
/**
* Sets the "disabled" property on the input element.
*
* @param isDisabled The disabled value
*/
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
} }

View File

@ -17,18 +17,27 @@ export const NUMBER_VALUE_ACCESSOR: any = {
}; };
/** /**
* The accessor for writing a number value and listening to changes that is used by the * @description
* `NgModel`, `FormControlDirective`, and `FormControlName` directives. * The `ControlValueAccessor` for writing a number value and listening to number input changes.
* The value accessor is used by the `FormControlDirective`, `FormControlName`, and `NgModel`
* directives.
* *
* @usageNotes * @usageNotes
* ### Example
* *
* ``` * ### Using a number input with a reactive form.
* <input type="number" [(ngModel)]="age"> *
* The following example shows how to use a number input with a reactive form.
*
* ```ts
* const totalCountControl = new FormControl();
* ```
*
* ```
* <input type="number" [formControl]="totalCountControl">
* ``` * ```
* *
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
*/ */
@Directive({ @Directive({
selector: selector:
@ -41,22 +50,55 @@ export const NUMBER_VALUE_ACCESSOR: any = {
providers: [NUMBER_VALUE_ACCESSOR] providers: [NUMBER_VALUE_ACCESSOR]
}) })
export class NumberValueAccessor implements ControlValueAccessor { export class NumberValueAccessor implements ControlValueAccessor {
/**
* @description
* The registered callback function called when a change or input event occurs on the input
* element.
*/
onChange = (_: any) => {}; onChange = (_: any) => {};
/**
* @description
* The registered callback function called when a blur event occurs on the input element.
*/
onTouched = () => {}; onTouched = () => {};
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
/**
* Sets the "value" property on the input element.
*
* @param value The checked value
*/
writeValue(value: number): void { writeValue(value: number): void {
// The value needs to be normalized for IE9, otherwise it is set to 'null' when null // The value needs to be normalized for IE9, otherwise it is set to 'null' when null
const normalizedValue = value == null ? '' : value; const normalizedValue = value == null ? '' : value;
this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue); this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
} }
/**
* @description
* Registers a function called when the control value changes.
*
* @param fn The callback function
*/
registerOnChange(fn: (_: number|null) => void): void { registerOnChange(fn: (_: number|null) => void): void {
this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); }; this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); };
} }
/**
* @description
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn: () => void): void { this.onTouched = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; }
/**
* Sets the "disabled" property on the input element.
*
* @param isDisabled The disabled value
*/
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
} }

View File

@ -18,16 +18,25 @@ export const RADIO_VALUE_ACCESSOR: any = {
}; };
/** /**
* Internal class used by Angular to uncheck radio buttons with the matching name. * @description
* Class used by Angular to track radio buttons. For internal use only.
*/ */
@Injectable() @Injectable()
export class RadioControlRegistry { export class RadioControlRegistry {
private _accessors: any[] = []; private _accessors: any[] = [];
/**
* @description
* Adds a control to the internal registry. For internal use only.
*/
add(control: NgControl, accessor: RadioControlValueAccessor) { add(control: NgControl, accessor: RadioControlValueAccessor) {
this._accessors.push([control, accessor]); this._accessors.push([control, accessor]);
} }
/**
* @description
* Removes a control from the internal registry. For internal use only.
*/
remove(accessor: RadioControlValueAccessor) { remove(accessor: RadioControlValueAccessor) {
for (let i = this._accessors.length - 1; i >= 0; --i) { for (let i = this._accessors.length - 1; i >= 0; --i) {
if (this._accessors[i][1] === accessor) { if (this._accessors[i][1] === accessor) {
@ -37,6 +46,10 @@ export class RadioControlRegistry {
} }
} }
/**
* @description
* Selects a radio button. For internal use only.
*/
select(accessor: RadioControlValueAccessor) { select(accessor: RadioControlValueAccessor) {
this._accessors.forEach((c) => { this._accessors.forEach((c) => {
if (this._isSameGroup(c, accessor) && c[1] !== accessor) { if (this._isSameGroup(c, accessor) && c[1] !== accessor) {
@ -56,32 +69,22 @@ export class RadioControlRegistry {
/** /**
* @description * @description
* * The `ControlValueAccessor` for writing radio control values and listening to radio control
* Writes radio control values and listens to radio control changes. * changes. The value accessor is used by the `FormControlDirective`, `FormControlName`, and
* * `NgModel` directives.
* Used by `NgModel`, `FormControlDirective`, and `FormControlName`
* to keep the view synced with the `FormControl` model.
*
* If you have imported the `FormsModule` or the `ReactiveFormsModule`, this
* value accessor will be active on any radio control that has a form directive. You do
* **not** need to add a special selector to activate it.
* *
* @usageNotes * @usageNotes
* ### How to use radio buttons with form directives
* *
* To use radio buttons in a template-driven form, you'll want to ensure that radio buttons * ### Using radio buttons with reactive form directives
* in the same group have the same `name` attribute. Radio buttons with different `name`
* attributes do not affect each other.
* *
* {@example forms/ts/radioButtons/radio_button_example.ts region='TemplateDriven'} * The follow example shows how to use radio buttons in a reactive form. When using radio buttons in
* * a reactive form, radio buttons in the same group should have the same `formControlName`.
* When using radio buttons in a reactive form, radio buttons in the same group should have the * Providing a `name` attribute is optional.
* same `formControlName`. You can also add a `name` attribute, but it's optional.
* *
* {@example forms/ts/reactiveRadioButtons/reactive_radio_button_example.ts region='Reactive'} * {@example forms/ts/reactiveRadioButtons/reactive_radio_button_example.ts region='Reactive'}
* *
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi * @publicApi
*/ */
@Directive({ @Directive({
@ -101,32 +104,81 @@ export class RadioControlValueAccessor implements ControlValueAccessor,
/** @internal */ /** @internal */
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
_fn !: Function; _fn !: Function;
/**
* @description
* The registered callback function called when a change event occurs on the input element.
*/
onChange = () => {}; onChange = () => {};
/**
* @description
* The registered callback function called when a blur event occurs on the input element.
*/
onTouched = () => {}; onTouched = () => {};
/**
* @description
* Tracks the name of the radio input element.
*/
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() name !: string; @Input() name !: string;
/**
* @description
* Tracks the name of the `FormControl` bound to the directive. The name corresponds
* to a key in the parent `FormGroup` or `FormArray`.
*/
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
@Input() formControlName !: string; @Input() formControlName !: string;
/**
* @description
* Tracks the value of the radio input element
*/
@Input() value: any; @Input() value: any;
constructor( constructor(
private _renderer: Renderer2, private _elementRef: ElementRef, private _renderer: Renderer2, private _elementRef: ElementRef,
private _registry: RadioControlRegistry, private _injector: Injector) {} private _registry: RadioControlRegistry, private _injector: Injector) {}
/**
* @description
* A lifecycle method called when the directive is initialized. For internal use only.
*
* @param changes A object of key/value pairs for the set of changed inputs.
*/
ngOnInit(): void { ngOnInit(): void {
this._control = this._injector.get(NgControl); this._control = this._injector.get(NgControl);
this._checkName(); this._checkName();
this._registry.add(this._control, this); this._registry.add(this._control, this);
} }
/**
* @description
* Lifecycle method called before the directive's instance is destroyed. For internal use only.
*
* @param changes A object of key/value pairs for the set of changed inputs.
*/
ngOnDestroy(): void { this._registry.remove(this); } ngOnDestroy(): void { this._registry.remove(this); }
/**
* @description
* Sets the "checked" property value on the radio input element.
*
* @param value The checked value
*/
writeValue(value: any): void { writeValue(value: any): void {
this._state = value === this.value; this._state = value === this.value;
this._renderer.setProperty(this._elementRef.nativeElement, 'checked', this._state); this._renderer.setProperty(this._elementRef.nativeElement, 'checked', this._state);
} }
/**
* @description
* Registers a function called when the control value changes.
*
* @param fn The callback function
*/
registerOnChange(fn: (_: any) => {}): void { registerOnChange(fn: (_: any) => {}): void {
this._fn = fn; this._fn = fn;
this.onChange = () => { this.onChange = () => {
@ -135,10 +187,26 @@ export class RadioControlValueAccessor implements ControlValueAccessor,
}; };
} }
/**
* Sets the "value" on the radio input element and unchecks it.
*
* @param value
*/
fireUncheck(value: any): void { this.writeValue(value); } fireUncheck(value: any): void { this.writeValue(value); }
/**
* @description
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn: () => {}): void { this.onTouched = fn; } registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
/**
* Sets the "disabled" property on the input element.
*
* @param isDisabled The disabled value
*/
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
} }

View File

@ -17,18 +17,27 @@ export const RANGE_VALUE_ACCESSOR: StaticProvider = {
}; };
/** /**
* The accessor for writing a range value and listening to changes that is used by the * @description
* `NgModel`, `FormControlDirective`, and `FormControlName` directives. * The `ControlValueAccessor` for writing a range value and listening to range input changes.
* The value accessor is used by the `FormControlDirective`, `FormControlName`, and `NgModel`
* directives.
* *
* @usageNotes * @usageNotes
* ### Example
* *
* ``` * ### Using a range input with a reactive form
* <input type="range" [(ngModel)]="age" > *
* The following example shows how to use a range input with a reactive form.
*
* ```ts
* const ageControl = new FormControl();
* ```
*
* ```
* <input type="range" [formControl]="ageControl">
* ``` * ```
* *
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
*/ */
@Directive({ @Directive({
selector: selector:
@ -41,21 +50,53 @@ export const RANGE_VALUE_ACCESSOR: StaticProvider = {
providers: [RANGE_VALUE_ACCESSOR] providers: [RANGE_VALUE_ACCESSOR]
}) })
export class RangeValueAccessor implements ControlValueAccessor { export class RangeValueAccessor implements ControlValueAccessor {
/**
* @description
* The registered callback function called when a change or input event occurs on the input
* element.
*/
onChange = (_: any) => {}; onChange = (_: any) => {};
/**
* @description
* The registered callback function called when a blur event occurs on the input element.
*/
onTouched = () => {}; onTouched = () => {};
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
/**
* Sets the "value" property on the input element.
*
* @param value The checked value
*/
writeValue(value: any): void { writeValue(value: any): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'value', parseFloat(value)); this._renderer.setProperty(this._elementRef.nativeElement, 'value', parseFloat(value));
} }
/**
* @description
* Registers a function called when the control value changes.
*
* @param fn The callback function
*/
registerOnChange(fn: (_: number|null) => void): void { registerOnChange(fn: (_: number|null) => void): void {
this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); }; this.onChange = (value) => { fn(value == '' ? null : parseFloat(value)); };
} }
/**
* @description
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn: () => void): void { this.onTouched = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; }
/**
* Sets the "disabled" property on the range input element.
*
* @param isDisabled The disabled value
*/
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
} }

View File

@ -28,35 +28,26 @@ function _extractId(valueString: string): string {
/** /**
* @description * @description
* * The `ControlValueAccessor` for writing select control values and listening to select control
* Writes values and listens to changes on a select element. * changes. The value accessor is used by the `FormControlDirective`, `FormControlName`, and
* * `NgModel` directives.
* Used by `NgModel`, `FormControlDirective`, and `FormControlName`
* to keep the view synced with the `FormControl` model.
*
* If you have imported the `FormsModule` or the `ReactiveFormsModule`, this
* value accessor will be active on any select control that has a form directive. You do
* **not** need to add a special selector to activate it.
* *
* @usageNotes * @usageNotes
* ### How to use select controls with form directives *
* ### Using select controls in a reactive form
*
* The following examples show how to use a select control in a reactive form.
*
* {@example forms/ts/reactiveSelectControl/reactive_select_control_example.ts region='Component'}
*
* ### Using select controls in a template-driven form
* *
* To use a select in a template-driven form, simply add an `ngModel` and a `name` * To use a select in a template-driven form, simply add an `ngModel` and a `name`
* attribute to the main `<select>` tag. * attribute to the main `<select>` tag.
* *
* If your option values are simple strings, you can bind to the normal `value` property
* on the option. If your option values happen to be objects (and you'd like to save the
* selection in your form as an object), use `ngValue` instead:
*
* {@example forms/ts/selectControl/select_control_example.ts region='Component'} * {@example forms/ts/selectControl/select_control_example.ts region='Component'}
* *
* In reactive forms, you'll also want to add your form directive (`formControlName` or * ### Customizing option selection
* `formControl`) on the main `<select>` tag. Like in the former example, you have the
* choice of binding to the `value` or `ngValue` property on the select's options.
*
* {@example forms/ts/reactiveSelectControl/reactive_select_control_example.ts region='Component'}
*
* ### Caveat: Option selection
* *
* Angular uses object identity to select option. It's possible for the identities of items * Angular uses object identity to select option. It's possible for the identities of items
* to change while the data does not. This can happen, for example, if the items are produced * to change while the data does not. This can happen, for example, if the items are produced
@ -67,10 +58,12 @@ function _extractId(valueString: string): string {
* `compareWith` takes a **function** which has two arguments: `option1` and `option2`. * `compareWith` takes a **function** which has two arguments: `option1` and `option2`.
* If `compareWith` is given, Angular selects option by the return value of the function. * If `compareWith` is given, Angular selects option by the return value of the function.
* *
* ### Syntax * ```ts
* const selectedCountriesControl = new FormControl();
* ```
* *
* ``` * ```
* <select [compareWith]="compareFn" [(ngModel)]="selectedCountries"> * <select [compareWith]="compareFn" [formControl]="selectedCountriesControl">
* <option *ngFor="let country of countries" [ngValue]="country"> * <option *ngFor="let country of countries" [ngValue]="country">
* {{country.name}} * {{country.name}}
* </option> * </option>
@ -81,13 +74,13 @@ function _extractId(valueString: string): string {
* } * }
* ``` * ```
* *
* Note: We listen to the 'change' event because 'input' events aren't fired * **Note:** We listen to the 'change' event because 'input' events aren't fired
* for selects in Firefox and IE: * for selects in Firefox and IE:
* https://bugzilla.mozilla.org/show_bug.cgi?id=1024350 * https://bugzilla.mozilla.org/show_bug.cgi?id=1024350
* https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4660045/ * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/4660045/
* *
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi * @publicApi
*/ */
@Directive({ @Directive({
@ -103,9 +96,23 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
/** @internal */ /** @internal */
_idCounter: number = 0; _idCounter: number = 0;
/**
* @description
* The registered callback function called when a change event occurs on the input element.
*/
onChange = (_: any) => {}; onChange = (_: any) => {};
/**
* @description
* The registered callback function called when a blur event occurs on the input element.
*/
onTouched = () => {}; onTouched = () => {};
/**
* @description
* Tracks the option comparison algorithm for tracking identities when
* checking for changes.
*/
@Input() @Input()
set compareWith(fn: (o1: any, o2: any) => boolean) { set compareWith(fn: (o1: any, o2: any) => boolean) {
if (typeof fn !== 'function') { if (typeof fn !== 'function') {
@ -118,6 +125,12 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
/**
* Sets the "value" property on the input element. The "selectedIndex"
* property is also set if an ID is provided on the option element.
*
* @param value The checked value
*/
writeValue(value: any): void { writeValue(value: any): void {
this.value = value; this.value = value;
const id: string|null = this._getOptionId(value); const id: string|null = this._getOptionId(value);
@ -128,14 +141,32 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
this._renderer.setProperty(this._elementRef.nativeElement, 'value', valueString); this._renderer.setProperty(this._elementRef.nativeElement, 'value', valueString);
} }
/**
* @description
* Registers a function called when the control value changes.
*
* @param fn The callback function
*/
registerOnChange(fn: (value: any) => any): void { registerOnChange(fn: (value: any) => any): void {
this.onChange = (valueString: string) => { this.onChange = (valueString: string) => {
this.value = this._getOptionValue(valueString); this.value = this._getOptionValue(valueString);
fn(this.value); fn(this.value);
}; };
} }
/**
* @description
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn: () => any): void { this.onTouched = fn; } registerOnTouched(fn: () => any): void { this.onTouched = fn; }
/**
* Sets the "disabled" property on the select input element.
*
* @param isDisabled The disabled value
*/
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
} }
@ -160,17 +191,20 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
/** /**
* @description * @description
*
* Marks `<option>` as dynamic, so Angular can be notified when options change. * Marks `<option>` as dynamic, so Angular can be notified when options change.
* *
* See docs for `SelectControlValueAccessor` for usage examples. * @see `SelectControlValueAccessor`
* *
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi * @publicApi
*/ */
@Directive({selector: 'option'}) @Directive({selector: 'option'})
export class NgSelectOption implements OnDestroy { export class NgSelectOption implements OnDestroy {
/**
* @description
* ID of the option element
*/
// TODO(issue/24571): remove '!'. // TODO(issue/24571): remove '!'.
id !: string; id !: string;
@ -180,6 +214,11 @@ export class NgSelectOption implements OnDestroy {
if (this._select) this.id = this._select._registerOption(); if (this._select) this.id = this._select._registerOption();
} }
/**
* @description
* Tracks the value bound to the option element. Unlike the value binding,
* ngValue supports binding to objects.
*/
@Input('ngValue') @Input('ngValue')
set ngValue(value: any) { set ngValue(value: any) {
if (this._select == null) return; if (this._select == null) return;
@ -188,6 +227,11 @@ export class NgSelectOption implements OnDestroy {
this._select.writeValue(this._select.value); this._select.writeValue(this._select.value);
} }
/**
* @description
* Tracks simple string values bound to the option element.
* For objects, use the `ngValue` input binding.
*/
@Input('value') @Input('value')
set value(value: any) { set value(value: any) {
this._setElementValue(value); this._setElementValue(value);
@ -199,6 +243,10 @@ export class NgSelectOption implements OnDestroy {
this._renderer.setProperty(this._element.nativeElement, 'value', value); this._renderer.setProperty(this._element.nativeElement, 'value', value);
} }
/**
* @description
* Lifecycle method called before the directive's instance is destroyed. For internal use only.
*/
ngOnDestroy(): void { ngOnDestroy(): void {
if (this._select) { if (this._select) {
this._select._optionMap.delete(this.id); this._select._optionMap.delete(this.id);

View File

@ -41,33 +41,35 @@ abstract class HTMLCollection {
} }
/** /**
* The accessor for writing a value and listening to changes on a select element. * @description
* The `ControlValueAccessor` for writing multi-select control values and listening to multi-select control
* changes. The value accessor is used by the `FormControlDirective`, `FormControlName`, and `NgModel`
* directives.
*
* @see `SelectControlValueAccessor`
* *
* @usageNotes * @usageNotes
* ### Caveat: Options selection *
* * ### Using a multi-select control
* Angular uses object identity to select options. It's possible for the identities of items *
* to change while the data does not. This can happen, for example, if the items are produced * The follow example shows you how to use a multi-select control with a reactive form.
* from an RPC to the server, and that RPC is re-run. Even if the data hasn't changed, the *
* second response will produce objects with different identities. * ```ts
* * const countryControl = new FormControl();
* To customize the default option comparison algorithm, `<select multiple>` supports `compareWith` * ```
* input. `compareWith` takes a **function** which has two arguments: `option1` and `option2`.
* If `compareWith` is given, Angular selects options by the return value of the function.
*
* ### Syntax
* *
* ``` * ```
* <select multiple [compareWith]="compareFn" [(ngModel)]="selectedCountries"> * <select multiple name="countries" [formControl]="countryControl">
* <option *ngFor="let country of countries" [ngValue]="country"> * <option *ngFor="let country of countries" [ngValue]="country">
* {{country.name}} * {{ country.name }}
* </option> * </option>
* </select> * </select>
*
* compareFn(c1: Country, c2: Country): boolean {
* return c1 && c2 ? c1.id === c2.id : c1 === c2;
* }
* ``` * ```
*
* ### Customizing option selection
*
* To customize the default option comparison algorithm, `<select>` supports `compareWith` input.
* See the `SelectControlValueAccessor` for usage.
* *
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule * @ngModule FormsModule
@ -80,15 +82,34 @@ abstract class HTMLCollection {
providers: [SELECT_MULTIPLE_VALUE_ACCESSOR] providers: [SELECT_MULTIPLE_VALUE_ACCESSOR]
}) })
export class SelectMultipleControlValueAccessor implements ControlValueAccessor { export class SelectMultipleControlValueAccessor implements ControlValueAccessor {
/**
* @description
* The current value
*/
value: any; value: any;
/** @internal */ /** @internal */
_optionMap: Map<string, NgSelectMultipleOption> = new Map<string, NgSelectMultipleOption>(); _optionMap: Map<string, NgSelectMultipleOption> = new Map<string, NgSelectMultipleOption>();
/** @internal */ /** @internal */
_idCounter: number = 0; _idCounter: number = 0;
/**
* @description
* The registered callback function called when a change event occurs on the input element.
*/
onChange = (_: any) => {}; onChange = (_: any) => {};
/**
* @description
* The registered callback function called when a blur event occurs on the input element.
*/
onTouched = () => {}; onTouched = () => {};
/**
* @description
* Tracks the option comparison algorithm for tracking identities when
* checking for changes.
*/
@Input() @Input()
set compareWith(fn: (o1: any, o2: any) => boolean) { set compareWith(fn: (o1: any, o2: any) => boolean) {
if (typeof fn !== 'function') { if (typeof fn !== 'function') {
@ -101,6 +122,13 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
/**
* @description
* Sets the "value" property on one or of more
* of the select's options.
*
* @param value The value
*/
writeValue(value: any): void { writeValue(value: any): void {
this.value = value; this.value = value;
let optionSelectedStateSetter: (opt: NgSelectMultipleOption, o: any) => void; let optionSelectedStateSetter: (opt: NgSelectMultipleOption, o: any) => void;
@ -114,6 +142,13 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
this._optionMap.forEach(optionSelectedStateSetter); this._optionMap.forEach(optionSelectedStateSetter);
} }
/**
* @description
* Registers a function called when the control value changes
* and writes an array of the selected options.
*
* @param fn The callback function
*/
registerOnChange(fn: (value: any) => any): void { registerOnChange(fn: (value: any) => any): void {
this.onChange = (_: any) => { this.onChange = (_: any) => {
const selected: Array<any> = []; const selected: Array<any> = [];
@ -140,8 +175,20 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
fn(selected); fn(selected);
}; };
} }
/**
* @description
* Registers a function called when the control is touched.
*
* @param fn The callback function
*/
registerOnTouched(fn: () => any): void { this.onTouched = fn; } registerOnTouched(fn: () => any): void { this.onTouched = fn; }
/**
* Sets the "disabled" property on the select input element.
*
* @param isDisabled The disabled value
*/
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
} }
@ -169,18 +216,14 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
} }
/** /**
* @description
* Marks `<option>` as dynamic, so Angular can be notified when options change. * Marks `<option>` as dynamic, so Angular can be notified when options change.
* *
* @usageNotes * @see `SelectMultipleControlValueAccessor`
* ### Example
* *
* ```
* <select multiple name="city" ngModel>
* <option *ngFor="let c of cities" [value]="c"></option>
* </select>
* ```
* @ngModule FormsModule
* @ngModule ReactiveFormsModule * @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi
*/ */
@Directive({selector: 'option'}) @Directive({selector: 'option'})
export class NgSelectMultipleOption implements OnDestroy { export class NgSelectMultipleOption implements OnDestroy {
@ -197,6 +240,11 @@ export class NgSelectMultipleOption implements OnDestroy {
} }
} }
/**
* @description
* Tracks the value bound to the option element. Unlike the value binding,
* ngValue supports binding to objects.
*/
@Input('ngValue') @Input('ngValue')
set ngValue(value: any) { set ngValue(value: any) {
if (this._select == null) return; if (this._select == null) return;
@ -205,6 +253,11 @@ export class NgSelectMultipleOption implements OnDestroy {
this._select.writeValue(this._select.value); this._select.writeValue(this._select.value);
} }
/**
* @description
* Tracks simple string values bound to the option element.
* For objects, use the `ngValue` input binding.
*/
@Input('value') @Input('value')
set value(value: any) { set value(value: any) {
if (this._select) { if (this._select) {
@ -226,6 +279,10 @@ export class NgSelectMultipleOption implements OnDestroy {
this._renderer.setProperty(this._element.nativeElement, 'selected', selected); this._renderer.setProperty(this._element.nativeElement, 'selected', selected);
} }
/**
* @description
* Lifecycle method called before the directive's instance is destroyed. For internal use only.
*/
ngOnDestroy(): void { ngOnDestroy(): void {
if (this._select) { if (this._select) {
this._select._optionMap.delete(this.id); this._select._optionMap.delete(this.id);