Skip to content

Commit

Permalink
✨ Add lifecycle hooks for page and islands components
Browse files Browse the repository at this point in the history
  • Loading branch information
koeeenig committed Feb 8, 2024
1 parent eb7d574 commit fa76c6b
Show file tree
Hide file tree
Showing 54 changed files with 663 additions and 4,286 deletions.
2 changes: 1 addition & 1 deletion src/BlazeKit.Abstraction/BlazeKit.Abstraction.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
Expand Down
2 changes: 1 addition & 1 deletion src/BlazeKit.Abstraction/IPostRequest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;

namespace BlazeKit.Abstraction;
/// <summary>
Expand Down
10 changes: 0 additions & 10 deletions src/BlazeKit.Abstraction/IServerLoad.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/BlazeKit.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using System.Diagnostics;

Console.OutputEncoding = System.Text.Encoding.UTF8;
Debugger.Launch();
//Debugger.Launch();

var app = new Spectre.Console.Cli.CommandApp();
app.Configure(config =>
Expand Down
5 changes: 3 additions & 2 deletions src/BlazeKit.Hydration/BKitData.razor
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@using System.Diagnostics
@code {
[Inject]
private DataHydrationContext HydrationContext { get; set; }
Expand All @@ -7,12 +8,12 @@
// set update callback to render the component when data has been added to the data hydration context
HydrationContext.OnUpdate(() =>
{
Console.WriteLine("Data has changed -> rerender");
Debug.WriteLine("Data has changed -> rerender");
InvokeAsync(this.StateHasChanged);
});
}
}
@if(!HydrationContext.IsEmpty())
{
@(new MarkupString($"<script type=\"blazekit/json\">{HydrationContext.Serialized()}</script>"))
@(new MarkupString($"<script type=\"blazekit/json\">{HydrationContext.AsJson()}</script>"))
}
18 changes: 17 additions & 1 deletion src/BlazeKit.Hydration/DataHydrationContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace BlazeKit.Hydration;
public class DataHydrationContext
Expand Down Expand Up @@ -74,10 +75,25 @@ public bool TryGet<T>(string key, out T value)
}
}

public string Serialized() {
internal string AsJson() {
return System.Text.Json.JsonSerializer.Serialize(hydrationData);
}

internal string AsBase64()
{
return
Convert.ToBase64String(
JsonSerializer.SerializeToUtf8Bytes(
System.Text.Json.JsonSerializer.Serialize(hydrationData),
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
})
);
}


internal void OnUpdate(Action onUpdate)
{
Expand Down
17 changes: 13 additions & 4 deletions src/BlazeKit.Reactivity/Blazor/Derived.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public sealed class Derived<T> : ISignal<T>
private HashSet<Action> subscriptions;
private readonly Effect effect;
private T value;
private bool initialCall = false;

/// <summary>
/// A derived value as <see cref="ISignal{T}"/> that can be used in a Blazor component."/>
Expand All @@ -28,12 +29,20 @@ public Derived(Func<T> fn, IReactiveComponent component)
{
var derived = fn();
Console.WriteLine($"{derived}");
this.value = derived;
foreach (var subscription in subscriptions)
if(!this.value.Equals(derived))
{
subscription();
this.value = derived;
foreach (var subscription in subscriptions)
{
subscription();
}
if(!initialCall)
{
component.Update();
initialCall = true;
}
}
component.Update();
});
}

Expand Down
19 changes: 12 additions & 7 deletions src/BlazeKit.Reactivity/Blazor/State.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public sealed class State<T> : ISignal<T>
private readonly Signal<T> signal;
private readonly Effect effect;
private readonly IReactiveComponent component;
private bool initialCall = false;

/// <summary>
/// A <see cref="ISignal{T}"/> that can be used in a Blazor component."/>
Expand All @@ -24,19 +25,23 @@ public sealed class State<T> : ISignal<T>
public State(T value, IReactiveComponent component)
{
this.signal = new Signal<T>(value);
this.effect = new Effect(() =>
{
// required to keep track of the dependency
var _ = signal.Value;
component.Update();
});
this.component = component;
}

public T Value
{
get => signal.Value;
set => signal.Value = value;
set
{
if(!signal.Value.Equals(value)) {
signal.Value = value;
if(!initialCall)
{
component.Update();
initialCall = true;
}
}
}
}

public void Subscribe(Action<T> subscriber)
Expand Down
137 changes: 137 additions & 0 deletions src/BlazeKit.Static/BKHtmlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
using Microsoft.Extensions.Logging;
using System.Diagnostics.CodeAnalysis;

namespace BlazeKit.Static
{
/// <summary>
/// Provides a mechanism for rendering components non-interactively as HTML markup.
/// </summary>
public sealed class BKHtmlRenderer : IDisposable, IAsyncDisposable
{
private readonly BKStaticHtmlRenderer _passiveHtmlRenderer;

/// <summary>
/// Constructs an instance of <see cref="HtmlRenderer"/>.
/// </summary>
/// <param name="services">The services to use when rendering components.</param>
/// <param name="loggerFactory">The logger factory to use.</param>
public BKHtmlRenderer(IServiceProvider services, ILoggerFactory loggerFactory)
{
_passiveHtmlRenderer = new BKStaticHtmlRenderer(services, loggerFactory);
}

/// <inheritdoc />
public void Dispose()
=> _passiveHtmlRenderer.Dispose();

/// <inheritdoc />
public ValueTask DisposeAsync()
=> _passiveHtmlRenderer.DisposeAsync();

/// <summary>
/// Gets the <see cref="Components.Dispatcher" /> associated with this instance. Any calls to
/// <see cref="RenderComponentAsync{TComponent}()"/> or <see cref="BeginRenderingComponent{TComponent}()"/>
/// must be performed using this <see cref="Components.Dispatcher" />.
/// </summary>
public Dispatcher Dispatcher => _passiveHtmlRenderer.Dispatcher;

/// <summary>
/// Adds an instance of the specified component and instructs it to render. The resulting content represents the
/// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete
/// any asynchronous operations such as loading, await <see cref="HtmlRootComponent.QuiescenceTask"/> before
/// reading content from the <see cref="HtmlRootComponent"/>.
/// </summary>
/// <typeparam name="TComponent">The component type.</typeparam>
/// <returns>An <see cref="HtmlRootComponent"/> instance representing the render output.</returns>
public HtmlRootComponent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent
=> _passiveHtmlRenderer.BeginRenderingComponent(typeof(TComponent), ParameterView.Empty);

/// <summary>
/// Adds an instance of the specified component and instructs it to render. The resulting content represents the
/// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete
/// any asynchronous operations such as loading, await <see cref="HtmlRootComponent.QuiescenceTask"/> before
/// reading content from the <see cref="HtmlRootComponent"/>.
/// </summary>
/// <typeparam name="TComponent">The component type.</typeparam>
/// <param name="parameters">Parameters for the component.</param>
/// <returns>An <see cref="HtmlRootComponent"/> instance representing the render output.</returns>
public HtmlRootComponent BeginRenderingComponent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(
ParameterView parameters) where TComponent : IComponent
=> _passiveHtmlRenderer.BeginRenderingComponent(typeof(TComponent), parameters);

/// <summary>
/// Adds an instance of the specified component and instructs it to render. The resulting content represents the
/// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete
/// any asynchronous operations such as loading, await <see cref="HtmlRootComponent.QuiescenceTask"/> before
/// reading content from the <see cref="HtmlRootComponent"/>.
/// </summary>
/// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
/// <returns>An <see cref="HtmlRootComponent"/> instance representing the render output.</returns>
public HtmlRootComponent BeginRenderingComponent(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
=> _passiveHtmlRenderer.BeginRenderingComponent(componentType, ParameterView.Empty);

/// <summary>
/// Adds an instance of the specified component and instructs it to render. The resulting content represents the
/// initial synchronous rendering output, which may later change. To wait for the component hierarchy to complete
/// any asynchronous operations such as loading, await <see cref="HtmlRootComponent.QuiescenceTask"/> before
/// reading content from the <see cref="HtmlRootComponent"/>.
/// </summary>
/// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
/// <param name="parameters">Parameters for the component.</param>
/// <returns>An <see cref="HtmlRootComponent"/> instance representing the render output.</returns>
public HtmlRootComponent BeginRenderingComponent(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType,
ParameterView parameters)
=> _passiveHtmlRenderer.BeginRenderingComponent(componentType, parameters);

/// <summary>
/// Adds an instance of the specified component and instructs it to render, waiting
/// for the component hierarchy to complete asynchronous tasks such as loading.
/// </summary>
/// <typeparam name="TComponent">The component type.</typeparam>
/// <returns>A task that completes with <see cref="HtmlRootComponent"/> once the component hierarchy has completed any asynchronous tasks such as loading.</returns>
public Task<HtmlRootComponent> RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>() where TComponent : IComponent
=> RenderComponentAsync<TComponent>(ParameterView.Empty);

/// <summary>
/// Adds an instance of the specified component and instructs it to render, waiting
/// for the component hierarchy to complete asynchronous tasks such as loading.
/// </summary>
/// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
/// <returns>A task that completes with <see cref="HtmlRootComponent"/> once the component hierarchy has completed any asynchronous tasks such as loading.</returns>
public Task<HtmlRootComponent> RenderComponentAsync(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
=> RenderComponentAsync(componentType, ParameterView.Empty);

/// <summary>
/// Adds an instance of the specified component and instructs it to render, waiting
/// for the component hierarchy to complete asynchronous tasks such as loading.
/// </summary>
/// <typeparam name="TComponent">The component type.</typeparam>
/// <param name="parameters">Parameters for the component.</param>
/// <returns>A task that completes with <see cref="HtmlRootComponent"/> once the component hierarchy has completed any asynchronous tasks such as loading.</returns>
public Task<HtmlRootComponent> RenderComponentAsync<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(
ParameterView parameters) where TComponent : IComponent
=> RenderComponentAsync(typeof(TComponent), parameters);

/// <summary>
/// Adds an instance of the specified component and instructs it to render, waiting
/// for the component hierarchy to complete asynchronous tasks such as loading.
/// </summary>
/// <param name="componentType">The component type. This must implement <see cref="IComponent"/>.</param>
/// <param name="parameters">Parameters for the component.</param>
/// <returns>A task that completes with <see cref="HtmlRootComponent"/> once the component hierarchy has completed any asynchronous tasks such as loading.</returns>
public async Task<HtmlRootComponent> RenderComponentAsync(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType,
ParameterView parameters)
{
var content = BeginRenderingComponent(componentType, parameters);
await content.QuiescenceTask;
return content;
}
}
}
37 changes: 37 additions & 0 deletions src/BlazeKit.Static/BKStaticHtmlRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web.HtmlRendering;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure;

namespace BlazeKit.Static;

/// <summary>
/// A <see cref="Renderer"/> subclass that is intended for static HTML rendering. Application
/// developers should not normally use this class directly. Instead, use
/// <see cref="HtmlRenderer"/> for a more convenient API.
/// </summary>
#pragma warning disable BL0006 // Do not use RenderTree types
public partial class BKStaticHtmlRenderer : StaticHtmlRenderer
{
public BKStaticHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : base(serviceProvider,loggerFactory)
{

}
protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers((DynamicallyAccessedMemberTypes)(-1))] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode renderMode)
=> renderMode switch
{
InteractiveWebAssemblyRenderMode => componentActivator.CreateInstance(componentType),
_ => throw new NotSupportedException($"Cannot create a component of type '{componentType}' because its render mode '{renderMode}' is not supported by BlazeKit rendering."),
};
}
#pragma warning restore BL0006 // Do not use RenderTree types
3 changes: 2 additions & 1 deletion src/BlazeKit.Static/BlazorRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Html;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;

namespace BlazeKit.Static;
Expand Down Expand Up @@ -37,7 +38,7 @@ public BlazorRenderer(Func<HtmlRenderer> htmlRenderer, IServiceProvider serviceP

public override Dispatcher Dispatcher => this.htmlRenderer.Value.Dispatcher;


/// <summary>
/// Renders a component T which doesn't require any parameters
/// </summary>
Expand Down
4 changes: 3 additions & 1 deletion src/BlazeKit.Static/StaticSiteGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ public Task Build(Type rootComponent, IServiceCollection serviceCollection)
serviceCollection.AddSingleton<INavigationInterception>(new FkNavigationInterception());
serviceCollection.AddSingleton<IScrollToLocationHash>(new FkScrollToLocationHash());
serviceCollection.AddSingleton<IErrorBoundaryLogger>(new StaticErrorBoundaryLogger());
//serviceCollection.AddScoped<DataHydrationContext>();
foreach (var route in this.routes.Value)
{
Expand Down Expand Up @@ -129,7 +131,7 @@ public Task Build(Type rootComponent, IServiceCollection serviceCollection)
foreach(var item in contentCollection.Items)
{
var routeWithParams = contentCollection.Route(item);
Console.WriteLine($"Building route: {routeWithParams}");
Console.WriteLine($"Building route for Content Collection '{contentCollection.Name}': {routeWithParams}");
Prerender(routeWithParams, routeManager, rootComponent, serviceCollection);
}
Expand Down
2 changes: 1 addition & 1 deletion src/BlazeKit.Web/BlazeKit.Web.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
Expand Down
Loading

0 comments on commit fa76c6b

Please sign in to comment.