-
Notifications
You must be signed in to change notification settings - Fork 24.8k
/
ng_model.ts
355 lines (321 loc) Β· 12.9 KB
/
ng_model.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {booleanAttribute, ChangeDetectorRef, Directive, EventEmitter, forwardRef, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Provider, Self, SimpleChanges} from '@angular/core';
import {FormHooks} from '../model/abstract_model';
import {FormControl} from '../model/form_control';
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
import {ControlContainer} from './control_container';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
import {NgControl} from './ng_control';
import {NgForm} from './ng_form';
import {NgModelGroup} from './ng_model_group';
import {CALL_SET_DISABLED_STATE, controlPath, isPropertyUpdated, selectValueAccessor, SetDisabledStateOption, setUpControl} from './shared';
import {formGroupNameException, missingNameException, modelParentException} from './template_driven_errors';
import {AsyncValidator, AsyncValidatorFn, Validator, ValidatorFn} from './validators';
const formControlBinding: Provider = {
provide: NgControl,
useExisting: forwardRef(() => NgModel)
};
/**
* `ngModel` forces an additional change detection run when its inputs change:
* E.g.:
* ```
* <div>{{myModel.valid}}</div>
* <input [(ngModel)]="myValue" #myModel="ngModel">
* ```
* I.e. `ngModel` can export itself on the element and then be used in the template.
* Normally, this would result in expressions before the `input` that use the exported directive
* to have an old value as they have been
* dirty checked before. As this is a very common case for `ngModel`, we added this second change
* detection run.
*
* Notes:
* - this is just one extra run no matter how many `ngModel`s have been changed.
* - this is a general problem when using `exportAs` for directives!
*/
const resolvedPromise = (() => Promise.resolve())();
/**
* @description
* Creates a `FormControl` instance from a [domain
* model](https://en.wikipedia.org/wiki/Domain_model) and binds it to a form control element.
*
* The `FormControl` instance tracks the value, user interaction, and
* validation status of the control and keeps the view synced with the model. If used
* within a parent form, the directive also registers itself with the form as a child
* control.
*
* This directive is used by itself or as part of a larger form. Use the
* `ngModel` selector to activate it.
*
* It accepts a domain model as an optional `Input`. If you have a one-way binding
* to `ngModel` with `[]` syntax, changing the domain model's value in the component
* class sets the value in the view. If you have a two-way binding with `[()]` syntax
* (also known as 'banana-in-a-box syntax'), the value in the UI always syncs back to
* the domain model in your class.
*
* To inspect the properties of the associated `FormControl` (like the validity state),
* export the directive into a local template variable using `ngModel` as the key (ex:
* `#myVar="ngModel"`). You can then access the control using the directive's `control` property.
* However, the most commonly used properties (like `valid` and `dirty`) also exist on the control
* for direct access. See a full list of properties directly available in
* `AbstractControlDirective`.
*
* @see {@link RadioControlValueAccessor}
* @see {@link SelectControlValueAccessor}
*
* @usageNotes
*
* ### Using ngModel on a standalone control
*
* The following examples show a simple standalone control using `ngModel`:
*
* {@example forms/ts/simpleNgModel/simple_ng_model_example.ts region='Component'}
*
* When using the `ngModel` within `<form>` tags, you'll also need to supply a `name` attribute
* so that the control can be registered with the parent form under that name.
*
* In the context of a parent form, it's often unnecessary to include one-way or two-way binding,
* as the parent form syncs the value for you. You access its properties by exporting it into a
* local template variable using `ngForm` such as (`#f="ngForm"`). Use the variable where
* needed on form submission.
*
* If you do need to populate initial values into your form, using a one-way binding for
* `ngModel` tends to be sufficient as long as you use the exported form's value rather
* than the domain model's value on submit.
*
* ### Using ngModel within a form
*
* The following example shows controls using `ngModel` within a form:
*
* {@example forms/ts/simpleForm/simple_form_example.ts region='Component'}
*
* ### Using a standalone ngModel within a group
*
* The following example shows you how to use a standalone ngModel control
* within a form. This controls the display of the form, but doesn't contain form data.
*
* ```html
* <form>
* <input name="login" ngModel placeholder="Login">
* <input type="checkbox" ngModel [ngModelOptions]="{standalone: true}"> Show more options?
* </form>
* <!-- form value: {login: ''} -->
* ```
*
* ### Setting the ngModel `name` attribute through options
*
* The following example shows you an alternate way to set the name attribute. Here,
* an attribute identified as name is used within a custom form control component. To still be able
* to specify the NgModel's name, you must specify it using the `ngModelOptions` input instead.
*
* ```html
* <form>
* <my-custom-form-control name="Nancy" ngModel [ngModelOptions]="{name: 'user'}">
* </my-custom-form-control>
* </form>
* <!-- form value: {user: ''} -->
* ```
*
* @ngModule FormsModule
* @publicApi
*/
@Directive({
selector: '[ngModel]:not([formControlName]):not([formControl])',
providers: [formControlBinding],
exportAs: 'ngModel'
})
export class NgModel extends NgControl implements OnChanges, OnDestroy {
public override readonly control: FormControl = new FormControl();
// At runtime we coerce arbitrary values assigned to the "disabled" input to a "boolean".
// This is not reflected in the type of the property because outside of templates, consumers
// should only deal with booleans. In templates, a string is allowed for convenience and to
// match the native "disabled attribute" semantics which can be observed on input elements.
// This static member tells the compiler that values of type "string" can also be assigned
// to the input in a template.
/** @nodoc */
static ngAcceptInputType_isDisabled: boolean|string;
/** @internal */
_registered = false;
/**
* Internal reference to the view model value.
* @nodoc
*/
viewModel: any;
/**
* @description
* Tracks the name bound to the directive. If a parent form exists, it
* uses this name as a key to retrieve this control's value.
*/
@Input() override name: string = '';
/**
* @description
* Tracks whether the control is disabled.
*/
// TODO(issue/24571): remove '!'.
@Input('disabled') isDisabled!: boolean;
/**
* @description
* Tracks the value bound to this directive.
*/
@Input('ngModel') model: any;
/**
* @description
* Tracks the configuration options for this `ngModel` instance.
*
* **name**: An alternative to setting the name attribute on the form control element. See
* the [example](api/forms/NgModel#using-ngmodel-on-a-standalone-control) for using `NgModel`
* as a standalone control.
*
* **standalone**: When set to true, the `ngModel` will not register itself with its parent form,
* and acts as if it's not in the form. Defaults to false. If no parent form exists, this option
* has no effect.
*
* **updateOn**: Defines the event upon which the form control value and validity update.
* Defaults to 'change'. Possible values: `'change'` | `'blur'` | `'submit'`.
*
*/
// TODO(issue/24571): remove '!'.
@Input('ngModelOptions') options!: {name?: string, standalone?: boolean, updateOn?: FormHooks};
/**
* @description
* Event emitter for producing the `ngModelChange` event after
* the view model updates.
*/
@Output('ngModelChange') update = new EventEmitter();
constructor(
@Optional() @Host() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator|ValidatorFn)[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
(AsyncValidator|AsyncValidatorFn)[],
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
@Optional() @Inject(ChangeDetectorRef) private _changeDetectorRef?: ChangeDetectorRef|null,
@Optional() @Inject(CALL_SET_DISABLED_STATE) private callSetDisabledState?:
SetDisabledStateOption) {
super();
this._parent = parent;
this._setValidators(validators);
this._setAsyncValidators(asyncValidators);
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
/** @nodoc */
ngOnChanges(changes: SimpleChanges) {
this._checkForErrors();
if (!this._registered || 'name' in changes) {
if (this._registered) {
this._checkName();
if (this.formDirective) {
// We can't call `formDirective.removeControl(this)`, because the `name` has already been
// changed. We also can't reset the name temporarily since the logic in `removeControl`
// is inside a promise and it won't run immediately. We work around it by giving it an
// object with the same shape instead.
const oldName = changes['name'].previousValue;
this.formDirective.removeControl({name: oldName, path: this._getPath(oldName)});
}
}
this._setUpControl();
}
if ('isDisabled' in changes) {
this._updateDisabled(changes);
}
if (isPropertyUpdated(changes, this.viewModel)) {
this._updateValue(this.model);
this.viewModel = this.model;
}
}
/** @nodoc */
ngOnDestroy(): void {
this.formDirective && this.formDirective.removeControl(this);
}
/**
* @description
* Returns an array that represents the path from the top-level form to this control.
* Each index is the string name of the control on that level.
*/
override get path(): string[] {
return this._getPath(this.name);
}
/**
* @description
* The top-level directive for this control if present, otherwise null.
*/
get formDirective(): any {
return this._parent ? this._parent.formDirective : null;
}
/**
* @description
* Sets the new value for the view model and emits an `ngModelChange` event.
*
* @param newValue The new value emitted by `ngModelChange`.
*/
override viewToModelUpdate(newValue: any): void {
this.viewModel = newValue;
this.update.emit(newValue);
}
private _setUpControl(): void {
this._setUpdateStrategy();
this._isStandalone() ? this._setUpStandalone() : this.formDirective.addControl(this);
this._registered = true;
}
private _setUpdateStrategy(): void {
if (this.options && this.options.updateOn != null) {
this.control._updateOn = this.options.updateOn;
}
}
private _isStandalone(): boolean {
return !this._parent || !!(this.options && this.options.standalone);
}
private _setUpStandalone(): void {
setUpControl(this.control, this, this.callSetDisabledState);
this.control.updateValueAndValidity({emitEvent: false});
}
private _checkForErrors(): void {
if (!this._isStandalone()) {
this._checkParentType();
}
this._checkName();
}
private _checkParentType(): void {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!(this._parent instanceof NgModelGroup) &&
this._parent instanceof AbstractFormGroupDirective) {
throw formGroupNameException();
} else if (!(this._parent instanceof NgModelGroup) && !(this._parent instanceof NgForm)) {
throw modelParentException();
}
}
}
private _checkName(): void {
if (this.options && this.options.name) this.name = this.options.name;
if (!this._isStandalone() && !this.name && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw missingNameException();
}
}
private _updateValue(value: any): void {
resolvedPromise.then(() => {
this.control.setValue(value, {emitViewToModelChange: false});
this._changeDetectorRef?.markForCheck();
});
}
private _updateDisabled(changes: SimpleChanges) {
const disabledValue = changes['isDisabled'].currentValue;
// checking for 0 to avoid breaking change
const isDisabled = disabledValue !== 0 && booleanAttribute(disabledValue);
resolvedPromise.then(() => {
if (isDisabled && !this.control.disabled) {
this.control.disable();
} else if (!isDisabled && this.control.disabled) {
this.control.enable();
}
this._changeDetectorRef?.markForCheck();
});
}
private _getPath(controlName: string): string[] {
return this._parent ? controlPath(controlName, this._parent) : [controlName];
}
}