0

Is there a way to two-way bind an attribute of a custom HTML element in Blazor? I have a custom element with a value attribute linked to a text input internally that I'd like to bind the value of.

<template id="text">
    <div style="height:40px; width:100%; display:flex; flex-direction:column; justify-content:center; align-items:flex-start; padding:10px">
        <label for="textline" style="padding-left:5px"><slot name="label"></slot></label>
        <input type="text" id="textline" style="border:solid 2px lightgrey; border-radius:5px; padding:5px; font-family:Montserrat; height:100%; font-size:15px; width:100%; box-sizing: border-box;" />
    </div>
</template>

Here's the JS defining the custom element:

customElements.define(
"bonsai-text-box",
class extends HTMLElement {
    static observedAttributes = ["label", "value"];
    constructor() {
        super();
    }

    async connectedCallback() {
        let html = await fetch("https://nexusdockapidev.ctdi.com/GetBonsaiHTML");
        let json = await html.json();
        let div = document.createElement('div');
        div.innerHTML = json.html;
        this.appendChild(div);
        let template = document.getElementById("text");
        let templateContent = template.content;

        const label = this.getAttribute("label");
        const labelSpan = document.createElement("span");
        labelSpan.setAttribute("slot", "label");
        labelSpan.setAttribute("id", "labelspan");
        labelSpan.innerHTML = label;

        const shadowRoot = this.attachShadow({ mode: "open" });

        let css = await fetch("https://nexusdockapidev.ctdi.com/GetBonsaiCSS");
        let cssjson = await css.json();
        let style = document.createElement('style');
        style.innerHTML = cssjson.css;
        shadowRoot.appendChild(style);

        shadowRoot.appendChild(templateContent.cloneNode(true));
        this.appendChild(labelSpan);

        this.inputNode = this.shadowRoot.querySelector('input')
        this.inputNode.addEventListener("change", (e) => {
            this.setAttribute("value", e.target.value);
        });
    }

    get value () {
      return this.inputNode.value
    }
  
    set value (newValue) {
      this.inputNode.value = newValue
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === "label") {
            const oldlabelspan = this.querySelector("#labelspan");
            if (oldlabelspan) {
                oldlabelspan.parentNode.removeChild(oldlabelspan);
                const labelSpan = document.createElement("span");
                labelSpan.setAttribute("slot", "label");
                labelSpan.setAttribute("id", "labelspan");
                labelSpan.innerHTML = newValue;
                this.appendChild(labelSpan);
                this.dispatchEvent(new Event("change"));
            }
        }
        else if (name === 'value') {
            this.inputNode.value = newValue;
        }
    }
},
);

I've tried @bind and @bind-value and neither seem to work how I expect.

1 Answer 1

0

I don't understand all your design choices. I presume the value attribute on the Web Component is what you are after.

Were you caught in the change Event? Which only triggers on Enter
onkeyup (addEventListener only required for multiple listeners) will catch every keystroke

I have refactored your code... just a bit... it's a hobby :-)

Also available as: https://jsfiddle.net/WebComponents/o35zmcdw/

<template id="BONSAI-TEXT-BOX">
  <style>
    .inputdiv {
      height: 40px;
      width: 100%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: flex-start;
      padding: 10px
    }
    .inputdiv label {
      padding-left: 5px;
    }
    .inputdiv input {
      border: solid 2px lightgrey;
      border-radius: 5px;
      padding: 5px;
      font: 15px Montserrat;
      height: 100%;
      width: 100%;
      box-sizing: border-box;
    }
  </style>
  <div class="inputdiv">
    Template injected
    <label for="textline">
      <slot name="label"></slot>
    </label>
    <input type="text" id="textline" />
  </div>
</template>

<script>
  customElements.define(
    "bonsai-text-box",
    class extends HTMLElement {
      static observedAttributes = ["label", "value"];
      constructor() {
        const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props);
        super()
          .attachShadow({
            mode: "open"
          })
          .append(
            this.shadowstyle = createElement("style", { // can't be .style, that is a standard property!!
              innerHTML: ":host{display:block}" +
                ":host([value]){background:green}" +
                ":host(:not([value])){background:lightcoral}" +
                "::slotted(*){background:blue; color:gold}"
            }),
            this.div = createElement("div", {
              innerHTML: `first DIV in shadowDOM`
            }),
            this.labelSpan = createElement("span", {
              slot: "label",
              //id: "labelspan",
              innerHTML: "label"
            })
          )
      }
      async connectedCallback() {
        console.log("connectedCallback");
        //let html = await fetch("https://nexusdockapidev.ctdi.com/GetBonsaiHTML");
        //let json = await html.json();
        this.div.innerHTML = "DIV innerHTML set after fetch call"; //json.html;
        this.labelSpan.innerHTML = this.getAttribute("label");
        //let css = await fetch("https://nexusdockapidev.ctdi.com/GetBonsaiCSS");
        //let cssjson = await css.json();
        this.style.innerHTML = "div{background:green}"; //cssjson.css;
        this.shadowRoot.append(document.getElementById(this.nodeName).content.cloneNode(true));
        this.inputNode = this.shadowRoot.querySelector("#textline");
        this.onkeyup = (evt) => {
          console.log("newvalue:", evt.target.value);
          this.value = evt.target.value;
        }
      }
      get value() {
        return this.inputNode.value
      }
      set value(newValue) {
        this.setAttribute("value", newValue);
        if (!newValue) this.removeAttribute("value");
        this.inputNode.value = newValue;
      }
      attributeChangedCallback(name, oldValue, newValue) {
        console.log("attributeChanged", name, oldValue, newValue, this.isConnected);
        return;
        if (name === "label") {
          this.labelSpan.innerHTML = newValue;
          this.dispatchEvent(new Event("change"));
        } else if (name === "value") {
          this.value = newValue; // use the Setter!
        }
      }
    });
</script>

<bonsai-text-box label="ATTR LABEL">
  <span slot="label">SLOTTED LABEL!</span>
</bonsai-text-box>

click Run Code snippet, after that click full-page on the right

Sign up to request clarification or add additional context in comments.

1 Comment

I appreciate the feedback, but I'm still missing how to bind the value field in Blazor. Every variant of @bind I've tried has either thrown and error or just not worked.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.