Skip to content

Commit 0eeb1b5

Browse files
committed
fix(forms): allow FormRoot to be used without submission options (#67727)
The `[formRoot]` directive will no longer call `submit()` if the bound form doesn't define its own submission options. This allows the directive to be used solely for the default behavior it provides: setting `novalidate` on the `<form>` and calling `preventDefault()` on the `submit` event. Fix #67367 PR Close #67727
1 parent f4a5b42 commit 0eeb1b5

File tree

2 files changed

+39
-5
lines changed

2 files changed

+39
-5
lines changed

packages/forms/signals/src/directive/form_root.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {Directive, input} from '@angular/core';
9+
import {Directive, input, untracked} from '@angular/core';
1010

1111
import {submit} from '../api/structure';
12-
import {FieldTree} from '../api/types';
12+
import {FieldState, FieldTree} from '../api/types';
13+
import {FieldNode} from '../field/node';
1314

1415
/**
1516
* A directive that binds a `FieldTree` to a `<form>` element.
1617
*
1718
* It automatically:
1819
* 1. Sets `novalidate` on the form element to disable browser validation.
1920
* 2. Listens for the `submit` event, prevents the default behavior, and calls `submit()` on the
20-
* `FieldTree`.
21+
* `FieldTree` if it defines its own submission options.
2122
*
2223
* @usageNotes
2324
*
@@ -42,6 +43,14 @@ export class FormRoot<T> {
4243

4344
protected onSubmit(event: Event): void {
4445
event.preventDefault();
45-
submit(this.fieldTree());
46+
47+
untracked(() => {
48+
const fieldTree = this.fieldTree();
49+
const node = fieldTree() as FieldState<unknown> as FieldNode;
50+
51+
if (node.structure.fieldManager.submitOptions) {
52+
submit(fieldTree);
53+
}
54+
});
4655
}
4756
}

packages/forms/signals/test/node/form_root.spec.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('FormRoot', () => {
4444
expect(formElement.hasAttribute('novalidate')).toBeTrue();
4545
});
4646

47-
it('should call submit on the field tree when form is submitted', async () => {
47+
it('should call submit if the field tree defines submit options', async () => {
4848
const fixture = act(() => TestBed.createComponent(TestCmp));
4949
const component = fixture.componentInstance;
5050
const formElement = fixture.nativeElement.querySelector('form') as HTMLFormElement;
@@ -53,9 +53,34 @@ describe('FormRoot', () => {
5353
act(() => formElement.dispatchEvent(event));
5454

5555
expect(event.defaultPrevented).toBe(true);
56+
expect(component.f().touched()).toBeTrue();
5657
expect(component.submitted).toBeTrue();
5758
});
5859

60+
it('should not call submit if the field tree does not define submit options', async () => {
61+
@Component({
62+
template: `
63+
<form [formRoot]="f">
64+
<button type="submit">Submit</button>
65+
</form>
66+
`,
67+
imports: [FormRoot],
68+
})
69+
class TestCmpNoSubmit {
70+
readonly f = form(signal({}));
71+
}
72+
73+
const fixture = act(() => TestBed.createComponent(TestCmpNoSubmit));
74+
const component = fixture.componentInstance;
75+
const formElement = fixture.nativeElement.querySelector('form') as HTMLFormElement;
76+
77+
const event = new Event('submit', {cancelable: true});
78+
act(() => formElement.dispatchEvent(event));
79+
80+
expect(event.defaultPrevented).toBe(true);
81+
expect(component.f().touched()).withContext('submit would mark this as touched').toBeFalse();
82+
});
83+
5984
it('works when FormsModule is imported', () => {
6085
@Component({
6186
template: `

0 commit comments

Comments
 (0)