Skip to content

Commit 28b37a1

Browse files
committed
feat: IConditionalOutputComponent used to determine if a component should produce markup
1 parent a56855e commit 28b37a1

11 files changed

+165
-101
lines changed

src/Htmxor/Builder/HtmxorComponentEndpointDataSource.cs

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Reflection;
2+
using Htmxor.Components;
23
using Microsoft.AspNetCore.Antiforgery;
34
using Microsoft.AspNetCore.Components.Endpoints;
45
using Microsoft.AspNetCore.Http;
@@ -53,6 +54,10 @@ private static List<Endpoint> UpdateEndpoints(IReadOnlyList<ComponentInfo> compo
5354
{
5455
builder.Metadata.Add(new HtmxorLayoutComponentMetadata(componentInfo.ComponentLayoutType));
5556
}
57+
else
58+
{
59+
builder.Metadata.Add(new HtmxorLayoutComponentMetadata(typeof(HtmxorLayoutComponentBase)));
60+
}
5661

5762
builder.RequestDelegate = static httpContext =>
5863
{

src/Htmxor/Components/FragmentBase.cs

-9
This file was deleted.

src/Htmxor/Components/HtmxPartial.cs

+13-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
using System.Diagnostics.CodeAnalysis;
2-
using Microsoft.AspNetCore.Components;
3-
using Microsoft.AspNetCore.Components.Rendering;
1+
using Microsoft.AspNetCore.Components;
42

53
namespace Htmxor.Components;
64

7-
public sealed class HtmxPartial : FragmentBase
5+
public sealed class HtmxPartial : IComponent, IConditionalOutputComponent
86
{
7+
private RenderHandle renderHandle;
8+
99
[Parameter, EditorRequired]
1010
public required RenderFragment ChildContent { get; set; }
1111

1212
[Parameter] public bool Condition { get; set; } = true;
1313

14-
public override Task SetParametersAsync(ParameterView parameters)
14+
15+
public Task SetParametersAsync(ParameterView parameters)
1516
{
1617
if (!parameters.TryGetValue<RenderFragment>(nameof(ChildContent), out var childContent))
1718
{
@@ -20,12 +21,15 @@ public override Task SetParametersAsync(ParameterView parameters)
2021

2122
ChildContent = childContent;
2223
Condition = parameters.GetValueOrDefault(nameof(Condition), true);
23-
return base.SetParametersAsync(parameters);
24+
renderHandle.Render(ChildContent);
25+
return Task.CompletedTask;
2426
}
2527

26-
protected override void BuildRenderTree([NotNull] RenderTreeBuilder builder)
27-
=> builder.AddContent(0, ChildContent);
28+
void IComponent.Attach(RenderHandle renderHandle)
29+
{
30+
this.renderHandle = renderHandle;
31+
}
2832

29-
protected override bool ShouldRender() => Condition;
33+
bool IConditionalOutputComponent.ShouldOutput(int _) => Condition;
3034
}
3135

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Microsoft.AspNetCore.Components;
2+
using Microsoft.AspNetCore.Components.Rendering;
3+
4+
namespace Htmxor.Components;
5+
6+
public class HtmxorLayoutComponentBase : LayoutComponentBase, IConditionalOutputComponent
7+
{
8+
public bool ShouldOutput(int conditionalChildren)
9+
=> conditionalChildren == 0;
10+
11+
protected override void BuildRenderTree([NotNull] RenderTreeBuilder builder)
12+
=> builder.AddContent(0, Body);
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Htmxor.Components;
2+
3+
/// <summary>
4+
/// Represents a component that can conditionally produce markup.
5+
/// </summary>
6+
public interface IConditionalOutputComponent
7+
{
8+
/// <summary>
9+
/// Determine whether this component should produce any markup during a request.
10+
/// </summary>
11+
/// <param name="conditionalChildren">The number of children that implements <see cref="IConditionalOutputComponent"/>.</param>
12+
/// <returns><see langword="true"/> if the component should produce markup, <see langword="false"/> otherwise.</returns>
13+
bool ShouldOutput(int conditionalChildren);
14+
}
15+

src/Htmxor/HtmxorComponentEndpointInvoker.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ public HtmxorComponentEndpointInvoker(HtmxorRenderer renderer, ILogger<HtmxorCom
3232
this.logger = logger;
3333
}
3434

35-
public Task Render(HttpContext context) => renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context));
35+
public Task Render(HttpContext context)
36+
=> renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context));
3637

3738
private async Task RenderComponentCore(HttpContext context)
3839
{
@@ -129,10 +130,10 @@ private async Task RenderComponentCore(HttpContext context)
129130
return;
130131
}
131132

132-
// Matches MVC's MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize
133-
var defaultBufferSize = 16 * 1024;
134-
await using var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
135-
using var bufferWriter = new BufferedTextWriter(writer);
133+
// Matches MVC's MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize
134+
var defaultBufferSize = 16 * 1024;
135+
await using var writer = new HttpResponseStreamWriter(context.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
136+
using var bufferWriter = new ConditionalBufferedTextWriter(writer);
136137

137138
// Importantly, we must not yield this thread (which holds exclusive access to the renderer sync context)
138139
// in between the first call to htmlContent.WriteTo and the point where we start listening for subsequent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
namespace Htmxor.Rendering.Buffering;
2+
3+
/// <summary>
4+
/// A text writer that will only output when its <see cref="ShouldWrite"/> returns true.
5+
/// </summary>
6+
internal sealed class ConditionalBufferedTextWriter : BufferedTextWriter
7+
{
8+
internal bool ShouldWrite { get; set; } = true;
9+
10+
public ConditionalBufferedTextWriter(TextWriter underlying) : base(underlying)
11+
{
12+
}
13+
14+
public override void Write(char value)
15+
{
16+
if (ShouldWrite)
17+
{
18+
base.Write(value);
19+
}
20+
}
21+
22+
public override void Write(char[] buffer, int index, int count)
23+
{
24+
if (ShouldWrite)
25+
{
26+
base.Write(buffer, index, count);
27+
}
28+
}
29+
30+
public override void Write(string? value)
31+
{
32+
if (ShouldWrite)
33+
{
34+
base.Write(value);
35+
}
36+
}
37+
38+
public override void Write(int value)
39+
{
40+
if (ShouldWrite)
41+
{
42+
base.Write(value);
43+
}
44+
}
45+
}
+38-5
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,56 @@
1-
using Microsoft.AspNetCore.Components;
1+
using System.Diagnostics;
2+
using Htmxor.Components;
3+
using Microsoft.AspNetCore.Components;
24
using Microsoft.AspNetCore.Components.Rendering;
3-
using Microsoft.AspNetCore.Components.RenderTree;
45

56
namespace Htmxor.Rendering;
67

78
internal class HtmxorComponentState : ComponentState
89
{
9-
public HtmxorComponentState(Renderer renderer, int componentId, IComponent component, HtmxorComponentState? parentComponentState)
10+
private readonly HtmxorComponentState? parentComponentState;
11+
private int conditionalChildrenCount;
12+
private IConditionalOutputComponent? conditionalOutput;
13+
private bool isDisposed;
14+
15+
public HtmxorComponentState(HtmxorRenderer renderer, int componentId, IComponent component, HtmxorComponentState? parentComponentState)
1016
: base(renderer, componentId, component, parentComponentState)
1117
{
18+
if (component is IConditionalOutputComponent conditionalOutput)
19+
{
20+
this.conditionalOutput = conditionalOutput;
21+
parentComponentState?.ConditionalChildAdded();
22+
}
23+
24+
this.parentComponentState = parentComponentState;
1225
}
1326

1427
public override ValueTask DisposeAsync()
1528
{
29+
if (parentComponentState is not null && conditionalOutput is not null && !isDisposed)
30+
{
31+
parentComponentState.ConditionalChildDisposed();
32+
}
33+
34+
isDisposed = true;
35+
1636
return base.DisposeAsync();
1737
}
1838

19-
internal bool HasPartialFragments()
39+
private void ConditionalChildAdded()
2040
{
21-
throw new NotImplementedException();
41+
conditionalChildrenCount++;
42+
parentComponentState?.ConditionalChildAdded();
2243
}
44+
45+
private void ConditionalChildDisposed()
46+
{
47+
conditionalChildrenCount--;
48+
parentComponentState?.ConditionalChildDisposed();
49+
Debug.Assert(conditionalChildrenCount >= 0, "conditionalChildrenCount should never be able to be less than zero");
50+
}
51+
52+
internal bool ShouldGenerateMarkup()
53+
=> conditionalOutput?.ShouldOutput(conditionalChildrenCount)
54+
?? parentComponentState?.ShouldGenerateMarkup()
55+
?? true;
2356
}

src/Htmxor/Rendering/HtmxorRenderer.HtmlWriting.cs

+15-11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Text;
99
using System.Text.Encodings.Web;
1010
using Htmxor.Http;
11+
using Htmxor.Rendering.Buffering;
1112
using Microsoft.AspNetCore.Components;
1213
using Microsoft.AspNetCore.Components.Forms;
1314
using Microsoft.AspNetCore.Components.Rendering;
@@ -30,22 +31,25 @@ internal partial class HtmxorRenderer
3031
args: [new CascadingParameterAttribute(), string.Empty, typeof(FormMappingContext)],
3132
culture: CultureInfo.InvariantCulture)!;
3233

33-
private readonly TextEncoder _javaScriptEncoder;
34-
private TextEncoder _htmlEncoder;
35-
private string? _closestSelectValueAsString;
36-
private bool writeFromCompleteRenderTree;
34+
private readonly TextEncoder javaScriptEncoder;
35+
private TextEncoder htmlEncoder;
36+
private string? closestSelectValueAsString;
3737

3838
private void WriteRootComponent(HtmxorComponentState rootComponentState, TextWriter output)
3939
{
4040
// We're about to walk over some buffers inside the renderer that can be mutated during rendering.
4141
// So, we require exclusive access to the renderer during this synchronous process.
4242
Dispatcher.AssertAccess();
43-
4443
WriteComponent(rootComponentState, output);
4544
}
4645

4746
private void WriteComponent(HtmxorComponentState componentState, TextWriter output)
4847
{
48+
if (output is ConditionalBufferedTextWriter conditionalOutput)
49+
{
50+
conditionalOutput.ShouldWrite = componentState.ShouldGenerateMarkup();
51+
}
52+
4953
var frames = GetCurrentRenderTreeFrames(componentState.ComponentId);
5054
RenderFrames(componentState, output, frames, 0, frames.Count);
5155
}
@@ -87,7 +91,7 @@ private int RenderCore(
8791
case RenderTreeFrameType.Attribute:
8892
throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}");
8993
case RenderTreeFrameType.Text:
90-
_htmlEncoder.Encode(output, frame.TextContent);
94+
htmlEncoder.Encode(output, frame.TextContent);
9195
return ++position;
9296
case RenderTreeFrameType.Markup:
9397
output.Write(frame.MarkupContent);
@@ -144,7 +148,7 @@ private int RenderElement(HtmxorComponentState componentState, TextWriter output
144148
{
145149
// Textarea is a special type of form field where the value is given as text content instead of a 'value' attribute
146150
// So, if we captured a value attribute, use that instead of any child content
147-
_htmlEncoder.Encode(output, capturedValueAttribute);
151+
htmlEncoder.Encode(output, capturedValueAttribute);
148152
afterElement = position + frame.ElementSubtreeLength; // Skip descendants
149153
}
150154
else if (string.Equals(frame.ElementName, "script", StringComparison.OrdinalIgnoreCase))
@@ -193,15 +197,15 @@ private int RenderScriptElementChildren(HtmxorComponentState componentState, Tex
193197
// user-supplied content inside a <script> block, but that if someone does, we
194198
// want the encoding style to match the context for correctness and safety. This is
195199
// also consistent with .cshtml's treatment of <script>.
196-
var originalEncoder = _htmlEncoder;
200+
var originalEncoder = htmlEncoder;
197201
try
198202
{
199-
_htmlEncoder = _javaScriptEncoder;
203+
htmlEncoder = javaScriptEncoder;
200204
return RenderChildren(componentState, output, frames, position, maxElements);
201205
}
202206
finally
203207
{
204-
_htmlEncoder = originalEncoder;
208+
htmlEncoder = originalEncoder;
205209
}
206210
}
207211

@@ -219,7 +223,7 @@ private void RenderHiddenFieldForNamedSubmitEvent(HtmxorComponentState component
219223
if (TryCreateScopeQualifiedEventName(componentState.ComponentId, namedEventFrame.NamedEventAssignedName, out var combinedFormName))
220224
{
221225
output.Write("<input type=\"hidden\" name=\"_handler\" value=\"");
222-
_htmlEncoder.Encode(output, combinedFormName);
226+
htmlEncoder.Encode(output, combinedFormName);
223227
output.Write("\" />");
224228
}
225229
}

src/Htmxor/Rendering/HtmxorRenderer.Rendering.cs

-14
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,6 @@ namespace Htmxor.Rendering;
1010

1111
internal partial class HtmxorRenderer
1212
{
13-
private HttpContext httpContext = default!; // Always set at the start of an inbound call
14-
15-
private void SetHttpContext(HttpContext httpContext)
16-
{
17-
if (this.httpContext is null)
18-
{
19-
this.httpContext = httpContext;
20-
}
21-
else if (this.httpContext != httpContext)
22-
{
23-
throw new InvalidOperationException("The HttpContext cannot change value once assigned.");
24-
}
25-
}
26-
2713
internal async ValueTask<RenderedComponentHtmlContent> RenderEndpointComponent(
2814
HttpContext httpContext,
2915
[DynamicallyAccessedMembers(Component)] Type rootComponentType,

0 commit comments

Comments
 (0)