Access class function in Web Component from inline Element
Asked Answered
D

5

6

I want to execute a defined class function from an Element inside my Web Component:

customElements.define('first-component', class FirstComponent extends HTMLElement {
    constructor() {
        super();
     }

     log() {
        console.log('Well Done!')
     }

     connectedCallback() {
        this.innerHTML = '<button onclick="log()">Do it</button>'
     }
});

State right now: ReferenceError: log is not defined

Drynurse answered 28/12, 2018 at 19:43 Comment(0)
M
10

With parentElement, or closest()

In order to call the log() method of the custom element, you'll have to get a reference on it.

In your example, the custom element is the parent element of the <button> element, so you should call the parentElement property of the button as already stated by @Smankusors:

<button onclick="this.parentElement.log()>Do it</button>

With getRootNode()

Alternately, in a more complex DOM tree, and if a Shadow DOM is used, you can use getRootNode() combined with host to get the custom element reference.

customElements.define('first-component', class FirstComponent extends HTMLElement {
     log() {
        console.log('Well Done!')
     }

     connectedCallback() {
        this.attachShadow({mode: 'open'})
            .innerHTML = '<button onclick="this.getRootNode().host.log()">Do it</button>'
     }
})
<first-component></first-component>

With a unique identifier

You can also call the custom element by its id property (if it has one) :

customElements.define('first-component', class FirstComponent extends HTMLElement {
     log() {
        console.log('Well Done!')
     }

     connectedCallback() {
        if (!this.id)
            this.id = "_id"
        this.innerHTML = `<button onclick="${this.id}.log()">Do it</button>`
     }
})
<first-component></first-component>

With handleEvent()

For security reasons, you can avoid inline script and implement the handleEvent() method, then call inside it a specific method depending on some criterions :

customElements.define('first-component', class FirstComponent extends HTMLElement {
    log() {
        console.log('Well Done!')
    }
     
    handleEvent(ev) {
        if (ev.target.innerText == 'Do it')
            this.log()
    }

    connectedCallback() {
        this.innerHTML = '<button>Do it</button>'
        this.addEventListener('click', this)
    }
})
<first-component></first-component>
Manvil answered 28/12, 2018 at 21:15 Comment(4)
Could you elaborate what the security reasons are?Need
@Need search for "javascript injection" or read for example glebbahmutov.com/blog/disable-inline-javascript-for-securityManvil
Is there an approach similar to getRootNode() for web components that don't use Shadow DOM?Need
@Need yes: "this.closest('first-component').log()" should do the trickManvil
T
2

That shouldn't be log(), but this.log(), because that log function scope is only that element, not in window scope, so your code should be

customElements.define('first-component', class FirstComponent extends HTMLElement {
    constructor() {
        super();
     }

     log() {
        console.log('Well Done!')
     }

     connectedCallback()
        this.innerHTML = '<button onclick="this.parentElement.log()">Do it</button>'
     }
});

-- EDIT -- Sorry, my mistake, I just saw that you added button inside custom element, well... It should be this.parentElement.log() if you still want to prefer inline

Tradein answered 28/12, 2018 at 20:4 Comment(3)
Also tried that before. The Problem is that this will bind to the button and also fail.Drynurse
Did you even try this? This does not work, this is still the window scope hereChargeable
oops, sorry, well you can try this.parentElement.log()Tradein
C
0

Since the DOM and its elements does not have any knowledge of the scope it lives in, just setting the value of the innerHTML won't work since log does not exist on window which is the DOM scope. Hence this, it's best practice to create the element and append it to the Shadow Dom of the custom element and at the same time add the eventListener to the button.

customElements.define('first-component', class FirstComponent extends HTMLElement {
    constructor() {
        super();
     }

     log() {
        console.log('Well Done!')
     }

     connectedCallback() { // This parentheses was also missing
         var shadow = this.attachShadow({mode: 'open'});
         const button = document.createElement("button");
         button.textContent = 'Do it!';
         button.addEventListener('click', () => this.log());
         shadow.appendChild(button);
     }
});
<first-component id="component"></first-component>
Chargeable answered 28/12, 2018 at 20:8 Comment(2)
It works but I dislike this solution, haha ! Thank youDrynurse
Not sure if there is any other solutions tbh @Drynurse . I guess you can have a peek at #37866737 to see if that helps, I know it's pointing to react but it more or less nativeChargeable
P
0

You should - for many reasons - stop using inline event listeners. Instead, use addEventListener - in this case in the connectedCallback.

customElements.define('first-element', class FirstElement extends HTMLElement {
    constructor() {
        super();
     }

     log() {
        console.log('Well Done!')
     }

     connectedCallback() {
        const btn = document.createElement('button');
        btn.textContent = 'Do it!';
        btn.type = 'button'; // otherwise it's type=submit
        btn.addEventListener('click', this.log);
        this.appendChild(btn);
     }
});
<first-element></first-element>
Pestana answered 28/12, 2018 at 20:14 Comment(3)
Why is using inline event listeners bad? I think its more readable and many Frameworks follow this patternDrynurse
Those frameworks parse the HTML and replace the inline event listeners with addEventListener internally. With native web components, you're in a different context. Inline event listeners - aside from being bad for the same reasons inline styles are bad - also do not comply with content security policy which cannot be applied on code using inline event listeners.Pestana
See: https://mcmap.net/q/53240/-addeventlistener-vs-onclickPortia
D
0

Here's my approach using template literals and window properties.

By using template literals you get the scope you want, but the onclick event still needs access to your method. I guess using window is the only way to share something from outer scope for the event in this case. Since the event takes in string type, I can just make it call window and some unique property name where the shared method lives in.

customElements.define('first-component', class FirstComponent extends HTMLElement {
   log() {
      console.log('Well Done!')
   }

   bind(callback) {
     if (!callback) return '';
     const name = 'generate_random_name';
     window[name] = callback;
     return `window.${name}();`;
   }

   connectedCallback() {
      this.innerHTML = `<button onclick=${this.bind(() => this.log())}>Do it</button>`
   }
});
<first-component></first-component>

To go a little bit further, we can also get rid of the bind method by using a tagged template literal. This allows us to manipulate the template string and it's dynamic values before they get rendered. Naming the tag to "html" is useful because it enables html's syntax highlighting.

function html(strings, ...values) {
  let str = '';
  strings.forEach((string, index) => {
    const value = values[index];
    if (typeof value === 'function') {
      const callback = value;
      const name = 'generate_random_name';
      window[name] = callback;
      str += string + `window.${name}();`;
    } else {
      str += string
    }
  });
  return str;
}

customElements.define('first-component', class FirstComponent extends HTMLElement {
  log() {
    console.log('Well Done!')
  }

  connectedCallback() {
    this.innerHTML = html`<button onclick=${() => this.log()}>Do it</button>`
  }
});
<first-component></first-component>
Diversiform answered 17/10, 2021 at 18:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.