|
6 | 6 | * found in the LICENSE file at https://angular.dev/license |
7 | 7 | */ |
8 | 8 |
|
9 | | -import {inject, Injector, runInInjectionContext, untracked, WritableSignal} from '@angular/core'; |
10 | | - |
| 9 | +import { |
| 10 | + inject, |
| 11 | + Injector, |
| 12 | + runInInjectionContext, |
| 13 | + ɵRuntimeError as RuntimeError, |
| 14 | + untracked, |
| 15 | + WritableSignal, |
| 16 | +} from '@angular/core'; |
| 17 | +import {RuntimeErrorCode} from '../errors'; |
11 | 18 | import {BasicFieldAdapter, FieldAdapter} from '../field/field_adapter'; |
12 | 19 | import {FormFieldManager} from '../field/manager'; |
13 | 20 | import {FieldNode} from '../field/node'; |
@@ -56,14 +63,16 @@ export interface FormSubmitOptions<TModel> { |
56 | 63 | * @category structure |
57 | 64 | * @experimental 21.0.0 |
58 | 65 | */ |
59 | | -export interface FormOptions { |
| 66 | +export interface FormOptions<TModel> { |
60 | 67 | /** |
61 | 68 | * The injector to use for dependency injection. If this is not provided, the injector for the |
62 | 69 | * current [injection context](guide/di/dependency-injection-context), will be used. |
63 | 70 | */ |
64 | 71 | injector?: Injector; |
65 | 72 | /** The name of the root form, used in generating name attributes for the fields. */ |
66 | 73 | name?: string; |
| 74 | + /** Options that define how to handle form submission. */ |
| 75 | + submission?: FormSubmitOptions<TModel>; |
67 | 76 |
|
68 | 77 | /** |
69 | 78 | * Adapter allows managing fields in a more flexible way. |
@@ -148,7 +157,7 @@ export function form<TModel>(model: WritableSignal<TModel>): FieldTree<TModel>; |
148 | 157 | */ |
149 | 158 | export function form<TModel>( |
150 | 159 | model: WritableSignal<TModel>, |
151 | | - schemaOrOptions: SchemaOrSchemaFn<TModel> | FormOptions, |
| 160 | + schemaOrOptions: SchemaOrSchemaFn<TModel> | FormOptions<TModel>, |
152 | 161 | ): FieldTree<TModel>; |
153 | 162 |
|
154 | 163 | /** |
@@ -197,14 +206,18 @@ export function form<TModel>( |
197 | 206 | export function form<TModel>( |
198 | 207 | model: WritableSignal<TModel>, |
199 | 208 | schema: SchemaOrSchemaFn<TModel>, |
200 | | - options: FormOptions, |
| 209 | + options: FormOptions<TModel>, |
201 | 210 | ): FieldTree<TModel>; |
202 | 211 |
|
203 | 212 | export function form<TModel>(...args: any[]): FieldTree<TModel> { |
204 | 213 | const [model, schema, options] = normalizeFormArgs<TModel>(args); |
205 | 214 | const injector = options?.injector ?? inject(Injector); |
206 | 215 | const pathNode = runInInjectionContext(injector, () => SchemaImpl.rootCompile(schema)); |
207 | | - const fieldManager = new FormFieldManager(injector, options?.name); |
| 216 | + const fieldManager = new FormFieldManager( |
| 217 | + injector, |
| 218 | + options?.name, |
| 219 | + options?.submission as FormSubmitOptions<unknown> | undefined, |
| 220 | + ); |
208 | 221 | const adapter = options?.adapter ?? new BasicFieldAdapter(); |
209 | 222 | const fieldRoot = FieldNode.newRoot(fieldManager, model, pathNode, adapter); |
210 | 223 | fieldManager.createFieldManagementEffect(fieldRoot.structure); |
@@ -389,38 +402,62 @@ export function applyWhenValue( |
389 | 402 | */ |
390 | 403 | export async function submit<TModel>( |
391 | 404 | form: FieldTree<TModel>, |
392 | | - options: FormSubmitOptions<TModel>, |
| 405 | + options?: FormSubmitOptions<TModel>, |
| 406 | +): Promise<boolean>; |
| 407 | +export async function submit<TModel>( |
| 408 | + form: FieldTree<TModel>, |
| 409 | + action: FormSubmitOptions<TModel>['action'], |
| 410 | +): Promise<boolean>; |
| 411 | +export async function submit<TModel>( |
| 412 | + form: FieldTree<TModel>, |
| 413 | + options?: FormSubmitOptions<TModel> | FormSubmitOptions<TModel>['action'], |
393 | 414 | ): Promise<boolean> { |
394 | | - return untracked(async () => { |
395 | | - const {action, onInvalid} = options; |
396 | | - const ignoreValidators = options.ignoreValidators ?? 'pending'; |
397 | | - const node = form() as unknown as FieldNode; |
| 415 | + const node = form() as unknown as FieldNode; |
| 416 | + const opts = |
| 417 | + typeof options === 'function' |
| 418 | + ? {action: options} |
| 419 | + : ({ |
| 420 | + ...(node.structure.fieldManager.submitOptions ?? {}), |
| 421 | + ...(options ?? {}), |
| 422 | + } as Partial<FormSubmitOptions<TModel>>); |
| 423 | + const action = opts?.action; |
| 424 | + if (!action) { |
| 425 | + throw new RuntimeError( |
| 426 | + RuntimeErrorCode.MISSING_SUBMIT_ACTION, |
| 427 | + ngDevMode && |
| 428 | + 'Cannot submit form with no submit action. Specify the action when creating the form, or as an additional argument to `submit()`.', |
| 429 | + ); |
| 430 | + } |
| 431 | + |
| 432 | + const onInvalid = opts?.onInvalid; |
| 433 | + const ignoreValidators = opts?.ignoreValidators ?? 'pending'; |
398 | 434 |
|
| 435 | + // Determine whether or not to run the action based on the current validity. |
| 436 | + let shouldRunAction = true; |
| 437 | + untracked(() => { |
399 | 438 | markAllAsTouched(node); |
400 | 439 |
|
401 | | - // Determine whether or not to run the action based on the current validity. |
402 | | - let shouldRunAction = true; |
403 | 440 | if (ignoreValidators === 'none') { |
404 | 441 | shouldRunAction = node.valid(); |
405 | 442 | } else if (ignoreValidators === 'pending') { |
406 | 443 | shouldRunAction = !node.invalid(); |
407 | 444 | } |
| 445 | + }); |
408 | 446 |
|
409 | | - // Run the action (or alternatively the `onInvalid` callback) |
410 | | - try { |
411 | | - if (shouldRunAction) { |
412 | | - node.submitState.selfSubmitting.set(true); |
413 | | - const errors = await action(form); |
414 | | - errors && setSubmissionErrors(node, errors); |
415 | | - return !errors || (isArray(errors) && errors.length === 0); |
416 | | - } else if (onInvalid) { |
417 | | - onInvalid(form); |
418 | | - } |
419 | | - return false; |
420 | | - } finally { |
421 | | - node.submitState.selfSubmitting.set(false); |
| 447 | + // Run the action (or alternatively the `onInvalid` callback) |
| 448 | + try { |
| 449 | + if (shouldRunAction) { |
| 450 | + node.submitState.selfSubmitting.set(true); |
| 451 | + const errors = await untracked(() => action?.(form)); |
| 452 | + errors && setSubmissionErrors(node, errors); |
| 453 | + return !errors || (isArray(errors) && errors.length === 0); |
| 454 | + } else { |
| 455 | + untracked(() => onInvalid?.(form)); |
422 | 456 | } |
423 | | - }); |
| 457 | + return false; |
| 458 | + } finally { |
| 459 | + node.submitState.selfSubmitting.set(false); |
| 460 | + } |
424 | 461 | } |
425 | 462 |
|
426 | 463 | /** |
|
0 commit comments