Search code examples
javascripteventsonclickbindcustom-element

JS > bind HTML onclick event to a Custom Element v1 object


TL;DR

How can I bind the callback of an inline onclick-event of a div element to the Custom Element, in which the div is placed? I want to use onclick="foo('bar')" instead of onclick="this.foo('bar')".

Long version:

Having appended an Custom Element with a clickable div to DOM as follows:

<my-element>
    <!-- 1. works  -->
    <div onclick="this.foo('bar')">click me</div>

    <!-- 2. not working -->
    <div onclick="foo('bar')">click me</div>

    <!-- edit: 3. playground (works) -->
    <div onclick="me.foo('bar')">click me</div>
</my-element>

... now I want to bind the foo()-function to my Custom Element (<my-element>).

1st solution: Here onclick calls a foo()-function on this (this.foo()), where this later gets bound to my Custom Element (see following code).

2nd solution: Here I want to omit this. and again bind it to my Custom Element. But: while the binding works in the above solution (1.), it does not without a prepended this. - that's my problem.

edited 3rd solution: Uses a Proxy to project the function call from me to this Custom Element. It doesn't solve the problem, but at least I tried.

So the problem is: How to get solution 2 working?

My Custom Element v1 class - including some code I tried:

class MyElement extends HTMLElement
{
    constructor()
    {
        super()
    }

    connectedCallback()
    {
        var self = this

        this.addEventListener('click', this.handleClick, true)

        // Proxy for solution 3
        window.me = new Proxy({}, 
        {
            get: function(target, prop) 
            {
                return self[prop]
            }
        })
    }

    handleClick(ev)
    {
        console.log(ev.target.onclick)
            // returns: ƒ onclick(event) { foo("bar") }
            // > is typeof function

        // provides the binding to "this" for solution 1
        ev.target.onclick = ev.target.onclick.bind(this)

        // call
        ev.target.onclick()
            // works on first solution
            // throws error in 2nd solution: foo is not defined
    }

    foo(str)
    {
        console.log(str)
    }
}

customElements.define('my-element, MyElement)

Some explanations:

  • Using addEventListener and setting its 3rd parameter (useCapture) to true, I capture the event before getting executed.

  • I'm able to log the foo()-function to the console. It is encapsulated in a callable function, which seems to be the reason, why I can't bind my context to the foo()-function itself.

  • Calling the function via ev.target.onclick() throws an error, as in the given context (window) no foo()-function exists.

Some thoughts:

  • React does something similar (I haven't used React yet), but I can't see how to transfer their solution, as they eventually somehow preprocess their onclick events (with eval?). That might be a reason why their syntax is onclick={foo} instead of onclick="foo()". I also don't know, if passing parameters is possible in React. See: https://facebook.github.io/react/docs/handling-events.html

  • Referring to that, it might be a problem, that in my solution the foo() function is probably already somehow called inside the window context, what means, that it's context is already set and can't be changed anymore...? Anyways, trying to set onclick="foo" without brackets and calling it later doesn't work either.

Good read to this topic:

http://reactkungfu.com/2015/07/why-and-how-to-bind-methods-in-your-react-component-classes/

So, I don't know if there is a solution at all. Ideas or explanations are welcome anyways!

EDIT 2: Fixed.

It also works, when the element is created and appended from inside the element class:

var el = document.createElement('div')
el.innerHTML = 'click me'
el.onclick = () => {
    this.foo('bar')
}
this.appendChild(el)

EDIT 3, 4, 5: Played around a little (see solution 3) and assigned a Proxy object to a global variable ('me'), which projects any function calls to this Custom Element - kinda __noSuchMethod__. So... by choosing a 1- or 2-character-variable one could save at least a few hits on his keyboard by not typing this. but me., vm. or even i. ... best solution so far. Sad! Unfortunately, solution 3 can also not be used as general pattern for all elements, as it bounds the context only to one (i.e. the last connected) Custom Element.


EDIT 6 - Preliminary conclusions

As it turns out, it seems not possible to bind a html-inline onclick-event (onclick="foo('bar')") to the Custom Element class, in which the clickable element is embedded.

So the most obvious ways to go to me would be:

A) use the this keyword (like onclick="this.foo('bar')") and bind it to the class as shown above. Why this.foo() is bindable whereas foo() without this is not, remains unclear at the moment. I wonder bc both functions do not really differ - both have already bindings: this.foo() is bound to the clickable element, foo() is bound to window.

B) use eval as suggested by @Supersharp, i.e. add an event listener to the Custom Element, listen for clicks, get the value of the onclick-attribute as string, prepend "this." to the string and effectively do this: eval("this." + "foo()").

C) alternatively to inline onclick, I would tend to use event delegation and use declarative html attributes to indicate behaviour. To do this, again add a click event listener to the Custom Element class:

myElement.addEventListener('click', function(ev) {

        if (ev.target.dataset.action)
        {
            this[ev.target.dataset.action]()
        }

    }, false)

Your clickable element would look like <div data-action="next">Show more</div>.


Solution

  • A solution is to use eval() on the concatenation of "this." and the content of the onclick attribute the event.target element.

    Don't forget to use event.stopPropagation() to stop dispatching the event.

    handleClick( ev )
    {
        eval( 'this.' + ev.target.getAttribute( 'onclick' ) )
    
        ev.stopPropagation()
    }
    

    If you don't want to use eval(), you should parse the content of ev.target.getAttribute( 'onclick' ) to extract the function name et arguments and then call the element method if it exists:

    if ( typeof this.name === 'function ) 
        this[name](arguments)
    

    Update

    I suggest that you give an id to the custom element, then call the id + method:

    <my-element id="me">
        <div onclick="me.foo('bar')">click me</div>
    </my-element>
    

    This way you don't need to redirect/catch the event, and the div can be anywhere in the element.