Reputation: 841
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:
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
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.
<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;
}
}
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