javascriptc#blazorblazor-webassemblysceditor

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


I want to use SCEditor in my Blazor page.

For example I create a new Blazor WASM project and I did these steps:

  1. According to documentation I add this codes and references to index.html:

    window.InitEditor = () => {
        var textarea = document.getElementById('myTextArea');
        sceditor.create(textarea, {
            format: 'bbcode',
            style: 'https://cdn.jsdelivr.net/npm/sceditor@3/minified/themes/content/default.min.css'
        });
    };
    
  2. Add a textarea in counter page:

    <textarea id="myTextArea" style="width:100%; height:200px;"></textarea>
    
  3. add follow code to code section of counter page:

    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        JsRuntime.InvokeVoidAsync("InitEditor");
        return base.OnAfterRenderAsync(firstRender);
    }
    

Now I run the project and I see this editor:

enter image description here

then I click on fetch data menu and I see this:

enter image description here

Surprisingly, editor has shown in fetch data page. if I click on counter page again I see this:

enter image description here

There are 2 Editors O-O. and if I click on this menus then editors are increasing...

I changed the code this way:

protected override Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
        JsRuntime.InvokeVoidAsync("InitEditor");
    return base.OnAfterRenderAsync(firstRender);
}

the editor is shown once in counter page and fetch data page. Again I don't have any textarea in fetch data page.

enter image description here

enter image description here

How can I solve this problem?


Solution

  • 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.

    Ideally, you would encapsulate everything SCEditor-related into a dedicated component that deals only with SCEditor, while the rest of your app would use that component like any other Blazor component.