Skip to content

Instantly share code, notes, and snippets.

@teneko
Last active November 28, 2024 09:37
Show Gist options
  • Save teneko/24a62f81eedabeb2faf7884efdd44663 to your computer and use it in GitHub Desktop.
Save teneko/24a62f81eedabeb2faf7884efdd44663 to your computer and use it in GitHub Desktop.
C# Blazor Base Component or Super Component as High-Order-Component or Higher-Level-Component
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
/// <summary>
/// A helper to implement the concept of a high-order component.
/// Imagine you have a multiple concrete page content components but now you want to wrap each of them with the SAME frame.
/// The current blazor workflow would be to manually wrap every concrete page content component by the page frame component.
/// Wouldn't it be cool if every concrete page content component would inherit from page frame component and wrap its
/// derived component with the desired page frame? By declaring HighOrderComponent in the base component and using it
/// in the parent component you can use the concept of a high-order component.
/// </summary>
/// <param name="manuallyAllowSubComponentRendering">
/// Enable it for deubg pruposes.
/// Call <see cref="AllowSuperFragmentRendering" /> in <see cref="ComponentBase.OnAfterRender(bool)"/> by yourself.
/// </param>
public class HighOrderComponent(bool manuallyAllowSubComponentRendering = false)
{
private RenderFragment? _self;
private RenderFragment? _sub;
private bool _hasSelfComponentBeenSetExplictly;
private bool _allowSubComponentRendering = false;
public RenderFragment Self {
get => _self ?? throw new InvalidOperationException("HOC is in invalid state: no self component has been set.");
set {
if (value is null) {
throw new ArgumentNullException("Self component must not be null");
}
if (_self is not null) {
throw new InvalidOperationException("Self component must be initialized once.");
}
_self = value;
_hasSelfComponentBeenSetExplictly = true;
}
}
public bool BuildRenderTree(RenderFragment sub, RenderFragment super, RenderTreeBuilder builder)
{
ArgumentNullException.ThrowIfNull(sub);
ArgumentNullException.ThrowIfNull(super);
ArgumentNullException.ThrowIfNull(builder);
if (sub == super) {
throw new ArgumentException("Expected the owning component's render fragment to be derived from the base component's render fragment, so they do not result into reference-equality.");
}
if (_sub is null) {
_sub = sub;
} else if (!_hasSelfComponentBeenSetExplictly && _sub != sub) {
throw new InvalidOperationException("Super component render fragment reference identity changed.");
}
if (_allowSubComponentRendering) {
return true;
}
_allowSubComponentRendering = true;
super(builder);
if (!manuallyAllowSubComponentRendering) {
_allowSubComponentRendering = false;
}
return false;
}
public void AllowSuperFragmentRendering()
{
if (!manuallyAllowSubComponentRendering) {
throw new InvalidOperationException($"{nameof(HighOrderComponent)} was not configured to allow super fragment rendering manually.");
}
_allowSubComponentRendering = true;
}
public static implicit operator RenderFragment(HighOrderComponent hoc) => hoc._sub ?? throw new InvalidOperationException("HOC is in invalid state: no sub component has been set.");
}
@teneko
Copy link
Author

teneko commented Nov 22, 2024

Example how to chain two classes:

Template.razor

<div>
    template
    @HOC
</div>

@code {
    protected HighOrderComponent HOC = new();
}

Template2.razor

@inherits Template

@if (!base.HOC.BuildRenderTree(BuildRenderTree, base.BuildRenderTree, __builder))
{
    return;
}

<div>
    template#2
</div>

Example how to chain three and more classes

Template.razor

<div>
    template
    @HOC
</div>

@code {
    protected HighOrderComponent HOC = new();
}

Template2.razor

@inherits Template

@if (!base.HOC.BuildRenderTree(HOC.Self, base.BuildRenderTree, __builder))
{
    return;
}

<div>
    template#2
    @HOC
</div>

@code {
    protected new HighOrderComponent HOC = new();

    public Template2() => base.HOC.Self = base.BuildRenderTree;
}

Template3.razor

@inherits Template2

@if (!base.HOC.BuildRenderTree(BuildRenderTree, base.BuildRenderTree, __builder))
{
    return;
}

<div>
    template#3
</div>

@code {
    public Template3() => base.HOC.Self = base.BuildRenderTree;
}

@teneko
Copy link
Author

teneko commented Nov 22, 2024

Problems I faced with this approach: When using a base component as HOC, than you must use ::deep to also target the derived classes and you should not use components providing cascading value(s) (e.g. EditForm) inside HOCs (in derived components should be okay, but not if the derived components is a HOC for another derived component), because it leads to a render loop. I couldn't find out a solution to it yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment