diff --git a/packages/forms/signals/public_api.ts b/packages/forms/signals/public_api.ts index 6b5a578c02c5..706073975552 100644 --- a/packages/forms/signals/public_api.ts +++ b/packages/forms/signals/public_api.ts @@ -20,5 +20,5 @@ export * from './src/api/rules/validation/validation_errors'; export * from './src/api/structure'; export * from './src/api/transformed_value'; export * from './src/api/types'; -export * from './src/directive/form_field_directive'; -export * from './src/directive/ng_signal_form'; +export * from './src/directive/form_field'; +export * from './src/directive/form_root'; diff --git a/packages/forms/signals/src/api/control.ts b/packages/forms/signals/src/api/control.ts index 22168fddf9d4..4c30c8c42542 100644 --- a/packages/forms/signals/src/api/control.ts +++ b/packages/forms/signals/src/api/control.ts @@ -7,7 +7,7 @@ */ import {InputSignal, InputSignalWithTransform, ModelSignal, OutputRef} from '@angular/core'; -import type {FormFieldBindingOptions} from '../directive/form_field_directive'; +import type {FormFieldBindingOptions} from '../directive/form_field'; import type {ValidationError, WithOptionalFieldTree} from './rules/validation/validation_errors'; import type {DisabledReason} from './types'; diff --git a/packages/forms/signals/src/api/rules/validation/validation_errors.ts b/packages/forms/signals/src/api/rules/validation/validation_errors.ts index 0c1589239e14..834dbe51cb7d 100644 --- a/packages/forms/signals/src/api/rules/validation/validation_errors.ts +++ b/packages/forms/signals/src/api/rules/validation/validation_errors.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import type {FormField} from '../../../directive/form_field_directive'; +import type {FormField} from '../../../directive/form_field'; import type {ReadonlyFieldTree} from '../../types'; import type {StandardSchemaValidationError} from './standard_schema'; diff --git a/packages/forms/signals/src/api/types.ts b/packages/forms/signals/src/api/types.ts index bc2b6b6d2317..b624f350cb04 100644 --- a/packages/forms/signals/src/api/types.ts +++ b/packages/forms/signals/src/api/types.ts @@ -8,7 +8,7 @@ import {Injector, Signal, WritableSignal} from '@angular/core'; import {AbstractControl} from '@angular/forms'; -import type {FormField} from '../directive/form_field_directive'; +import type {FormField} from '../directive/form_field'; import type {MetadataKey, ValidationError} from './rules'; /** diff --git a/packages/forms/signals/src/directive/control_custom.ts b/packages/forms/signals/src/directive/control_custom.ts index 28a1b386c9ac..d4767a61e7db 100644 --- a/packages/forms/signals/src/directive/control_custom.ts +++ b/packages/forms/signals/src/directive/control_custom.ts @@ -7,7 +7,7 @@ */ import type {ɵControlDirectiveHost as ControlDirectiveHost} from '@angular/core'; -import type {FormField} from './form_field_directive'; +import type {FormField} from './form_field'; import { bindingUpdated, CONTROL_BINDING_NAMES, diff --git a/packages/forms/signals/src/directive/control_cva.ts b/packages/forms/signals/src/directive/control_cva.ts index 67f46514d739..1103ff2fd2de 100644 --- a/packages/forms/signals/src/directive/control_cva.ts +++ b/packages/forms/signals/src/directive/control_cva.ts @@ -15,7 +15,7 @@ import { readFieldStateBindingValue, } from './bindings'; import {setNativeDomProperty} from './native'; -import type {FormField} from './form_field_directive'; +import type {FormField} from './form_field'; export function cvaControlCreate( host: ControlDirectiveHost, diff --git a/packages/forms/signals/src/directive/control_native.ts b/packages/forms/signals/src/directive/control_native.ts index 07be95d7fb28..d90537886b38 100644 --- a/packages/forms/signals/src/directive/control_native.ts +++ b/packages/forms/signals/src/directive/control_native.ts @@ -19,7 +19,7 @@ import { readFieldStateBindingValue, type ControlBindingKey, } from './bindings'; -import type {FormField} from './form_field_directive'; +import type {FormField} from './form_field'; import {InputValidityMonitor} from './input_validity_monitor'; import { getNativeControlValue, diff --git a/packages/forms/signals/src/directive/form_field_directive.ts b/packages/forms/signals/src/directive/form_field.ts similarity index 100% rename from packages/forms/signals/src/directive/form_field_directive.ts rename to packages/forms/signals/src/directive/form_field.ts diff --git a/packages/forms/signals/src/directive/ng_signal_form.ts b/packages/forms/signals/src/directive/form_root.ts similarity index 68% rename from packages/forms/signals/src/directive/ng_signal_form.ts rename to packages/forms/signals/src/directive/form_root.ts index 7ba799d49a5b..9bb073f43d4a 100644 --- a/packages/forms/signals/src/directive/ng_signal_form.ts +++ b/packages/forms/signals/src/directive/form_root.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.dev/license */ -import {Directive, input} from '@angular/core'; +import {Directive, input, untracked} from '@angular/core'; import {submit} from '../api/structure'; -import {FieldTree} from '../api/types'; +import {FieldState, FieldTree} from '../api/types'; +import {FieldNode} from '../field/node'; /** * A directive that binds a `FieldTree` to a `
` element. @@ -17,7 +18,7 @@ import {FieldTree} from '../api/types'; * It automatically: * 1. Sets `novalidate` on the form element to disable browser validation. * 2. Listens for the `submit` event, prevents the default behavior, and calls `submit()` on the - * `FieldTree`. + * `FieldTree` if it defines its own submission options. * * @usageNotes * @@ -42,6 +43,14 @@ export class FormRoot { protected onSubmit(event: Event): void { event.preventDefault(); - submit(this.fieldTree()); + + untracked(() => { + const fieldTree = this.fieldTree(); + const node = fieldTree() as FieldState as FieldNode; + + if (node.structure.fieldManager.submitOptions) { + submit(fieldTree); + } + }); } } diff --git a/packages/forms/signals/src/field/node.ts b/packages/forms/signals/src/field/node.ts index 55b5a85a69bc..7df69c2b269e 100644 --- a/packages/forms/signals/src/field/node.ts +++ b/packages/forms/signals/src/field/node.ts @@ -33,7 +33,7 @@ import type { FieldTree, MarkAsTouchedOptions, } from '../api/types'; -import type {FormField} from '../directive/form_field_directive'; +import type {FormField} from '../directive/form_field'; import {DYNAMIC} from '../schema/logic'; import {LogicNode} from '../schema/logic_node'; import {FieldPathNode} from '../schema/path_node'; diff --git a/packages/forms/signals/src/field/state.ts b/packages/forms/signals/src/field/state.ts index 347f4aa2d212..2e979ca9a755 100644 --- a/packages/forms/signals/src/field/state.ts +++ b/packages/forms/signals/src/field/state.ts @@ -7,7 +7,7 @@ */ import {computed, signal, Signal} from '@angular/core'; -import type {FormField} from '../directive/form_field_directive'; +import type {FormField} from '../directive/form_field'; import type {Debouncer, DisabledReason} from '../api/types'; import {DEBOUNCER} from './debounce'; import type {FieldNode} from './node'; diff --git a/packages/forms/signals/test/node/ng_signal_form.spec.ts b/packages/forms/signals/test/node/form_root.spec.ts similarity index 78% rename from packages/forms/signals/test/node/ng_signal_form.spec.ts rename to packages/forms/signals/test/node/form_root.spec.ts index 8944a8e64339..19bd60b45f80 100644 --- a/packages/forms/signals/test/node/ng_signal_form.spec.ts +++ b/packages/forms/signals/test/node/form_root.spec.ts @@ -44,7 +44,7 @@ describe('FormRoot', () => { expect(formElement.hasAttribute('novalidate')).toBeTrue(); }); - it('should call submit on the field tree when form is submitted', async () => { + it('should call submit if the field tree defines submit options', async () => { const fixture = act(() => TestBed.createComponent(TestCmp)); const component = fixture.componentInstance; const formElement = fixture.nativeElement.querySelector('form') as HTMLFormElement; @@ -53,9 +53,34 @@ describe('FormRoot', () => { act(() => formElement.dispatchEvent(event)); expect(event.defaultPrevented).toBe(true); + expect(component.f().touched()).toBeTrue(); expect(component.submitted).toBeTrue(); }); + it('should not call submit if the field tree does not define submit options', async () => { + @Component({ + template: ` + + + + `, + imports: [FormRoot], + }) + class TestCmpNoSubmit { + readonly f = form(signal({})); + } + + const fixture = act(() => TestBed.createComponent(TestCmpNoSubmit)); + const component = fixture.componentInstance; + const formElement = fixture.nativeElement.querySelector('form') as HTMLFormElement; + + const event = new Event('submit', {cancelable: true}); + act(() => formElement.dispatchEvent(event)); + + expect(event.defaultPrevented).toBe(true); + expect(component.f().touched()).withContext('submit would mark this as touched').toBeFalse(); + }); + it('works when FormsModule is imported', () => { @Component({ template: ` diff --git a/packages/forms/signals/test/web/form_field_directive.spec.ts b/packages/forms/signals/test/web/form_field.spec.ts similarity index 100% rename from packages/forms/signals/test/web/form_field_directive.spec.ts rename to packages/forms/signals/test/web/form_field.spec.ts diff --git a/packages/forms/signals/test/web/signal_form_control_web.spec.ts b/packages/forms/signals/test/web/signal_form_control_web.spec.ts index 66684ec1b33f..da4ca61b3381 100644 --- a/packages/forms/signals/test/web/signal_form_control_web.spec.ts +++ b/packages/forms/signals/test/web/signal_form_control_web.spec.ts @@ -12,7 +12,7 @@ import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; import {disabled} from '@angular/forms/signals'; import {SignalFormControl} from '../../compat'; -import {FormField} from '../../src/directive/form_field_directive'; +import {FormField} from '../../src/directive/form_field'; describe('SignalFormControl (web)', () => { beforeEach(() => {