Skip to content

Commit afd7271

Browse files
SSR as library (#46935)
1 parent 5d10dce commit afd7271

26 files changed

+1549
-1084
lines changed

src/Components/Components/src/Infrastructure/ComponentStatePersistenceManager.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ public async Task RestoreStateAsync(IPersistentComponentStateStore store)
4848
/// <param name="renderer">The <see cref="Renderer"/> that components are being rendered.</param>
4949
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
5050
public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer renderer)
51+
=> PersistStateAsync(store, renderer.Dispatcher);
52+
53+
/// <summary>
54+
/// Persists the component application state into the given <see cref="IPersistentComponentStateStore"/>.
55+
/// </summary>
56+
/// <param name="store">The <see cref="IPersistentComponentStateStore"/> to restore the application state from.</param>
57+
/// <param name="dispatcher">The <see cref="Dispatcher"/> corresponding to the components' renderer.</param>
58+
/// <returns>A <see cref="Task"/> that will complete when the state has been restored.</returns>
59+
public Task PersistStateAsync(IPersistentComponentStateStore store, Dispatcher dispatcher)
5160
{
5261
if (_stateIsPersisted)
5362
{
@@ -56,7 +65,7 @@ public Task PersistStateAsync(IPersistentComponentStateStore store, Renderer ren
5665

5766
_stateIsPersisted = true;
5867

59-
return renderer.Dispatcher.InvokeAsync(PauseAndPersistState);
68+
return dispatcher.InvokeAsync(PauseAndPersistState);
6069

6170
async Task PauseAndPersistState()
6271
{

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#nullable enable
22
Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
3+
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
34
Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task!
45
*REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri!
56
Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri!
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components.HtmlRendering;
5+
6+
namespace Microsoft.AspNetCore.Components.Web;
7+
8+
/// <summary>
9+
/// Represents the output of rendering a component as HTML. The content can change if the component instance re-renders.
10+
/// </summary>
11+
public sealed class HtmlComponent
12+
{
13+
private readonly HtmlRendererCore? _renderer;
14+
private readonly int _componentId;
15+
private readonly Task _quiescenceTask;
16+
17+
internal HtmlComponent(HtmlRendererCore? renderer, int componentId, Task quiescenceTask)
18+
{
19+
_renderer = renderer;
20+
_componentId = componentId;
21+
_quiescenceTask = quiescenceTask;
22+
}
23+
24+
/// <summary>
25+
/// Gets an instance of <see cref="HtmlComponent"/> that produces no content.
26+
/// </summary>
27+
public static HtmlComponent Empty { get; } = new HtmlComponent(null, 0, Task.CompletedTask);
28+
29+
/// <summary>
30+
/// Obtains a <see cref="Task"/> that completes when the component hierarchy has completed asynchronous tasks such as loading.
31+
/// </summary>
32+
/// <returns>A <see cref="Task"/> that completes when the component hierarchy has completed asynchronous tasks such as loading.</returns>
33+
public Task WaitForQuiescenceAsync()
34+
=> _quiescenceTask;
35+
36+
/// <summary>
37+
/// Returns an HTML string representation of the component's latest output.
38+
/// </summary>
39+
/// <returns>An HTML string representation of the component's latest output.</returns>
40+
public string ToHtmlString()
41+
{
42+
if (_renderer is null)
43+
{
44+
return string.Empty;
45+
}
46+
47+
using var writer = new StringWriter();
48+
WriteHtmlTo(writer);
49+
return writer.ToString();
50+
}
51+
52+
/// <summary>
53+
/// Writes the component's latest output as HTML to the specified writer.
54+
/// </summary>
55+
/// <param name="output">The output destination.</param>
56+
public void WriteHtmlTo(TextWriter output)
57+
{
58+
if (_renderer is not null)
59+
{
60+
HtmlComponentWriter.Write(_renderer, _componentId, output);
61+
}
62+
}
63+
}
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Text.Encodings.Web;
6+
using Microsoft.AspNetCore.Components.HtmlRendering;
7+
using Microsoft.AspNetCore.Components.RenderTree;
8+
9+
namespace Microsoft.AspNetCore.Components.Web;
10+
11+
// This is OK to be a struct because it never gets passed around anywhere. Other code can't even get an instance
12+
// of it. It just keeps track of some contextual information during a single synchronous HTML output operation.
13+
internal ref struct HtmlComponentWriter
14+
{
15+
private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
16+
{
17+
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
18+
};
19+
20+
private static readonly HtmlEncoder _htmlEncoder = HtmlEncoder.Default;
21+
private readonly HtmlRendererCore _renderer;
22+
private readonly TextWriter _output;
23+
private string? _closestSelectValueAsString;
24+
25+
public static void Write(HtmlRendererCore renderer, int componentId, TextWriter output)
26+
{
27+
// We're about to walk over some buffers inside the renderer that can be mutated during rendering.
28+
// So, we require exclusive access to the renderer during this synchronous process.
29+
renderer.Dispatcher.AssertAccess();
30+
31+
var context = new HtmlComponentWriter(renderer, output);
32+
context.RenderComponent(componentId);
33+
}
34+
35+
private HtmlComponentWriter(HtmlRendererCore renderer, TextWriter output)
36+
{
37+
_renderer = renderer;
38+
_output = output;
39+
}
40+
41+
private int RenderFrames(ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
42+
{
43+
var nextPosition = position;
44+
var endPosition = position + maxElements;
45+
while (position < endPosition)
46+
{
47+
nextPosition = RenderCore(frames, position);
48+
if (position == nextPosition)
49+
{
50+
throw new InvalidOperationException("We didn't consume any input.");
51+
}
52+
position = nextPosition;
53+
}
54+
55+
return nextPosition;
56+
}
57+
58+
private int RenderCore(
59+
ArrayRange<RenderTreeFrame> frames,
60+
int position)
61+
{
62+
ref var frame = ref frames.Array[position];
63+
switch (frame.FrameType)
64+
{
65+
case RenderTreeFrameType.Element:
66+
return RenderElement(frames, position);
67+
case RenderTreeFrameType.Attribute:
68+
throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}");
69+
case RenderTreeFrameType.Text:
70+
_htmlEncoder.Encode(_output, frame.TextContent);
71+
return ++position;
72+
case RenderTreeFrameType.Markup:
73+
_output.Write(frame.MarkupContent);
74+
return ++position;
75+
case RenderTreeFrameType.Component:
76+
return RenderChildComponent(frames, position);
77+
case RenderTreeFrameType.Region:
78+
return RenderFrames(frames, position + 1, frame.RegionSubtreeLength - 1);
79+
case RenderTreeFrameType.ElementReferenceCapture:
80+
case RenderTreeFrameType.ComponentReferenceCapture:
81+
return ++position;
82+
default:
83+
throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'.");
84+
}
85+
}
86+
87+
private int RenderElement(ArrayRange<RenderTreeFrame> frames, int position)
88+
{
89+
ref var frame = ref frames.Array[position];
90+
_output.Write('<');
91+
_output.Write(frame.ElementName);
92+
int afterElement;
93+
var isTextArea = string.Equals(frame.ElementName, "textarea", StringComparison.OrdinalIgnoreCase);
94+
// We don't want to include value attribute of textarea element.
95+
var afterAttributes = RenderAttributes(frames, position + 1, frame.ElementSubtreeLength - 1, !isTextArea, out var capturedValueAttribute);
96+
97+
// When we see an <option> as a descendant of a <select>, and the option's "value" attribute matches the
98+
// "value" attribute on the <select>, then we auto-add the "selected" attribute to that option. This is
99+
// a way of converting Blazor's select binding feature to regular static HTML.
100+
if (_closestSelectValueAsString != null
101+
&& string.Equals(frame.ElementName, "option", StringComparison.OrdinalIgnoreCase)
102+
&& string.Equals(capturedValueAttribute, _closestSelectValueAsString, StringComparison.Ordinal))
103+
{
104+
_output.Write(" selected");
105+
}
106+
107+
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
108+
if (remainingElements > 0 || isTextArea)
109+
{
110+
_output.Write('>');
111+
112+
var isSelect = string.Equals(frame.ElementName, "select", StringComparison.OrdinalIgnoreCase);
113+
if (isSelect)
114+
{
115+
_closestSelectValueAsString = capturedValueAttribute;
116+
}
117+
118+
if (isTextArea && !string.IsNullOrEmpty(capturedValueAttribute))
119+
{
120+
// Textarea is a special type of form field where the value is given as text content instead of a 'value' attribute
121+
// So, if we captured a value attribute, use that instead of any child content
122+
_htmlEncoder.Encode(_output, capturedValueAttribute);
123+
afterElement = position + frame.ElementSubtreeLength; // Skip descendants
124+
}
125+
else
126+
{
127+
afterElement = RenderChildren(frames, afterAttributes, remainingElements);
128+
}
129+
130+
if (isSelect)
131+
{
132+
// There's no concept of nested <select> elements, so as soon as we're exiting one of them,
133+
// we can safely say there is no longer any value for this
134+
_closestSelectValueAsString = null;
135+
}
136+
137+
_output.Write("</");
138+
_output.Write(frame.ElementName);
139+
_output.Write('>');
140+
Debug.Assert(afterElement == position + frame.ElementSubtreeLength);
141+
return afterElement;
142+
}
143+
else
144+
{
145+
if (SelfClosingElements.Contains(frame.ElementName))
146+
{
147+
_output.Write(" />");
148+
}
149+
else
150+
{
151+
_output.Write("></");
152+
_output.Write(frame.ElementName);
153+
_output.Write('>');
154+
}
155+
Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength);
156+
return afterAttributes;
157+
}
158+
}
159+
160+
private int RenderAttributes(
161+
ArrayRange<RenderTreeFrame> frames, int position, int maxElements, bool includeValueAttribute, out string? capturedValueAttribute)
162+
{
163+
capturedValueAttribute = null;
164+
165+
if (maxElements == 0)
166+
{
167+
return position;
168+
}
169+
170+
for (var i = 0; i < maxElements; i++)
171+
{
172+
var candidateIndex = position + i;
173+
ref var frame = ref frames.Array[candidateIndex];
174+
175+
if (frame.FrameType != RenderTreeFrameType.Attribute)
176+
{
177+
if (frame.FrameType == RenderTreeFrameType.ElementReferenceCapture)
178+
{
179+
continue;
180+
}
181+
182+
return candidateIndex;
183+
}
184+
185+
if (frame.AttributeName.Equals("value", StringComparison.OrdinalIgnoreCase))
186+
{
187+
capturedValueAttribute = frame.AttributeValue as string;
188+
189+
if (!includeValueAttribute)
190+
{
191+
continue;
192+
}
193+
}
194+
195+
switch (frame.AttributeValue)
196+
{
197+
case bool flag when flag:
198+
_output.Write(' ');
199+
_output.Write(frame.AttributeName);
200+
break;
201+
case string value:
202+
_output.Write(' ');
203+
_output.Write(frame.AttributeName);
204+
_output.Write('=');
205+
_output.Write('\"');
206+
_htmlEncoder.Encode(_output, value);
207+
_output.Write('\"');
208+
break;
209+
default:
210+
break;
211+
}
212+
}
213+
214+
return position + maxElements;
215+
}
216+
217+
private int RenderChildren(ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
218+
{
219+
if (maxElements == 0)
220+
{
221+
return position;
222+
}
223+
224+
return RenderFrames(frames, position, maxElements);
225+
}
226+
227+
private void RenderComponent(int componentId)
228+
{
229+
var frames = _renderer.GetCurrentRenderTreeFrames(componentId);
230+
RenderFrames(frames, 0, frames.Count);
231+
}
232+
233+
private int RenderChildComponent(ArrayRange<RenderTreeFrame> frames, int position)
234+
{
235+
ref var frame = ref frames.Array[position];
236+
237+
RenderComponent(frame.ComponentId);
238+
239+
return position + frame.ComponentSubtreeLength;
240+
}
241+
}

0 commit comments

Comments
 (0)