dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

An extended HTML custom element is not activated when anything in that subtree is Blazor-controlled #36269

Open Tragetaschen opened 3 years ago

Tragetaschen commented 3 years ago

Describe the bug

I have defined a HTML custom element that extends a built-in element. I can use this on pages, but only when the element or its children do not come in contact with Blazor in any way. This includes event handlers, interpolation (Current count: @currentCount) or referencing other components.

To Reproduce

In the index.html of a new Blazor WASM project, I add

<script>
    window.customElements.define('my-custom-element', class MyCustomElement extends HTMLDivElement {
        constructor() {
            super();
            console.log('instantiated');
        }
    }, { extends: 'div' });
</script>

to define a custom element that extends a <div>.

In the Counter.razor, I can reference that component

<div is="my-custom-element">Current count:</div>

and the log message appears in the console.

However, as soon as I do something Blazory, the custom element is not instantiated anymore. All the following make the instantiation not happen:

<div is="my-custom-element">Current count: @currentCount</div>
<div is="my-custom-element" @onclick="IncrementCount">Current count:</div>
<div is="my-custom-element">Current count: <FetchData/></div>

Further technical details

ASP.NET Core version 6 Preview 7

`dotnet --info` ``` .NET SDK (reflecting any global.json): Version: 6.0.100-preview.7.21379.14 Commit: 22d70b47bc Runtime Environment: OS Name: Windows OS Version: 10.0.19042 OS Platform: Windows RID: win10-x64 Base Path: C:\Program Files\dotnet\sdk\6.0.100-preview.7.21379.14\ Host (useful for support): Version: 6.0.0-preview.7.21377.19 Commit: 91ba01788d .NET SDKs installed: 5.0.400 [C:\Program Files\dotnet\sdk] 6.0.100-preview.3.21202.5 [C:\Program Files\dotnet\sdk] 6.0.100-preview.7.21379.14 [C:\Program Files\dotnet\sdk] .NET runtimes installed: Microsoft.AspNetCore.All 2.1.29 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All] Microsoft.AspNetCore.App 2.1.29 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 3.1.18 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 5.0.9 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.0-preview.3.21201.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.AspNetCore.App 6.0.0-preview.7.21378.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App] Microsoft.NETCore.App 2.1.29 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 3.1.18 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 5.0.9 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.0-preview.3.21201.4 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.NETCore.App 6.0.0-preview.7.21377.19 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App] Microsoft.WindowsDesktop.App 3.1.18 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 5.0.9 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.0-preview.3.21201.3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] Microsoft.WindowsDesktop.App 6.0.0-preview.7.21378.9 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App] ```
SteveSandersonMS commented 3 years ago

When Blazor constructs DOM content, there are two main ways it does it:

In the first case, the browser automatically honours your is attribute. In the second case, it doesn't. The fact that setting an is attribute doesn't have the same effect as if the attribute was in some static markup is a pretty awkward quirk of the DOM APIs that Blazor doesn't currently do anything to work around.

The way we'd need to solve this is by walking through the list of attributes before we instantiate the element, discovering any is attribute, and passing that value to the initial document.createElement call, as in this example. The drawback to this is that it could adversely affect performance for all DOM construction, not just in the case where people have an is attribute. Maybe we might be able to do something fancy at compile-time to capture the information so we don't have to walk the attribute list at runtime, but I haven't investigated that.

We can look into addressing this, but for now, the most direct workaround (if it's suitable for your scenario) would be to use custom elements with custom names, which always work, instead of using the is attribute. Sorry for the inconvenience.

ghost commented 2 years ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

ghost commented 10 months ago

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

thargol1 commented 5 months ago

I'm having the same issue in blazor 8. I provide my code for a future (soon I hope) test case:

The component:

class FilteredInput extends HTMLInputElement {
    static observedAttributes = ["allowedcharacters"];
    #allowedCharacters;
    connectedCallback() {
        console.log(`connectedCallback`);
        console.log(this);
        let me = this;
        me.addEventListener("beforeinput", (e) => me.#handleEvent(e, e.data, me));
        me.addEventListener("paste", (e) => me.#handleEvent(e, e.clipboardData.getData("text"), me));
        me.addEventListener("input", () => me.#repairValue());
        me.#repairValue();
    }
    #repairValue() {
        let me = this;
        let filteredValue = me.#convertToValidInput(me.value, me.#allowedCharacters);
        if (filteredValue !== me.value) {
            me.value = filteredValue;
        }
    }
    attributeChangedCallback(attrName, oldValue, newValue) {
        console.log(`attributeChangedCallback ${attrName}=${newValue}`);
        this.#allowedCharacters = newValue;
        this.#repairValue();
    }
    #handleEvent(event, data, me) {
        console.log(event);
        let newData = me.#convertToValidInput(data, me.#allowedCharacters);
        if (newData !== data) {
            event.preventDefault();
            if (newData) {
                me.setRangeText(newData);
                me.dispatchEvent(new InputEvent("input", {
                    bubbles: true,
                    cancelable: true,
                    data: newData,
                    inputType: event.type
                }));
            }
        }
    }
    #convertToValidInput(text, allowedCharacters) {
        if (!allowedCharacters || !text) {
            return text;
        }
        let result = "";
        for (let char of text) {
            if (!allowedCharacters.includes(char)) {
                let lc = char.toLowerCase(), uc = char.toUpperCase();
                if (char >= "A" && char <= "Z" && allowedCharacters.includes(lc)) {
                    char = lc;
                }
                else if (char >= "a" && char <= "z" && allowedCharacters.includes(uc)) {
                    char = uc;
                }
                else if ((char === ",") && allowedCharacters.includes(".")) {
                    char = ".";
                }
                else if ((char === ".") && allowedCharacters.includes(",")) {
                    char = ",";
                }
                else
                    char = "";
            }
            result += char;
        }
        return result;
    }
}
customElements.define('filtered-input', FilteredInput, { extends: "input" });

The blazor page:

@page "/"
<PageTitle>Home</PageTitle>
<h1>@x</h1>
<br />
A:
<input is="filtered-input" allowedcharacters="0123456789" @bind="x" id="a" />
<br />
B:
<input is="filtered-input" allowedcharacters="0123456789" id="b" />
<br />

@code {
    private string? x = "test";
}

The b-component is working fine, the a-component isn't.

This component is, when minimized and gzipped, only about 500 bytes. If I implement a SSR version in pure blazor C#, I get multiple KB's of websocket traffic.