diff --git a/packages/forms/signals/src/directive/form_root.ts b/packages/forms/signals/src/directive/form_root.ts index 7ba799d49a5..9bb073f43d4 100644 --- a/packages/forms/signals/src/directive/form_root.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/test/node/form_root.spec.ts b/packages/forms/signals/test/node/form_root.spec.ts index 8944a8e6433..19bd60b45f8 100644 --- a/packages/forms/signals/test/node/form_root.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: `