Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conditional writing of markup to output #31

Merged
merged 3 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Htmxor/Builder/HtmxorComponentEndpointDataSource.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using Htmxor.Components;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -53,6 +54,10 @@ private static List<Endpoint> UpdateEndpoints(IReadOnlyList<ComponentInfo> compo
{
builder.Metadata.Add(new HtmxorLayoutComponentMetadata(componentInfo.ComponentLayoutType));
}
else
{
builder.Metadata.Add(new HtmxorLayoutComponentMetadata(typeof(HtmxorLayoutComponentBase)));
}

builder.RequestDelegate = static httpContext =>
{
Expand Down
9 changes: 0 additions & 9 deletions src/Htmxor/Components/FragmentBase.cs

This file was deleted.

22 changes: 13 additions & 9 deletions src/Htmxor/Components/HtmxPartial.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components;

namespace Htmxor.Components;

public sealed class HtmxPartial : FragmentBase
public sealed class HtmxPartial : IComponent, IConditionalOutputComponent
{
private RenderHandle renderHandle;

[Parameter, EditorRequired]
public required RenderFragment ChildContent { get; set; }

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

public override Task SetParametersAsync(ParameterView parameters)

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

ChildContent = childContent;
Condition = parameters.GetValueOrDefault(nameof(Condition), true);
return base.SetParametersAsync(parameters);
renderHandle.Render(ChildContent);
return Task.CompletedTask;
}

protected override void BuildRenderTree([NotNull] RenderTreeBuilder builder)
=> builder.AddContent(0, ChildContent);
void IComponent.Attach(RenderHandle renderHandle)
{
this.renderHandle = renderHandle;
}

protected override bool ShouldRender() => Condition;
bool IConditionalOutputComponent.ShouldOutput(int _) => Condition;
}

13 changes: 13 additions & 0 deletions src/Htmxor/Components/HtmxorLayoutComponentBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

namespace Htmxor.Components;

public class HtmxorLayoutComponentBase : LayoutComponentBase, IConditionalOutputComponent
{
public bool ShouldOutput(int conditionalChildren)
=> conditionalChildren == 0;

protected override void BuildRenderTree([NotNull] RenderTreeBuilder builder)
=> builder.AddContent(0, Body);
}
15 changes: 15 additions & 0 deletions src/Htmxor/Components/IConditionalOutput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Htmxor.Components;

/// <summary>
/// Represents a component that can conditionally produce markup.
/// </summary>
public interface IConditionalOutputComponent
{
/// <summary>
/// Determine whether this component should produce any markup during a request.
/// </summary>
/// <param name="conditionalChildren">The number of children that implements <see cref="IConditionalOutputComponent"/>.</param>
/// <returns><see langword="true"/> if the component should produce markup, <see langword="false"/> otherwise.</returns>
bool ShouldOutput(int conditionalChildren);
}

11 changes: 6 additions & 5 deletions src/Htmxor/HtmxorComponentEndpointInvoker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public HtmxorComponentEndpointInvoker(HtmxorRenderer renderer, ILogger<HtmxorCom
this.logger = logger;
}

public Task Render(HttpContext context) => renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context));
public Task Render(HttpContext context)
=> renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context));

private async Task RenderComponentCore(HttpContext context)
{
Expand Down Expand Up @@ -129,10 +130,10 @@ private async Task RenderComponentCore(HttpContext context)
return;
}

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

// Importantly, we must not yield this thread (which holds exclusive access to the renderer sync context)
// in between the first call to htmlContent.WriteTo and the point where we start listening for subsequent
Expand Down
45 changes: 45 additions & 0 deletions src/Htmxor/Rendering/Buffering/ConditionalBufferedTextWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace Htmxor.Rendering.Buffering;

/// <summary>
/// A text writer that will only output when its <see cref="ShouldWrite"/> returns true.
/// </summary>
internal sealed class ConditionalBufferedTextWriter : BufferedTextWriter
{
internal bool ShouldWrite { get; set; } = true;

public ConditionalBufferedTextWriter(TextWriter underlying) : base(underlying)
{
}

public override void Write(char value)
{
if (ShouldWrite)
{
base.Write(value);
}
}

public override void Write(char[] buffer, int index, int count)
{
if (ShouldWrite)
{
base.Write(buffer, index, count);
}
}

public override void Write(string? value)
{
if (ShouldWrite)
{
base.Write(value);
}
}

public override void Write(int value)
{
if (ShouldWrite)
{
base.Write(value);
}
}
}
56 changes: 56 additions & 0 deletions src/Htmxor/Rendering/HtmxorComponentState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Diagnostics;
using Htmxor.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

namespace Htmxor.Rendering;

internal class HtmxorComponentState : ComponentState
{
private readonly HtmxorComponentState? parentComponentState;
private int conditionalChildrenCount;
private IConditionalOutputComponent? conditionalOutput;
private bool isDisposed;

public HtmxorComponentState(HtmxorRenderer renderer, int componentId, IComponent component, HtmxorComponentState? parentComponentState)
: base(renderer, componentId, component, parentComponentState)
{
if (component is IConditionalOutputComponent conditionalOutput)
{
this.conditionalOutput = conditionalOutput;
parentComponentState?.ConditionalChildAdded();
}

this.parentComponentState = parentComponentState;
}

public override ValueTask DisposeAsync()
{
if (parentComponentState is not null && conditionalOutput is not null && !isDisposed)
{
parentComponentState.ConditionalChildDisposed();
}

isDisposed = true;

return base.DisposeAsync();
}

private void ConditionalChildAdded()
{
conditionalChildrenCount++;
parentComponentState?.ConditionalChildAdded();
}

private void ConditionalChildDisposed()
{
conditionalChildrenCount--;
parentComponentState?.ConditionalChildDisposed();
Debug.Assert(conditionalChildrenCount >= 0, "conditionalChildrenCount should never be able to be less than zero");
}

internal bool ShouldGenerateMarkup()
=> conditionalOutput?.ShouldOutput(conditionalChildrenCount)
?? parentComponentState?.ShouldGenerateMarkup()
?? true;
}
Loading
Loading