Skip to content

Commit 96eb306

Browse files
Automatically emit hidden field for form handler name during SSR
1 parent 41b24f5 commit 96eb306

File tree

3 files changed

+160
-13
lines changed

3 files changed

+160
-13
lines changed

src/Components/Components/src/CascadingParameterState.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Reflection;
88
using Microsoft.AspNetCore.Components.Reflection;
99
using Microsoft.AspNetCore.Components.Rendering;
10+
using Microsoft.AspNetCore.Components.RenderTree;
1011
using static Microsoft.AspNetCore.Internal.LinkerFlags;
1112

1213
namespace Microsoft.AspNetCore.Components;
@@ -43,7 +44,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
4344
for (var infoIndex = 0; infoIndex < numInfos; infoIndex++)
4445
{
4546
ref var info = ref infos[infoIndex];
46-
var supplier = GetMatchingCascadingValueSupplier(info, componentState);
47+
var supplier = GetMatchingCascadingValueSupplier(info, componentState.Renderer, componentState.LogicalParentComponentState);
4748
if (supplier != null)
4849
{
4950
// Although not all parameters might be matched, we know the maximum number
@@ -55,10 +56,10 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
5556
return resultStates ?? (IReadOnlyList<CascadingParameterState>)Array.Empty<CascadingParameterState>();
5657
}
5758

58-
private static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, ComponentState componentState)
59+
internal static ICascadingValueSupplier? GetMatchingCascadingValueSupplier(in CascadingParameterInfo info, Renderer renderer, ComponentState? componentState)
5960
{
6061
// First scan up through the component hierarchy
61-
var candidate = componentState.LogicalParentComponentState;
62+
var candidate = componentState;
6263
while (candidate is not null)
6364
{
6465
if (candidate.Component is ICascadingValueSupplier valueSupplier && valueSupplier.CanSupplyValue(info))
@@ -70,7 +71,7 @@ public static IReadOnlyList<CascadingParameterState> FindCascadingParameters(Com
7071
}
7172

7273
// We got to the root and found no match, so now look at the providers registered in DI
73-
foreach (var valueSupplier in componentState.Renderer.ServiceProviderCascadingValueSuppliers)
74+
foreach (var valueSupplier in renderer.ServiceProviderCascadingValueSuppliers)
7475
{
7576
if (valueSupplier.CanSupplyValue(info))
7677
{

src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.HtmlWriting.cs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Text.Encodings.Web;
6+
using Microsoft.AspNetCore.Components.Forms;
67
using Microsoft.AspNetCore.Components.RenderTree;
78

89
namespace Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;
@@ -14,6 +15,11 @@ public partial class StaticHtmlRenderer
1415
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
1516
};
1617

18+
private static readonly CascadingParameterInfo _findFormMappingContext = new CascadingParameterInfo(
19+
new CascadingParameterAttribute(),
20+
string.Empty,
21+
typeof(FormMappingContext));
22+
1723
private static readonly HtmlEncoder _htmlEncoder = HtmlEncoder.Default;
1824
private string? _closestSelectValueAsString;
1925

@@ -29,16 +35,16 @@ protected internal virtual void WriteComponentHtml(int componentId, TextWriter o
2935
Dispatcher.AssertAccess();
3036

3137
var frames = GetCurrentRenderTreeFrames(componentId);
32-
RenderFrames(output, frames, 0, frames.Count);
38+
RenderFrames(componentId, output, frames, 0, frames.Count);
3339
}
3440

35-
private int RenderFrames(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
41+
private int RenderFrames(int componentId, TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
3642
{
3743
var nextPosition = position;
3844
var endPosition = position + maxElements;
3945
while (position < endPosition)
4046
{
41-
nextPosition = RenderCore(output, frames, position);
47+
nextPosition = RenderCore(componentId, output, frames, position);
4248
if (position == nextPosition)
4349
{
4450
throw new InvalidOperationException("We didn't consume any input.");
@@ -50,6 +56,7 @@ private int RenderFrames(TextWriter output, ArrayRange<RenderTreeFrame> frames,
5056
}
5157

5258
private int RenderCore(
59+
int componentId,
5360
TextWriter output,
5461
ArrayRange<RenderTreeFrame> frames,
5562
int position)
@@ -58,7 +65,7 @@ private int RenderCore(
5865
switch (frame.FrameType)
5966
{
6067
case RenderTreeFrameType.Element:
61-
return RenderElement(output, frames, position);
68+
return RenderElement(componentId, output, frames, position);
6269
case RenderTreeFrameType.Attribute:
6370
throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}");
6471
case RenderTreeFrameType.Text:
@@ -70,17 +77,19 @@ private int RenderCore(
7077
case RenderTreeFrameType.Component:
7178
return RenderChildComponent(output, frames, position);
7279
case RenderTreeFrameType.Region:
73-
return RenderFrames(output, frames, position + 1, frame.RegionSubtreeLength - 1);
80+
return RenderFrames(componentId, output, frames, position + 1, frame.RegionSubtreeLength - 1);
7481
case RenderTreeFrameType.ElementReferenceCapture:
7582
case RenderTreeFrameType.ComponentReferenceCapture:
83+
return ++position;
7684
case RenderTreeFrameType.NamedEvent:
85+
RenderHiddenFieldForNamedSubmitEvent(componentId, output, frames, position);
7786
return ++position;
7887
default:
7988
throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'.");
8089
}
8190
}
8291

83-
private int RenderElement(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position)
92+
private int RenderElement(int componentId, TextWriter output, ArrayRange<RenderTreeFrame> frames, int position)
8493
{
8594
ref var frame = ref frames.Array[position];
8695
output.Write('<');
@@ -120,7 +129,7 @@ private int RenderElement(TextWriter output, ArrayRange<RenderTreeFrame> frames,
120129
}
121130
else
122131
{
123-
afterElement = RenderChildren(output, frames, afterAttributes, remainingElements);
132+
afterElement = RenderChildren(componentId, output, frames, afterAttributes, remainingElements);
124133
}
125134

126135
if (isSelect)
@@ -153,6 +162,54 @@ private int RenderElement(TextWriter output, ArrayRange<RenderTreeFrame> frames,
153162
}
154163
}
155164

165+
private void RenderHiddenFieldForNamedSubmitEvent(int componentId, TextWriter output, ArrayRange<RenderTreeFrame> frames, int namedEventFramePosition)
166+
{
167+
// Strictly speaking we could just emit the hidden input unconditionally, but since we currently
168+
// only intend to support this for "form submit" events, validate that's the case
169+
if (TryFindEnclosingElementFrame(frames, namedEventFramePosition, out var enclosingElementFrameIndex))
170+
{
171+
ref var enclosingElementFrame = ref frames.Array[enclosingElementFrameIndex];
172+
if (string.Equals(enclosingElementFrame.ElementName, "form", StringComparison.OrdinalIgnoreCase))
173+
{
174+
if (FindFormMappingContext(componentId) is { } formMappingContext)
175+
{
176+
var combinedFormName = formMappingContext.GetCombinedFormName(
177+
frames.Array[namedEventFramePosition].NamedEventAssignedName);
178+
179+
output.Write("<input type=\"hidden\" name=\"handler\" value=\"");
180+
_htmlEncoder.Encode(output, combinedFormName);
181+
output.Write("\" />");
182+
}
183+
}
184+
}
185+
}
186+
187+
private FormMappingContext? FindFormMappingContext(int forComponentId)
188+
{
189+
var componentState = GetComponentState(forComponentId);
190+
var supplier = CascadingParameterState.GetMatchingCascadingValueSupplier(
191+
in _findFormMappingContext,
192+
componentState.Renderer,
193+
componentState);
194+
195+
return (FormMappingContext?)supplier?.GetCurrentValue(_findFormMappingContext);
196+
}
197+
198+
private static bool TryFindEnclosingElementFrame(ArrayRange<RenderTreeFrame> frames, int frameIndex, out int result)
199+
{
200+
while (--frameIndex >= 0)
201+
{
202+
if (frames.Array[frameIndex].FrameType == RenderTreeFrameType.Element)
203+
{
204+
result = frameIndex;
205+
return true;
206+
}
207+
}
208+
209+
result = default;
210+
return false;
211+
}
212+
156213
private static int RenderAttributes(
157214
TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements, bool includeValueAttribute, out string? capturedValueAttribute)
158215
{
@@ -210,14 +267,14 @@ private static int RenderAttributes(
210267
return position + maxElements;
211268
}
212269

213-
private int RenderChildren(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
270+
private int RenderChildren(int componentId, TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
214271
{
215272
if (maxElements == 0)
216273
{
217274
return position;
218275
}
219276

220-
return RenderFrames(output, frames, position, maxElements);
277+
return RenderFrames(componentId, output, frames, position, maxElements);
221278
}
222279

223280
private int RenderChildComponent(TextWriter output, ArrayRange<RenderTreeFrame> frames, int position)

src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Globalization;
55
using System.Text;
6+
using Microsoft.AspNetCore.Components.Forms;
7+
using Microsoft.AspNetCore.Components.Forms.Mapping;
68
using Microsoft.AspNetCore.Components.Rendering;
79
using Microsoft.AspNetCore.Components.Sections;
810
using Microsoft.AspNetCore.Components.Web;
@@ -1005,6 +1007,84 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () =>
10051007
});
10061008
}
10071009

1010+
[Fact]
1011+
public async Task RenderComponentAsync_DoesNotAddHiddenInputForNamedSubmitEvents_WithoutFormMappingScope()
1012+
{
1013+
// Arrange
1014+
var formValueMapper = new TestFormValueMapper();
1015+
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
1016+
{
1017+
rtb.OpenElement(0, "form");
1018+
rtb.AddNamedEvent(1, "onsubmit", "somename");
1019+
rtb.CloseElement();
1020+
})).BuildServiceProvider();
1021+
1022+
var htmlRenderer = GetHtmlRenderer(serviceProvider);
1023+
await htmlRenderer.Dispatcher.InvokeAsync(async () =>
1024+
{
1025+
// Act
1026+
var result = await htmlRenderer.RenderComponentAsync<TestComponent>();
1027+
1028+
// Assert
1029+
Assert.Equal("<form></form>", result.ToHtmlString());
1030+
});
1031+
}
1032+
1033+
[Fact]
1034+
public async Task RenderComponentAsync_AddsHiddenInputForNamedSubmitEvents_WithDefaultFormMappingContext()
1035+
{
1036+
// Arrange
1037+
var formValueMapper = new TestFormValueMapper();
1038+
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
1039+
{
1040+
rtb.OpenElement(0, "form");
1041+
rtb.AddNamedEvent(1, "onsubmit", "some <name>");
1042+
rtb.CloseElement();
1043+
}))
1044+
.AddSingleton<ICascadingValueSupplier>(new SupplyParameterFromFormValueProvider(formValueMapper, ""))
1045+
.AddSingleton<IFormValueMapper>(formValueMapper).BuildServiceProvider();
1046+
1047+
var htmlRenderer = GetHtmlRenderer(serviceProvider);
1048+
await htmlRenderer.Dispatcher.InvokeAsync(async () =>
1049+
{
1050+
// Act
1051+
var result = await htmlRenderer.RenderComponentAsync<TestComponent>();
1052+
1053+
// Assert
1054+
Assert.Equal("<form><input type=\"hidden\" name=\"handler\" value=\"some &lt;name&gt;\" /></form>", result.ToHtmlString());
1055+
});
1056+
}
1057+
1058+
[Fact]
1059+
public async Task RenderComponentAsync_AddsHiddenInputForNamedSubmitEvents_InsideNamedFormMappingScope()
1060+
{
1061+
// Arrange
1062+
var serviceProvider = new ServiceCollection().AddSingleton(new RenderFragment(rtb =>
1063+
{
1064+
rtb.OpenComponent<FormMappingScope>(0);
1065+
rtb.AddComponentParameter(1, nameof(FormMappingScope.Name), "myscope");
1066+
rtb.AddComponentParameter(1, nameof(FormMappingScope.ChildContent), (RenderFragment<FormMappingContext>)(ctx => rtb =>
1067+
{
1068+
rtb.OpenElement(0, "form");
1069+
rtb.AddNamedEvent(1, "onsubmit", "somename");
1070+
rtb.CloseElement();
1071+
}));
1072+
rtb.CloseComponent();
1073+
})).AddSingleton<IFormValueMapper, TestFormValueMapper>().BuildServiceProvider();
1074+
1075+
var htmlRenderer = GetHtmlRenderer(serviceProvider);
1076+
await htmlRenderer.Dispatcher.InvokeAsync(async () =>
1077+
{
1078+
// Act
1079+
var result = await htmlRenderer.RenderComponentAsync<TestComponent>();
1080+
1081+
// Assert
1082+
Assert.Equal("<form><input type=\"hidden\" name=\"handler\" value=\"myscope.somename\" /></form>", result.ToHtmlString());
1083+
});
1084+
}
1085+
1086+
// TODO: As above, but inside a FormMappingScope, showing its name also shows up
1087+
10081088
void AssertHtmlContentEquals(IEnumerable<string> expected, HtmlRootComponent actual)
10091089
=> AssertHtmlContentEquals(string.Join(string.Empty, expected), actual);
10101090

@@ -1179,4 +1259,13 @@ HtmlRenderer GetHtmlRenderer(IServiceProvider serviceProvider = null)
11791259

11801260
return new HtmlRenderer(serviceProvider, NullLoggerFactory.Instance);
11811261
}
1262+
1263+
class TestFormValueMapper : IFormValueMapper
1264+
{
1265+
public bool CanMap(Type valueType, string formName = null)
1266+
=> throw new NotImplementedException();
1267+
1268+
public void Map(FormValueMappingContext context)
1269+
=> throw new NotImplementedException();
1270+
}
11821271
}

0 commit comments

Comments
 (0)