Skip to content

Commit

Permalink
feat: HtmxLazy component
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed May 3, 2024
1 parent 606a807 commit 957a48d
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 27 deletions.
18 changes: 13 additions & 5 deletions samples/MinimalHtmxorApp/Components/Pages/Weather.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
@page "/weather"
<PageTitle>Weather - loading data</PageTitle>
@inherits ConditionalComponentBase
<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>
<table hx-get="/weather/data" hx-trigger="load" class="table">
<caption>Loading data . . .</caption>
</table>
<p>This component demonstrates showing lazy loaded data.</p>

<HtmxLazy Element="table" Id="weather-data" class="table">
<Loading>
<PageTitle>Weather - Loading data, please wait ...</PageTitle>
<caption>Loading data . . .</caption>
</Loading>
<ChildContent>
<WeatherData />
</ChildContent>
</HtmxLazy>
40 changes: 18 additions & 22 deletions samples/MinimalHtmxorApp/Components/Pages/WeatherData.razor
Original file line number Diff line number Diff line change
@@ -1,34 +1,30 @@
@page "/weather/data"
@* This shows how the page title can be updated during an htmx request. *@
<PageTitle>Weather - data loaded</PageTitle>
<table class="table">
<thead>
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
</tbody>
@code {
private WeatherForecast[] forecasts = [];

protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
await Task.Delay(2000);

var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering",
Expand Down
91 changes: 91 additions & 0 deletions src/Htmxor/Components/HtmxLazy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Htmxor.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;

namespace Htmxor.Components;

public sealed class HtmxLazy : ComponentBase, IConditionalOutputComponent
{
[Inject]
private HtmxContext HtmxContext { get; set; } = default!;

[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = "False positive. This is a parameter.")]
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? AdditionalAttributes { get; set; }

/// <summary>
/// The content to render out when the htmx load request arrives.
/// </summary>
[Parameter, EditorRequired]
public required RenderFragment ChildContent { get; set; }

/// <summary>
/// The content to should be rendered while the htmx load request is in progress.
/// </summary>
[Parameter]
public RenderFragment? Loading { get; set; }

/// <summary>
/// The ID of the element that <see cref="HtmxLazy"/> should render it's content inside.
/// This is also the ID that htmx will use to target the element to replace it's content.
/// </summary>
[Parameter, EditorRequired]
public required string Id { get; set; }

/// <summary>
/// The element type that <see cref="HtmxLazy"/> should render
/// <see cref="ChildContent"/> and <see cref="Loading"/> into.
/// </summary>
/// <remarks>Default is a <c>div</c> element.</remarks>
[Parameter]
public string Element { get; set; } = "div";

protected override void OnParametersSet()
{
Element = string.IsNullOrWhiteSpace(Element) ? "div" : Element.Trim();
Id = string.IsNullOrWhiteSpace(Id) ? Id : Id.Trim();

// If the user manually added some of these attributes they are removed
// to avoid inconsistent behavior.
// The user should not set these attributes manually.
if (AdditionalAttributes is not null)
{
AdditionalAttributes.Remove(HtmxConstants.Attributes.Id);
AdditionalAttributes.Remove(HtmxConstants.Attributes.HxGet);
AdditionalAttributes.Remove(HtmxConstants.Attributes.HxTrigger);
AdditionalAttributes.Remove(HtmxConstants.Attributes.HxTarget);
AdditionalAttributes.Remove(HtmxConstants.Attributes.HxSwap);
}
}

protected override void BuildRenderTree([NotNull] RenderTreeBuilder builder)
{
var request = HtmxContext.Request;
if (request.IsFullPageRequest)
{
builder.OpenElement(1, Element);
builder.AddAttribute(2, HtmxConstants.Attributes.Id, Id);
builder.AddAttribute(3, HtmxConstants.Attributes.HxGet, HtmxContext.Request.Path);
builder.AddAttribute(4, HtmxConstants.Attributes.HxTrigger, HtmxConstants.Triggers.Load);
builder.AddAttribute(5, HtmxConstants.Attributes.HxTarget, $"#{Id}");
builder.AddAttribute(6, HtmxConstants.Attributes.HxSwap, HtmxConstants.SwapStyles.InnerHTML);
if (AdditionalAttributes is not null)
{
builder.AddMultipleAttributes(7, AdditionalAttributes);
}
if (Loading is not null)
{
builder.AddContent(8, Loading);
}
builder.CloseElement();
}
else if (request.Target == Id && request.Trigger == Id)
{
builder.AddContent(9, ChildContent);
}
}

bool IConditionalOutputComponent.ShouldOutput([NotNull] HtmxContext context, int directConditionalChildren, int conditionalChildren)
=> context.Request.IsFullPageRequest
|| (context.Request.Target == Id && context.Request.Trigger == Id);
}

0 comments on commit 957a48d

Please sign in to comment.