curious coder
curious coder

Reputation: 841

Position Blazor component inside user content

I have a requirement to dynamically put a Blazor component inside user-provided content. Essentially, the component is supposed to extend user-provided markup with some UI elements.

Let's say the user provides some content that can have a "greeting-container" element in it and the component should insert a greeting button inside that element.

My current solution is to call a JavaScript function to move the DOM element in OnAfterRenderAsync (full code below). It seems to work fine, but manipulating DOM elements seems to be discouraged in Blazor since it can affect the diffing algorithm. So I have a couple of questions on this:

  1. How bad is it to move DOM elements like this? Does it cause performance issues, functional issues or some undefined behavior?
  2. Is there a better way to achieve the same result without using JavaScript? I was considering using the RenderTreeBuilder for this, but it seems like it might not be designed for this purpose since it's recommended to use hardcoded sequence numbers, which doesn't seem possible when dealing with dynamic content not known at compilation time.

Current solution code:

Greeter.razor

@page "/greeter"

@inject IJSRuntime JSRuntime;

<div>
    @((MarkupString)UserContentMarkup)
    <div id="greeting">
        <button @onclick="ToggleGreeting">Toggle greeting</button>
        @if (isGreetingVisible) {
            <p>Hello, @Name!</p>
        }
    </div>
</div>

@code {
    [Parameter]
    public string UserContentMarkup { get; set; }

    [Parameter]
    public string Name { get; set; }

    private bool isGreetingVisible;

    private void ToggleGreeting()
    {
        isGreetingVisible = !isGreetingVisible;
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await JSRuntime.InvokeVoidAsync("moveGreetingToContainer");
    }
}

_Host.cshtml

window.moveGreetingToContainer = () => {
    var greeting = document.getElementById("greeting");

    var container = document.getElementById("greeting-container");

    container.appendChild(greeting);
}

UserContentTest.razor

@page "/userContentTest"

@inject IJSRuntime JSRuntime;

<h3>Testing user content</h3>

<Greeter UserContentMarkup=@userContentMarkup Name="John"></Greeter>

@code {
    private string userContentMarkup = "Some <b>HTML</b> text followed by greeting <div id='greeting-container'></div> and <i>more</i> text";
}

Expected result (after clicking "Toggle greeting"):

<div>
    Some <b>HTML</b> text followed by greeting
    <div id="greeting-container">
        <div id="greeting">            
            <button>Toggle greeting</button>            
            <p>Hello, John!</p>            
        </div>
    </div> and <i>more</i> text    
</div>

Upvotes: 2

Views: 2330

Answers (1)

Mister Magoo
Mister Magoo

Reputation: 9019

Great question - and yes, using JS to move the dom elements is very bad as Blazor doesn't see the change you made to the dom.

What you can do is switch over to using a RenderFragment and more specifically RenderFragment<RenderFragment> which is markup that will be supplied with more markup as a parameter.

On the second line, I am invoking the UserContentMarkup method (which is a RenderFragment<RenderFragment>) and passing in the <div id=greeting> content as the context parameter.

Note: It is wrapped in a <text> element which is actually a way to embed HTML in C# in a Razor file. It does not render a <text> element to the page.

Greeter.razor

<div>
    @UserContentMarkup(
    @<text>
        <div id="greeting">
            <button @onclick="ToggleGreeting">Toggle greeting</button>
            @if (isGreetingVisible) {
                <p>Hello, @Name!</p>
            }
        </div>
    </text>
    )
</div>
@code {
    [Parameter]
    public RenderFragment<RenderFragment> UserContentMarkup { get; set; }

    [Parameter]
    public string Name { get; set; }

    private bool isGreetingVisible;

    private void ToggleGreeting()
    {
        isGreetingVisible = !isGreetingVisible;
    }
}

UserContentTest.razor

Here you can see two ways to consume Greeter - using markup in the page, or using a code method.

<h3>Testing user content</h3>

@* Using markup to supply user content - @context is where the button goes *@
<Greeter Name="John">
    <UserContentMarkup>
        Some <b>HTML</b> text followed by greeting 
        <div id='greeting-container'>@context</div> and <i>more</i> text
    </UserContentMarkup>
</Greeter>

@* Using a method to supply the user content - @context is where the button goes *@
<Greeter Name="John" UserContentMarkup=@userContent />

This code method can be confusing - it is a RenderFragment<RenderFragment> which means it has to be a method that accepts a RenderFragment as its only parameter, and returns a RenderFragment - the RenderFragment being returned in this case is markup wrapped in <text> to make it clear it is markup.

@code
{
    RenderFragment<RenderFragment> userContent 
        => context => @<text>Some stuff @context more stuff</text>;
}

Try it out here : https://blazorrepl.com/repl/QuPPaMEu34yA5KSl40

Upvotes: 2

Related Questions