Skip to content

Commit 20d64a7

Browse files
[release/8.0] Fix Blazor enhanced form bugs (#52765)
* fix "Blazor enhanced form handling doesn't honor button formmethod attribute" (#51942) * [Blazor] do not enhance form when method = "dialog" or target != "_self" (#52133) * 1. form is not enhanced when method = "dialog" 2. form is not enhanced when target != "_self" * Update src/Components/Web.JS/src/Services/NavigationEnhancement.ts Co-authored-by: Mackinnon Buck <[email protected]> * Update src/Components/Web.JS/src/Services/NavigationEnhancement.ts Co-authored-by: Mackinnon Buck <[email protected]> * fix warning message in the tests * 1. use console.warn instead 2. do not cast event.submitter to HTMLButtonElement * small fix --------- Co-authored-by: Mackinnon Buck <[email protected]> * Fix enhanced form handling use defalt enctype, honor enctype and formenctype (#52359) * 1. fix enhanced form handling use default enctype when not specified 2. fix enhanced form handling honor form enctype attribute 3. fix enhanced form handling honor submit button formenctype attribute * use SupplyParameterFromQuery to provide enctype and formenctype in the tests * update blazor.web.js --------- Co-authored-by: Mackinnon Buck <[email protected]>
1 parent 57585f9 commit 20d64a7

File tree

6 files changed

+198
-17
lines changed

6 files changed

+198
-17
lines changed

src/Components/Web.JS/dist/Release/blazor.web.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Services/NavigationEnhancement.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Note that we don't reference NavigationManager.ts from NavigationEnhancement.ts
3131
different bundles that only contain minimal content.
3232
*/
3333

34+
const acceptHeader = 'text/html; blazor-enhanced-nav=on';
35+
3436
let currentEnhancedNavigationAbortController: AbortController | null;
3537
let navigationEnhancementCallbacks: NavigationEnhancementCallbacks;
3638
let performingEnhancedPageLoad: boolean;
@@ -116,33 +118,52 @@ function onDocumentSubmit(event: SubmitEvent) {
116118
return;
117119
}
118120

121+
const method = event.submitter?.getAttribute('formmethod') || formElem.method;
122+
if (method === 'dialog') {
123+
console.warn('A form cannot be enhanced when its method is "dialog".');
124+
return;
125+
}
126+
127+
const target = event.submitter?.getAttribute('formtarget') || formElem.target;
128+
if (target !== '' && target !== '_self') {
129+
console.warn('A form cannot be enhanced when its target is different from the default value "_self".');
130+
return;
131+
}
132+
119133
event.preventDefault();
120134

121-
let url = new URL(formElem.action);
122-
const fetchOptions: RequestInit = { method: formElem.method };
135+
const url = new URL(event.submitter?.getAttribute('formaction') || formElem.action, document.baseURI);
136+
const fetchOptions: RequestInit = { method: method};
123137
const formData = new FormData(formElem);
124-
125-
const submitter = event.submitter as HTMLButtonElement;
126-
if (submitter) {
127-
if (submitter.name) {
128-
// Replicate the normal behavior of appending the submitter name/value to the form data
129-
formData.append(submitter.name, submitter.value);
130-
}
131-
if (submitter.getAttribute("formaction") !== null) {
132-
// Replicate the normal behavior of overriding action attribute of form element
133-
url = new URL(submitter.formAction);
134-
}
138+
139+
const submitterName = event.submitter?.getAttribute('name');
140+
const submitterValue = event.submitter!.getAttribute('value');
141+
if (submitterName && submitterValue) {
142+
formData.append(submitterName, submitterValue);
135143
}
136144

145+
const urlSearchParams = new URLSearchParams(formData as any).toString();
137146
if (fetchOptions.method === 'get') { // method is always returned as lowercase
138-
url.search = new URLSearchParams(formData as any).toString();
147+
url.search = urlSearchParams;
139148

140149
// For forms with method=get, we need to push a URL history entry equivalent to how it
141150
// would be pushed for a native <form method=get> submission. This is also equivalent to
142151
// how we push a URL history entry before starting enhanced page load on an <a> click.
143152
history.pushState(null, /* ignored title */ '', url.toString());
144153
} else {
145-
fetchOptions.body = formData;
154+
// Setting request body and content-type header depending on enctype
155+
const enctype = event.submitter?.getAttribute('formenctype') || formElem.enctype;
156+
if (enctype === 'multipart/form-data') {
157+
// Content-Type header will be set to 'multipart/form-data'
158+
fetchOptions.body = formData;
159+
} else {
160+
fetchOptions.body = urlSearchParams;
161+
fetchOptions.headers = {
162+
'content-type': enctype,
163+
// Setting Accept header here as well so it wouldn't be lost when coping headers
164+
'accept': acceptHeader
165+
};
166+
}
146167
}
147168

148169
performEnhancedPageLoad(url.toString(), /* interceptedLink */ false, fetchOptions);
@@ -168,7 +189,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
168189
headers: {
169190
// Because of no-cors, we can only send CORS-safelisted headers, so communicate the info about
170191
// enhanced nav as a MIME type parameter
171-
'accept': 'text/html; blazor-enhanced-nav=on',
192+
'accept': acceptHeader,
172193
},
173194
}, fetchOptions));
174195
let isNonRedirectedPostToADifferentUrlMessage: string | null = null;

src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,101 @@ public void SubmitButtonFormactionAttributeOverridesEnhancedFormAction()
13681368
Browser.Equal("Formaction url", () => Browser.Exists(By.TagName("html")).Text);
13691369
}
13701370

1371+
[Fact]
1372+
public void SubmitButtonFormmethodAttributeOverridesEnhancedFormMethod()
1373+
{
1374+
GoTo("forms/form-with-method-and-submit-button-with-formmethod/get/post");
1375+
Browser.DoesNotExist(By.Id("submitted"));
1376+
1377+
Browser.Exists(By.Id("submit-button")).Click();
1378+
1379+
Browser.Equal("Form submitted!", () => Browser.Exists(By.Id("submitted")).Text);
1380+
}
1381+
1382+
[Fact]
1383+
public void FormNotEnhancedWhenMethodEqualsDialog()
1384+
{
1385+
GoTo("forms/form-with-method-and-submit-button-with-formmethod/dialog");
1386+
Browser.Exists(By.Id("submit-button")).Click();
1387+
1388+
// We are not checking staleness of the form element because the default behavior is to stay on the page.
1389+
// Check the warning
1390+
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
1391+
Assert.True(logs.Count > 0);
1392+
Assert.Contains(logs, log => log.Message.Contains("A form cannot be enhanced when its method is \\\"dialog\\\"."));
1393+
}
1394+
1395+
[Fact]
1396+
public void FormNotEnhancedWhenFormmethodEqualsDialog()
1397+
{
1398+
GoTo("forms/form-with-method-and-submit-button-with-formmethod/get/dialog");
1399+
1400+
Browser.Exists(By.Id("submit-button")).Click();
1401+
1402+
// We are not checking staleness of the form element because the default behavior is to stay on the page.
1403+
// Check the warning
1404+
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
1405+
Assert.True(logs.Count > 0);
1406+
Assert.Contains(logs, log => log.Message.Contains("A form cannot be enhanced when its method is \\\"dialog\\\"."));
1407+
}
1408+
1409+
[Fact]
1410+
public void FormNotEnhancedWhenTargetIsNotEqualSelf()
1411+
{
1412+
GoTo("forms/form-with-target-and-submit-button-with-formtarget/_blank");
1413+
Browser.Exists(By.Id("submit-button")).Click();
1414+
1415+
// We are not checking staleness of form element because the default behavior is to open a new browser tab and the form remains on the original tab.
1416+
// Check the warning
1417+
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
1418+
Assert.True(logs.Count > 0);
1419+
Assert.Contains(logs, log => log.Message.Contains("A form cannot be enhanced when its target is different from the default value \\\"_self\\\"."));
1420+
}
1421+
1422+
[Fact]
1423+
public void FormNotEnhancedWhenFormtargetIsNotEqualSelf()
1424+
{
1425+
GoTo("forms/form-with-target-and-submit-button-with-formtarget/_self/_blank");
1426+
1427+
Browser.Exists(By.Id("submit-button")).Click();
1428+
1429+
// We are not checking staleness of form element because the default behavior is to open a new browser tab and the form remains on the original tab.
1430+
// Check the warning
1431+
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
1432+
Assert.True(logs.Count > 0);
1433+
Assert.Contains(logs, log => log.Message.Contains("A form cannot be enhanced when its target is different from the default value \\\"_self\\\"."));
1434+
}
1435+
1436+
[Fact]
1437+
public void FormEnctypeEqualsDefaultWhenNotSpecified()
1438+
{
1439+
GoTo("forms/form-with-enctype-and-submit-button-with-formenctype");
1440+
1441+
Browser.Exists(By.Id("submit-button")).Click();
1442+
1443+
Browser.Equal("application/x-www-form-urlencoded", () => Browser.Exists(By.Id("content-type")).Text);
1444+
}
1445+
1446+
[Fact]
1447+
public void FormEnctypeSetsContentTypeHeader()
1448+
{
1449+
GoTo("forms/form-with-enctype-and-submit-button-with-formenctype?enctype=multipart/form-data");
1450+
1451+
Browser.Exists(By.Id("submit-button")).Click();
1452+
1453+
Browser.Contains("multipart/form-data", () => Browser.Exists(By.Id("content-type")).Text);
1454+
}
1455+
1456+
[Fact]
1457+
public void SubmitButtonFormenctypeAttributeOverridesEnhancedFormEnctype()
1458+
{
1459+
GoTo("forms/form-with-enctype-and-submit-button-with-formenctype?enctype=text/plain&formenctype=application/x-www-form-urlencoded");
1460+
1461+
Browser.Exists(By.Id("submit-button")).Click();
1462+
1463+
Browser.Equal("application/x-www-form-urlencoded", () => Browser.Exists(By.Id("content-type")).Text);
1464+
}
1465+
13711466
// Can't just use GetAttribute or GetDomAttribute because they both auto-resolve it
13721467
// to an absolute URL. We want to be able to assert about the attribute's literal value.
13731468
private string ReadFormActionAttribute(IWebElement form)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@page "/forms/form-with-enctype-and-submit-button-with-formenctype"
2+
@using Microsoft.AspNetCore.Components.Forms
3+
@inject IHttpContextAccessor HttpContextAccessor
4+
5+
<h1>Form with enctype and Submit Button with formenctype</h1>
6+
7+
<form id="form" data-enhance method="post" enctype="@Enctype" @onsubmit="Submitted" @formname="someform">
8+
<AntiforgeryToken />
9+
<input id="submit-button" type="submit" formenctype="@Formenctype" />
10+
</form>
11+
12+
@if (_submitted)
13+
{
14+
<p>Http request ContentType: <span id="content-type">@_contentType</span></p>
15+
}
16+
17+
@code {
18+
bool _submitted = false;
19+
private string? _contentType;
20+
21+
[SupplyParameterFromQuery] public string? Enctype { get; set; }
22+
[SupplyParameterFromQuery] public string? Formenctype { get; set; }
23+
24+
public void Submitted()
25+
{
26+
_submitted = true;
27+
28+
var httpContext = HttpContextAccessor.HttpContext;
29+
_contentType = httpContext?.Request.ContentType;
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@page "/forms/form-with-method-and-submit-button-with-formmethod/{method}/{formmethod?}"
2+
@using Microsoft.AspNetCore.Components.Forms
3+
4+
<h1>Form with method and Submit Button with formaction</h1>
5+
6+
<form id="form" data-enhance method="@Method" @onsubmit="() => _submitted = true" @formname="someform">
7+
<AntiforgeryToken />
8+
<input id="submit-button" type="submit" formmethod="@Formmethod" />
9+
</form>
10+
11+
@if (_submitted)
12+
{
13+
<p id="submitted">Form submitted!</p>
14+
}
15+
16+
@code {
17+
bool _submitted = false;
18+
19+
[Parameter] public string Method { get; set; }
20+
[Parameter] public string? Formmethod { get; set; }
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@page "/forms/form-with-target-and-submit-button-with-formtarget/{target}/{formtarget?}"
2+
@using Microsoft.AspNetCore.Components.Forms
3+
4+
<h1>Form with target and Submit Button with formtarget</h1>
5+
6+
<form id="form" data-enhance target="@Target">
7+
<input id="submit-button" type="submit" formtarget="@Formtarget" />
8+
</form>
9+
10+
@code {
11+
[Parameter] public string Target { get; set; }
12+
[Parameter] public string? Formtarget { get; set; }
13+
}

0 commit comments

Comments
 (0)