From 037d2ffb16074e87368bd543723c8dbe6edb714b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 6 Sep 2023 15:29:35 +0100 Subject: [PATCH 01/30] Support data-enhance to change the enhancement of links and forms --- .../src/Services/NavigationEnhancement.ts | 26 +++++++++++++++++++ src/Components/Web/src/Forms/EditForm.cs | 22 +++++++++++++--- .../Web/src/PublicAPI.Unshipped.txt | 2 ++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts index f7f734d46b2e..ad655bd4cce1 100644 --- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts +++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts @@ -73,6 +73,10 @@ function onDocumentClick(event: MouseEvent) { return; } + if (event.target instanceof HTMLAnchorElement && !enhancedNavigationIsEnabledForLink(event.target)) { + return; + } + handleClickForNavigationInterception(event, absoluteInternalHref => { history.pushState(null, /* ignored title */ '', absoluteInternalHref); performEnhancedPageLoad(absoluteInternalHref); @@ -99,6 +103,10 @@ function onDocumentSubmit(event: SubmitEvent) { // to make sure this handler only ever runs after interactive handlers. const formElem = event.target; if (formElem instanceof HTMLFormElement) { + if (!enhancedNavigationIsEnabledForForm(formElem)) { + return; + } + event.preventDefault(); const url = new URL(formElem.action); @@ -278,3 +286,21 @@ function splitStream(frameBoundaryMarker: string) { } }); } + +function enhancedNavigationIsEnabledForLink(element: HTMLAnchorElement): boolean { + // For links, they default to being enhanced, but you can override at any ancestor level (both positively and negatively) + const closestOverride = element.closest('[data-enhance]'); + if (closestOverride) { + const attributeValue = closestOverride.getAttribute('data-enhance')!; + return attributeValue === '' || attributeValue.toLowerCase() === 'true'; + } else { + return true; + } +} + +function enhancedNavigationIsEnabledForForm(form: HTMLFormElement): boolean { + // For forms, they default *not* to being enhanced, and must be enabled explicitly on the form element itself (not an ancestor). + const attributeValue = form.getAttribute('data-enhance'); + return typeof(attributeValue) === 'string' + && attributeValue === '' || attributeValue?.toLowerCase() === 'true'; +} diff --git a/src/Components/Web/src/Forms/EditForm.cs b/src/Components/Web/src/Forms/EditForm.cs index 990a2d39b9d2..f59ba91e074c 100644 --- a/src/Components/Web/src/Forms/EditForm.cs +++ b/src/Components/Web/src/Forms/EditForm.cs @@ -46,6 +46,15 @@ public EditContext? EditContext } } + /// + /// If enabled, form submission is performed without fully reloading the page. This is + /// equivalent to adding data-enhance to the form. + /// + /// This flag is only relevant in server-side rendering (SSR) scenarios. For interactive + /// rendering, the flag has no effect since there is no full-page reload on submit anyway. + /// + [Parameter] public bool Enhance { get; set; } + /// /// Specifies the top-level model object for the form. An edit context will /// be constructed for this model. If using this parameter, do not also supply @@ -135,8 +144,13 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddAttribute(2, "method", "post"); } - builder.AddMultipleAttributes(3, AdditionalAttributes); - builder.AddAttribute(4, "onsubmit", _handleSubmitDelegate); + if (Enhance) + { + builder.AddAttribute(3, "data-enhance", ""); + } + + builder.AddMultipleAttributes(4, AdditionalAttributes); + builder.AddAttribute(5, "onsubmit", _handleSubmitDelegate); // In SSR cases, we register onsubmit as a named event and emit other child elements // to include the handler and antiforgery token in the post data @@ -147,10 +161,10 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) builder.AddNamedEvent("onsubmit", FormName); } - RenderSSRFormHandlingChildren(builder, 5); + RenderSSRFormHandlingChildren(builder, 6); } - builder.OpenComponent>(6); + builder.OpenComponent>(7); builder.AddComponentParameter(7, "IsFixed", true); builder.AddComponentParameter(8, "Value", _editContext); builder.AddComponentParameter(9, "ChildContent", ChildContent?.Invoke(_editContext)); diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 1a6b400c1f1f..b23d101f56fb 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -9,6 +9,8 @@ Microsoft.AspNetCore.Components.Forms.AntiforgeryStateProvider Microsoft.AspNetCore.Components.Forms.AntiforgeryStateProvider.AntiforgeryStateProvider() -> void Microsoft.AspNetCore.Components.Forms.AntiforgeryToken Microsoft.AspNetCore.Components.Forms.AntiforgeryToken.AntiforgeryToken() -> void +Microsoft.AspNetCore.Components.Forms.EditForm.Enhance.get -> bool +Microsoft.AspNetCore.Components.Forms.EditForm.Enhance.set -> void Microsoft.AspNetCore.Components.Forms.Editor Microsoft.AspNetCore.Components.Forms.Editor.Editor() -> void Microsoft.AspNetCore.Components.Forms.Editor.NameFor(System.Linq.Expressions.LambdaExpression! expression) -> string! From 7b112d0266c1120219ca8c4b0fa74daa41e03567 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 6 Sep 2023 15:31:42 +0100 Subject: [PATCH 02/30] Update E2E tests --- .../Components/ComponentWithFormBoundParameter.razor | 2 +- .../RazorComponents/Components/ComponentWithFormInside.razor | 2 +- .../RazorComponents/Pages/Forms/AmbiguousForms.razor | 4 ++-- .../RazorComponents/Pages/Forms/ComponentThatThrows.razor | 2 +- .../RazorComponents/Pages/Forms/DefaultForm.razor | 2 +- .../Pages/Forms/DefaultFormBoundCollectionParameter.razor | 4 ++-- .../Pages/Forms/DefaultFormBoundComplexTypeParameter.razor | 2 +- ...faultFormBoundComplexTypeParameterMultipleComponents.razor | 2 +- .../Pages/Forms/DefaultFormBoundDictionaryParameter.razor | 2 +- .../Forms/DefaultFormBoundDictionaryParameterErrors.razor | 2 +- .../Forms/DefaultFormBoundMultiplePrimitiveParameters.razor | 2 +- ...aultFormBoundMultiplePrimitiveParametersChangedNames.razor | 2 +- .../Pages/Forms/DefaultFormBoundParameter.razor | 2 +- .../Pages/Forms/DefaultFormBoundPrimitiveParameter.razor | 4 ++-- .../Pages/Forms/DefaultFormMaxCollectionLimit.razor | 2 +- .../Pages/Forms/DefaultFormMaxRecursionDepth.razor | 2 +- .../Pages/Forms/DefaultFormWithBodyOnInitialized.razor | 2 +- .../RazorComponents/Pages/Forms/DisappearingForm.razor | 2 +- .../Pages/Forms/FormAntiforgeryAfterResponseStarted.razor | 2 +- .../RazorComponents/Pages/Forms/FormDisableAntiforgery.razor | 2 +- .../RazorComponents/Pages/Forms/FormNoAntiforgery.razor | 2 +- .../RazorComponents/Pages/Forms/FormNoHandler.razor | 2 +- .../RazorComponents/Pages/Forms/FormThatBindsGuid.razor | 2 +- .../RazorComponents/Pages/Forms/FormThatBindsInteger.razor | 2 +- .../RazorComponents/Pages/Forms/ModifyHttpContextForm.razor | 2 +- .../RazorComponents/Pages/Forms/MutateAndReRenderChild.razor | 2 +- .../RazorComponents/Pages/Forms/NamedForm.razor | 2 +- .../RazorComponents/Pages/Forms/NamedFormBoundParameter.razor | 2 +- .../Pages/Forms/NamedFormBoundPrimitiveParameter.razor | 2 +- ...NamedFormBoundPrimitiveParameterValidatorIntegration.razor | 2 +- .../Pages/Forms/NamedFormContextNoFormContextLayout.razor | 2 +- .../RazorComponents/Pages/Forms/NestedNamedForm.razor | 2 +- .../Pages/Forms/NonStreamingRenderingForm.razor | 2 +- .../RazorComponents/Pages/Forms/PlainForm.razor | 2 +- .../RazorComponents/Pages/Forms/PostRedirectGet.razor | 4 ++-- .../Pages/Forms/PostRedirectGetStreaming.razor | 4 ++-- .../RazorComponents/Pages/Forms/StreamingRenderingForm.razor | 2 +- 37 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor index 7e23ff81dad0..7034f51ab3e7 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormBoundParameter.razor @@ -1,6 +1,6 @@ @using Microsoft.AspNetCore.Components.Forms -
+ diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormInside.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormInside.razor index 5918a065a1e0..5946d7dad5f1 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormInside.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Components/ComponentWithFormInside.razor @@ -1,5 +1,5 @@ @using Microsoft.AspNetCore.Components.Forms - + @if(BindingContext == null) { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/AmbiguousForms.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/AmbiguousForms.razor index cd896d11cefb..54719ad65f0b 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/AmbiguousForms.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/AmbiguousForms.razor @@ -3,10 +3,10 @@

Ambiguous forms

- + - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ComponentThatThrows.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ComponentThatThrows.razor index c840fe50c42a..0da499b44e26 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ComponentThatThrows.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ComponentThatThrows.razor @@ -3,7 +3,7 @@

Throw during initial render

- + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultForm.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultForm.razor index 12b038664c5a..e33200a8a5a5 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultForm.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultForm.razor @@ -3,7 +3,7 @@

Default form

- + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundCollectionParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundCollectionParameter.razor index 0105955bce5b..72e22c284207 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundCollectionParameter.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundCollectionParameter.razor @@ -3,7 +3,7 @@

Default form with dictionary bound parameter

- + @for(var i = 0; i< Model.Count;i++) { @@ -30,7 +30,7 @@ - + } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundComplexTypeParameter.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundComplexTypeParameter.razor index 47ca1c45a58a..156d41465e38 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundComplexTypeParameter.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundComplexTypeParameter.razor @@ -3,7 +3,7 @@

Default form with bound complex type parameter

- +