Skip to content

Commit 4a13210

Browse files
crisbetodylhunn
authored andcommitted
fix(forms): don't prevent default behavior for forms with method="dialog" (#47308)
The forms `submit` event handlers have a `return false` to prevent form submissions from reloading the page, however this also prevents the browser behavior for forms with `method="dialog"`. These changes add an exception since the `method="dialog"` doesn't refresh the page. Fixes #47150. PR Close #47308
1 parent 678c7f1 commit 4a13210

File tree

5 files changed

+70
-7
lines changed

5 files changed

+70
-7
lines changed

packages/forms/src/directives/ng_form.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ export class NgForm extends ControlContainer implements Form, AfterViewInit {
298298
(this as {submitted: boolean}).submitted = true;
299299
syncPendingControls(this.form, this._directives);
300300
this.ngSubmit.emit($event);
301-
return false;
301+
// Forms with `method="dialog"` have some special behavior
302+
// that won't reload the page and that shouldn't be prevented.
303+
return ($event?.target as HTMLFormElement | null)?.method === 'dialog';
302304
}
303305

304306
/**

packages/forms/src/directives/reactive_directives/form_group_directive.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,8 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan
9494
@Output() ngSubmit = new EventEmitter();
9595

9696
constructor(
97-
@Optional() @Self() @Inject(NG_VALIDATORS) private validators: (Validator|ValidatorFn)[],
98-
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) private asyncValidators:
97+
@Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator|ValidatorFn)[],
98+
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
9999
(AsyncValidator|AsyncValidatorFn)[]) {
100100
super();
101101
this._setValidators(validators);
@@ -271,7 +271,10 @@ export class FormGroupDirective extends ControlContainer implements Form, OnChan
271271
(this as {submitted: boolean}).submitted = true;
272272
syncPendingControls(this.form, this.directives);
273273
this.ngSubmit.emit($event);
274-
return false;
274+
// Forms with `method="dialog"` have some special behavior that won't reload the page and that
275+
// shouldn't be prevented. Note that we need to null check the `event` and the `target`, because
276+
// some internal apps call this method directly with the wrong arguments.
277+
return ($event?.target as HTMLFormElement | null)?.method === 'dialog';
275278
}
276279

277280
/**

packages/forms/test/reactive_integration_spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {ɵgetDOM as getDOM} from '@angular/common';
10-
import {Component, Directive, forwardRef, Input, NgModule, OnDestroy, Type} from '@angular/core';
10+
import {Component, Directive, ElementRef, forwardRef, Input, NgModule, OnDestroy, Type, ViewChild} from '@angular/core';
1111
import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
1212
import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MODE, ControlValueAccessor, DefaultValueAccessor, FormArray, FormBuilder, FormControl, FormControlDirective, FormControlName, FormGroup, FormGroupDirective, FormsModule, MaxValidator, MinLengthValidator, MinValidator, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, Validator, Validators} from '@angular/forms';
1313
import {By} from '@angular/platform-browser/src/dom/debug/by';
@@ -2114,6 +2114,20 @@ const ValueAccessorB = createControlValueAccessor('[cva-b]');
21142114
expect(passwordControl.value).toEqual('Carson', 'Expected value to change on submit.');
21152115
expect(passwordControl.valid).toBe(true, 'Expected validation to run on submit.');
21162116
});
2117+
2118+
it('should not prevent the default action on forms with method="dialog"', fakeAsync(() => {
2119+
if (typeof HTMLDialogElement === 'undefined') {
2120+
return;
2121+
}
2122+
2123+
const fixture = initTest(NativeDialogForm);
2124+
fixture.detectChanges();
2125+
tick();
2126+
const event = dispatchEvent(fixture.componentInstance.form.nativeElement, 'submit');
2127+
fixture.detectChanges();
2128+
2129+
expect(event.defaultPrevented).toBe(false);
2130+
}));
21172131
});
21182132
});
21192133

@@ -5437,3 +5451,17 @@ class MinMaxFormControlComp {
54375451
min: number|string = 1;
54385452
max: number|string = 10;
54395453
}
5454+
5455+
@Component({
5456+
template: `
5457+
<dialog open>
5458+
<form #form method="dialog" [formGroup]="formGroup">
5459+
<button>Submit</button>
5460+
</form>
5461+
</dialog>
5462+
`
5463+
})
5464+
class NativeDialogForm {
5465+
@ViewChild('form') form!: ElementRef<HTMLFormElement>;
5466+
formGroup = new FormGroup({});
5467+
}

packages/forms/test/template_integration_spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {CommonModule, ɵgetDOM as getDOM} from '@angular/common';
10-
import {Component, Directive, forwardRef, Input, Type, ViewChild} from '@angular/core';
10+
import {Component, Directive, ElementRef, forwardRef, Input, Type, ViewChild} from '@angular/core';
1111
import {ComponentFixture, fakeAsync, TestBed, tick, waitForAsync} from '@angular/core/testing';
1212
import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, ControlValueAccessor, FormControl, FormsModule, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, NG_ASYNC_VALIDATORS, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, NgModel, Validator} from '@angular/forms';
1313
import {By} from '@angular/platform-browser/src/dom/debug/by';
@@ -1062,6 +1062,21 @@ import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integrat
10621062
['fired', 'fired'],
10631063
'Expected ngModelChanges to fire again on submit if value changed.');
10641064
}));
1065+
1066+
1067+
it('should not prevent the default action on forms with method="dialog"', fakeAsync(() => {
1068+
if (typeof HTMLDialogElement === 'undefined') {
1069+
return;
1070+
}
1071+
1072+
const fixture = initTest(NativeDialogForm);
1073+
fixture.detectChanges();
1074+
tick();
1075+
const event = dispatchEvent(fixture.componentInstance.form.nativeElement, 'submit');
1076+
fixture.detectChanges();
1077+
1078+
expect(event.defaultPrevented).toBe(false);
1079+
}));
10651080
});
10661081

10671082
describe('ngFormOptions', () => {
@@ -2932,3 +2947,17 @@ class NgModelNoMinMaxValidator {
29322947
max!: number;
29332948
@ViewChild('myDir') myDir: any;
29342949
}
2950+
2951+
@Component({
2952+
selector: 'ng-model-nested',
2953+
template: `
2954+
<dialog open>
2955+
<form #form method="dialog">
2956+
<button>Submit</button>
2957+
</form>
2958+
</dialog>
2959+
`
2960+
})
2961+
class NativeDialogForm {
2962+
@ViewChild('form') form!: ElementRef<HTMLFormElement>;
2963+
}

packages/platform-browser/testing/src/browser_util.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ export class BrowserDetection {
9797

9898
export const browserDetection: BrowserDetection = BrowserDetection.setup();
9999

100-
export function dispatchEvent(element: any, eventType: any): void {
100+
export function dispatchEvent(element: any, eventType: any): Event {
101101
const evt: Event = getDOM().getDefaultDocument().createEvent('Event');
102102
evt.initEvent(eventType, true, true);
103103
getDOM().dispatchEvent(element, evt);
104+
return evt;
104105
}
105106

106107
export function createMouseEvent(eventType: string): MouseEvent {

0 commit comments

Comments
 (0)