mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
@@ -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 `<form>` 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<T> {
|
||||
|
||||
protected onSubmit(event: Event): void {
|
||||
event.preventDefault();
|
||||
submit(this.fieldTree());
|
||||
|
||||
untracked(() => {
|
||||
const fieldTree = this.fieldTree();
|
||||
const node = fieldTree() as FieldState<unknown> as FieldNode;
|
||||
|
||||
if (node.structure.fieldManager.submitOptions) {
|
||||
submit(fieldTree);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: `
|
||||
<form [formRoot]="f">
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
`,
|
||||
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: `
|
||||
|
||||
Reference in New Issue
Block a user