0

I have a JS class:

class MyClass {
    constructor() {
    }

    handleClick() {
        alert('clicked');
    }

    render() {
        return `<input type="radio" onClick="${this.handleClick} /><label>My Label</label>"`;
    }
}

window.addEventListener('load', init);

function init() {
    const myClassInstance = new MyClass();
    const el = document.getElementsByClassName('myelement')[0];
    el.insertAdjacentHTML('beforeend', myClassInstance.render());
}

This renders a radio input in the element with the class myelement but when I click the radio button I see this:

Uncaught SyntaxError: Unexpected token {

The error is referring to the handleClick method.

Is my syntax off? If I use onClick="${this.handleClick()}", I don't see errors but the function is called immediately and not on click. What am I doing wrong?

Michael Lynch
  • 2,682
  • 3
  • 31
  • 59
  • Apologies, completely mistook the question at first. – T.J. Crowder May 22 '18 at 16:23
  • The short answer is you can't: not that way. You would need a function to process the template literal and handle the function specially. Or use a library that does that for you, like [hyperhtml](https://github.com/WebReflection/hyperHTML), assuming you really want to use template literals. – Dark Falcon May 22 '18 at 16:27
  • Or Vue or React or Marko or any of the dozen other libs that handle stuff like this for you. – T.J. Crowder May 22 '18 at 16:30

2 Answers2

0

Your best bet would be not to use markup, since the function you call from an onxyz-attribute-style event handler has to be a global.

Instead, create the element and then assign the function to it:

class MyClass {
    constructor() {
    }

    handleClick(e) {
        alert('clicked');
    }

    render() {
        var input = document.createElement("input");
        input.type = "radio";
        input.addEventListener("click", e => this.handleClick(e));
        return input;
    }
}

window.addEventListener('load', init);

function init() {
    const myClassInstance = new MyClass();
    const el = document.getElementsByClassName('myelement')[0];
    el.appendChild(myClassInstance.render());
}
<div class="myelement"></div>

Note the use of an arrow function so that this is correct when we call this.handleClick. Obviously, you don't need it for just an alert, but I assume you'll want to use properties of the instance of MyClass. (To access the element on which the click occurred, use e.currentTarget since it won't be this.)


If you have a strong preference to do this with markup, an option would be:

  • Have an initEvents method or similar that consumers of the class are supposed to call (like they're supposed to call render)
  • Use a unique ID in the markup
  • Use that ID to find the element and hook up events

Or better yet: Have render accept the element to add to, and do the event hookup after adding it.

But working with your existing render approach:

const MyClass = (() => {
  let nextId = 1;
  class MyClass {
      constructor() {
          this.id = `__MyClass__${nextId++}`;
      }

      handleClick(e) {
          alert('clicked');
      }

      render() {
          return `<input type="radio" id="${this.id}" />`;
      }
      
      initEvents() {
          const e = document.getElementById(this.id);
          if (!e) {
              throw new Error("initEvents must be called after appending the markup from render to the DOM");
          }
          e.addEventListener("click", e => this.handleClick(e));
      }
  }
  return MyClass;
})();

window.addEventListener('load', init);

function init() {
    const myClassInstance = new MyClass();
    const el = document.getElementsByClassName('myelement')[0];
    el.insertAdjacentHTML('beforeend', myClassInstance.render());
    myClassInstance.initEvents();
}
<div class="myelement"></div>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • Your first example without any markup works great but I actually need to return both a radio input and label. Would I need to use markup in that case? – Michael Lynch May 22 '18 at 17:01
  • @MichaelLynch - In that *specific* case, I'd put the radio button inside the label, so that clicking on the label selects the radio button: `var label = document.createElement("label"); label.appendChild(input); label.appendChild(document.createTextNode(" Text for the label"));` (note the leading space on the label text) then return the `label`. In the general case, you can use `div` or `span` to wrap multiple elements. – T.J. Crowder May 22 '18 at 17:10
0

If you want to this onclick working then you need to assign global function to onclick otherwise you can not do in this way I did quick fix It is just a temporary solution you can either choose this or @T.J answer also looking good.

class MyClass {
    constructor() {}

    handleClick() {
        alert('clicked');
    }

    render() {
        const globalId = "h" + Math.floor((Math.random() * 100) + 12345);
        window[globalId] = this.handleClick;
        return `<input type="radio" onClick=window['${globalId}']() />`;
    }
}

window.addEventListener('load', init);

function init() {
    const myClassInstance = new MyClass();
    const el = document.getElementsByClassName('myelement')[0];
    el.insertAdjacentHTML('beforeend', myClassInstance.render());
}
<div class="myelement"></div>
Nishant Dixit
  • 5,388
  • 5
  • 17
  • 29