When I add SCEditor to my Blazor project, the editor keeps appearing in strange places, sometimes in multiple copies. How do I fix this?

Solution 1:

Blazor documentation warns:

Only mutate the Document Object Model (DOM) with JavaScript (JS) when the object doesn't interact with Blazor. Blazor maintains representations of the DOM and interacts directly with DOM objects. If an element rendered by Blazor is modified externally using JS directly or via JS Interop, the DOM may no longer match Blazor's internal representation, which can result in undefined behavior. Undefined behavior may merely interfere with the presentation of elements or their functions but may also introduce security risks to the app or server.

This guidance not only applies to your own JS interop code but also to any JS libraries that the app uses, including anything provided by a third-party framework, such as Bootstrap JS and jQuery.

SCEditor is exactly one of those DOM-mutating libraries, and the effects of failure to observe that guidance you can see for yourself. (The ‘security risks’ bit is rather nonsensical: if your app can be made insecure merely by modifying client-side code, then it wasn’t very secure to begin with. But it’s otherwise good advice.)

Blazor does provide some interoperability with external DOM mutation in the form of element references. The documentation again warns:

Only use an element reference to mutate the contents of an empty element that doesn't interact with Blazor. This scenario is useful when a third-party API supplies content to the element. Because Blazor doesn't interact with the element, there's no possibility of a conflict between Blazor's representation of the element and the Document Object Model (DOM).

Heeding that warning, you should probably write something like below (not tested). In the component file (.razor):

<div @ref="sceditorContainer"></div>

@inject IJSRuntime js

@code {

private ElementReference sceditorContainer;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    await base.OnAfterRenderAsync(firstRender);
    if (firstRender)
    {
        await js.InvokeVoidAsync("initEditor", sceditorContainer);
    }
}

}

And in JavaScript:

function initEditor(container) {
    const textarea = document.createElement('textarea');
    container.appendChild(textarea);
    sceditor.create(textarea, {
        format: 'bbcode',
        style: 'https://cdn.jsdelivr.net/npm/sceditor@3/minified/themes/content/default.min.css'
    });
}

If the SCEditor library is sufficiently well-behaved, it should only modify the DOM tree at most at the level of the parent of the textarea node you give it. You may think it would be enough to place a <textarea> in your component markup and capture a reference to that, but as it happens SCEditor adds siblings to that node, which may keep messing up rendering. For that reason, it is safer to put everything in an initially-empty wrapper element, which can act as a sandbox in which SCEditor has free rein.