-
Notifications
You must be signed in to change notification settings - Fork 24.8k
/
form_builder.ts
454 lines (422 loc) · 18.9 KB
/
form_builder.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
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
/**
* @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 {inject, Injectable} from '@angular/core';
import {AsyncValidatorFn, ValidatorFn} from './directives/validators';
import {ReactiveFormsModule} from './form_providers';
import {AbstractControl, AbstractControlOptions, FormHooks} from './model/abstract_model';
import {FormArray, UntypedFormArray} from './model/form_array';
import {FormControl, FormControlOptions, FormControlState, UntypedFormControl} from './model/form_control';
import {FormGroup, FormRecord, UntypedFormGroup} from './model/form_group';
function isAbstractControlOptions(options: AbstractControlOptions|{[key: string]: any}|null|
undefined): options is AbstractControlOptions {
return !!options &&
((options as AbstractControlOptions).asyncValidators !== undefined ||
(options as AbstractControlOptions).validators !== undefined ||
(options as AbstractControlOptions).updateOn !== undefined);
}
/**
* The union of all validator types that can be accepted by a ControlConfig.
*/
type ValidatorConfig = ValidatorFn|AsyncValidatorFn|ValidatorFn[]|AsyncValidatorFn[];
/**
* The compiler may not always be able to prove that the elements of the control config are a tuple
* (i.e. occur in a fixed order). This slightly looser type is used for inference, to catch cases
* where the compiler cannot prove order and position.
*
* For example, consider the simple case `fb.group({foo: ['bar', Validators.required]})`. The
* compiler will infer this as an array, not as a tuple.
*/
type PermissiveControlConfig<T> = Array<T|FormControlState<T>|ValidatorConfig>;
/**
* ControlConfig<T> is a tuple containing a value of type T, plus optional validators and async
* validators.
*
* @publicApi
*/
export type ControlConfig<T> = [T|FormControlState<T>, (ValidatorFn|(ValidatorFn[]))?, (AsyncValidatorFn|AsyncValidatorFn[])?];
// Disable clang-format to produce clearer formatting for this multiline type.
// clang-format off
/**
* FormBuilder accepts values in various container shapes, as well as raw values.
* Element returns the appropriate corresponding model class, given the container T.
* The flag N, if not never, makes the resulting `FormControl` have N in its type.
*/
export type ɵElement<T, N extends null> =
// The `extends` checks are wrapped in arrays in order to prevent TypeScript from applying type unions
// through the distributive conditional type. This is the officially recommended solution:
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
//
// Identify FormControl container types.
[T] extends [FormControl<infer U>] ? FormControl<U> :
// Or FormControl containers that are optional in their parent group.
[T] extends [FormControl<infer U>|undefined] ? FormControl<U> :
// FormGroup containers.
[T] extends [FormGroup<infer U>] ? FormGroup<U> :
// Optional FormGroup containers.
[T] extends [FormGroup<infer U>|undefined] ? FormGroup<U> :
// FormRecord containers.
[T] extends [FormRecord<infer U>] ? FormRecord<U> :
// Optional FormRecord containers.
[T] extends [FormRecord<infer U>|undefined] ? FormRecord<U> :
// FormArray containers.
[T] extends [FormArray<infer U>] ? FormArray<U> :
// Optional FormArray containers.
[T] extends [FormArray<infer U>|undefined] ? FormArray<U> :
// Otherwise unknown AbstractControl containers.
[T] extends [AbstractControl<infer U>] ? AbstractControl<U> :
// Optional AbstractControl containers.
[T] extends [AbstractControl<infer U>|undefined] ? AbstractControl<U> :
// FormControlState object container, which produces a nullable control.
[T] extends [FormControlState<infer U>] ? FormControl<U|N> :
// A ControlConfig tuple, which produces a nullable control.
[T] extends [PermissiveControlConfig<infer U>] ? FormControl<Exclude<U, ValidatorConfig| AbstractControlOptions>|N> :
FormControl<T|N>;
// clang-format on
/**
* @description
* Creates an `AbstractControl` from a user-specified configuration.
*
* The `FormBuilder` provides syntactic sugar that shortens creating instances of a
* `FormControl`, `FormGroup`, or `FormArray`. It reduces the amount of boilerplate needed to
* build complex forms.
*
* @see [Reactive Forms Guide](guide/reactive-forms)
*
* @publicApi
*/
@Injectable({providedIn: ReactiveFormsModule})
export class FormBuilder {
private useNonNullable: boolean = false;
/**
* @description
* Returns a FormBuilder in which automatically constructed @see FormControl} elements
* have `{nonNullable: true}` and are non-nullable.
*
* **Constructing non-nullable controls**
*
* When constructing a control, it will be non-nullable, and will reset to its initial value.
*
* ```ts
* let nnfb = new FormBuilder().nonNullable;
* let name = nnfb.control('Alex'); // FormControl<string>
* name.reset();
* console.log(name); // 'Alex'
* ```
*
* **Constructing non-nullable groups or arrays**
*
* When constructing a group or array, all automatically created inner controls will be
* non-nullable, and will reset to their initial values.
*
* ```ts
* let nnfb = new FormBuilder().nonNullable;
* let name = nnfb.group({who: 'Alex'}); // FormGroup<{who: FormControl<string>}>
* name.reset();
* console.log(name); // {who: 'Alex'}
* ```
* **Constructing *nullable* fields on groups or arrays**
*
* It is still possible to have a nullable field. In particular, any `FormControl` which is
* *already* constructed will not be altered. For example:
*
* ```ts
* let nnfb = new FormBuilder().nonNullable;
* // FormGroup<{who: FormControl<string|null>}>
* let name = nnfb.group({who: new FormControl('Alex')});
* name.reset(); console.log(name); // {who: null}
* ```
*
* Because the inner control is constructed explicitly by the caller, the builder has
* no control over how it is created, and cannot exclude the `null`.
*/
get nonNullable(): NonNullableFormBuilder {
const nnfb = new FormBuilder();
nnfb.useNonNullable = true;
return nnfb as NonNullableFormBuilder;
}
/**
* @description
* Constructs a new `FormGroup` instance. Accepts a single generic argument, which is an object
* containing all the keys and corresponding inner control types.
*
* @param controls A collection of child controls. The key for each child is the name
* under which it is registered.
*
* @param options Configuration options object for the `FormGroup`. The object should have the
* `AbstractControlOptions` type and might contain the following fields:
* * `validators`: A synchronous validator function, or an array of validator functions.
* * `asyncValidators`: A single async validator or array of async validator functions.
* * `updateOn`: The event upon which the control should be updated (options: 'change' | 'blur'
* | submit').
*/
group<T extends {}>(
controls: T,
options?: AbstractControlOptions|null,
): FormGroup<{[K in keyof T]: ɵElement<T[K], null>}>;
/**
* @description
* Constructs a new `FormGroup` instance.
*
* @deprecated This API is not typesafe and can result in issues with Closure Compiler renaming.
* Use the `FormBuilder#group` overload with `AbstractControlOptions` instead.
* Note that `AbstractControlOptions` expects `validators` and `asyncValidators` to be valid
* validators. If you have custom validators, make sure their validation function parameter is
* `AbstractControl` and not a sub-class, such as `FormGroup`. These functions will be called
* with an object of type `AbstractControl` and that cannot be automatically downcast to a
* subclass, so TypeScript sees this as an error. For example, change the `(group: FormGroup) =>
* ValidationErrors|null` signature to be `(group: AbstractControl) => ValidationErrors|null`.
*
* @param controls A record of child controls. The key for each child is the name
* under which the control is registered.
*
* @param options Configuration options object for the `FormGroup`. The legacy configuration
* object consists of:
* * `validator`: A synchronous validator function, or an array of validator functions.
* * `asyncValidator`: A single async validator or array of async validator functions
* Note: the legacy format is deprecated and might be removed in one of the next major versions
* of Angular.
*/
group(
controls: {[key: string]: any},
options: {[key: string]: any},
): FormGroup;
group(controls: {[key: string]: any}, options: AbstractControlOptions|{[key: string]:
any}|null = null):
FormGroup {
const reducedControls = this._reduceControls(controls);
let newOptions: FormControlOptions = {};
if (isAbstractControlOptions(options)) {
// `options` are `AbstractControlOptions`
newOptions = options;
} else if (options !== null) {
// `options` are legacy form group options
newOptions.validators = (options as any).validator;
newOptions.asyncValidators = (options as any).asyncValidator;
}
return new FormGroup(reducedControls, newOptions);
}
/**
* @description
* Constructs a new `FormRecord` instance. Accepts a single generic argument, which is an object
* containing all the keys and corresponding inner control types.
*
* @param controls A collection of child controls. The key for each child is the name
* under which it is registered.
*
* @param options Configuration options object for the `FormRecord`. The object should have the
* `AbstractControlOptions` type and might contain the following fields:
* * `validators`: A synchronous validator function, or an array of validator functions.
* * `asyncValidators`: A single async validator or array of async validator functions.
* * `updateOn`: The event upon which the control should be updated (options: 'change' | 'blur'
* | submit').
*/
record<T>(controls: {[key: string]: T}, options: AbstractControlOptions|null = null):
FormRecord<ɵElement<T, null>> {
const reducedControls = this._reduceControls(controls);
// Cast to `any` because the inferred types are not as specific as Element.
return new FormRecord(reducedControls, options) as any;
}
/** @deprecated Use `nonNullable` instead. */
control<T>(formState: T|FormControlState<T>, opts: FormControlOptions&{
initialValueIsDefault: true
}): FormControl<T>;
control<T>(formState: T|FormControlState<T>, opts: FormControlOptions&{nonNullable: true}):
FormControl<T>;
/**
* @deprecated When passing an `options` argument, the `asyncValidator` argument has no effect.
*/
control<T>(
formState: T|FormControlState<T>, opts: FormControlOptions,
asyncValidator: AsyncValidatorFn|AsyncValidatorFn[]): FormControl<T|null>;
control<T>(
formState: T|FormControlState<T>,
validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl<T|null>;
/**
* @description
* Constructs a new `FormControl` with the given state, validators and options. Sets
* `{nonNullable: true}` in the options to get a non-nullable control. Otherwise, the
* control will be nullable. Accepts a single generic argument, which is the type of the
* control's value.
*
* @param formState Initializes the control with an initial state value, or
* with an object that contains both a value and a disabled status.
*
* @param validatorOrOpts A synchronous validator function, or an array of
* such functions, or a `FormControlOptions` object that contains
* validation functions and a validation trigger.
*
* @param asyncValidator A single async validator or array of async validator
* functions.
*
* @usageNotes
*
* ### Initialize a control as disabled
*
* The following example returns a control with an initial value in a disabled state.
*
* <code-example path="forms/ts/formBuilder/form_builder_example.ts" region="disabled-control">
* </code-example>
*/
control<T>(
formState: T|FormControlState<T>,
validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl {
let newOptions: FormControlOptions = {};
if (!this.useNonNullable) {
return new FormControl(formState, validatorOrOpts, asyncValidator);
}
if (isAbstractControlOptions(validatorOrOpts)) {
// If the second argument is options, then they are copied.
newOptions = validatorOrOpts;
} else {
// If the other arguments are validators, they are copied into an options object.
newOptions.validators = validatorOrOpts;
newOptions.asyncValidators = asyncValidator;
}
return new FormControl<T>(formState, {...newOptions, nonNullable: true});
}
/**
* Constructs a new `FormArray` from the given array of configurations,
* validators and options. Accepts a single generic argument, which is the type of each control
* inside the array.
*
* @param controls An array of child controls or control configs. Each child control is given an
* index when it is registered.
*
* @param validatorOrOpts A synchronous validator function, or an array of such functions, or an
* `AbstractControlOptions` object that contains
* validation functions and a validation trigger.
*
* @param asyncValidator A single async validator or array of async validator functions.
*/
array<T>(
controls: Array<T>, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement<T, null>> {
const createdControls = controls.map(c => this._createControl(c));
// Cast to `any` because the inferred types are not as specific as Element.
return new FormArray(createdControls, validatorOrOpts, asyncValidator) as any;
}
/** @internal */
_reduceControls<T>(controls:
{[k: string]: T|ControlConfig<T>|FormControlState<T>|AbstractControl<T>}):
{[key: string]: AbstractControl} {
const createdControls: {[key: string]: AbstractControl} = {};
Object.keys(controls).forEach(controlName => {
createdControls[controlName] = this._createControl(controls[controlName]);
});
return createdControls;
}
/** @internal */
_createControl<T>(controls: T|FormControlState<T>|ControlConfig<T>|FormControl<T>|
AbstractControl<T>): FormControl<T>|FormControl<T|null>|AbstractControl<T> {
if (controls instanceof FormControl) {
return controls as FormControl<T>;
} else if (controls instanceof AbstractControl) { // A control; just return it
return controls;
} else if (Array.isArray(controls)) { // ControlConfig Tuple
const value: T|FormControlState<T> = controls[0];
const validator: ValidatorFn|ValidatorFn[]|null = controls.length > 1 ? controls[1]! : null;
const asyncValidator: AsyncValidatorFn|AsyncValidatorFn[]|null =
controls.length > 2 ? controls[2]! : null;
return this.control<T>(value, validator, asyncValidator);
} else { // T or FormControlState<T>
return this.control<T>(controls);
}
}
}
/**
* @description
* `NonNullableFormBuilder` is similar to {@link FormBuilder}, but automatically constructed
* {@link FormControl} elements have `{nonNullable: true}` and are non-nullable.
*
* @publicApi
*/
@Injectable({
providedIn: ReactiveFormsModule,
useFactory: () => inject(FormBuilder).nonNullable,
})
export abstract class NonNullableFormBuilder {
/**
* Similar to `FormBuilder#group`, except any implicitly constructed `FormControl`
* will be non-nullable (i.e. it will have `nonNullable` set to true). Note
* that already-constructed controls will not be altered.
*/
abstract group<T extends {}>(
controls: T,
options?: AbstractControlOptions|null,
): FormGroup<{[K in keyof T]: ɵElement<T[K], never>}>;
/**
* Similar to `FormBuilder#record`, except any implicitly constructed `FormControl`
* will be non-nullable (i.e. it will have `nonNullable` set to true). Note
* that already-constructed controls will not be altered.
*/
abstract record<T>(
controls: {[key: string]: T},
options?: AbstractControlOptions|null,
): FormRecord<ɵElement<T, never>>;
/**
* Similar to `FormBuilder#array`, except any implicitly constructed `FormControl`
* will be non-nullable (i.e. it will have `nonNullable` set to true). Note
* that already-constructed controls will not be altered.
*/
abstract array<T>(
controls: Array<T>, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement<T, never>>;
/**
* Similar to `FormBuilder#control`, except this overridden version of `control` forces
* `nonNullable` to be `true`, resulting in the control always being non-nullable.
*/
abstract control<T>(
formState: T|FormControlState<T>,
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl<T>;
}
/**
* UntypedFormBuilder is the same as @see FormBuilder, but it provides untyped controls.
*/
@Injectable({providedIn: ReactiveFormsModule})
export class UntypedFormBuilder extends FormBuilder {
/**
* Like `FormBuilder#group`, except the resulting group is untyped.
*/
override group(
controlsConfig: {[key: string]: any},
options?: AbstractControlOptions|null,
): UntypedFormGroup;
/**
* @deprecated This API is not typesafe and can result in issues with Closure Compiler renaming.
* Use the `FormBuilder#group` overload with `AbstractControlOptions` instead.
*/
override group(
controlsConfig: {[key: string]: any},
options: {[key: string]: any},
): UntypedFormGroup;
override group(
controlsConfig: {[key: string]: any},
options: AbstractControlOptions|{[key: string]: any}|null = null): UntypedFormGroup {
return super.group(controlsConfig, options);
}
/**
* Like `FormBuilder#control`, except the resulting control is untyped.
*/
override control(
formState: any, validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): UntypedFormControl {
return super.control(formState, validatorOrOpts, asyncValidator);
}
/**
* Like `FormBuilder#array`, except the resulting array is untyped.
*/
override array(
controlsConfig: any[],
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): UntypedFormArray {
return super.array(controlsConfig, validatorOrOpts, asyncValidator);
}
}