Search code examples
javascriptscoping

How to call an in-scope function from the onclick attribute of an html element using a functional vanilla JS (not-React) component?


I have something that I'm really close to being able to do, but I feel like I'm missing something pretty obvious. My question is how to associate a function to an onclick html attribute in a function that renders some HTML. I'm doing this for learning and illustration, so I'm aware of how this is done pretty straightforward in something like React.

function article_list(article_list_id) {

    const onclick_handler = (event) => {
      event.preventDefault();
      console.log(`I was clicked!!!`)
      return false;
    }
    
    const render = () => {
      console.log("I was rendered!!!")
      articleElement = document.getElementById(article_list_id)
      articleElement.innerHTML = `
        <ul>
        ${some_array_of_items.map(item => { 
          return `
            <li class="text-sm flex items-center">
              <span><a href="#" class="list-checklist" data-id="${item.id}" onclick="${onclick_handler}">${item.title}</a></span>
            </li>`
        }).join('')}  
      `
    }

    return Object.assign({}, {
      render,
    })

Here, I'd like for the a link to be able to have a simple onclick="" html attribute that then calls the function. I know that I am messing up my .bind() or .apply() and, because I am appending a string to the .innerHTML value, I'm not fully sure if there is an impact there because of it.

I know there is an answer, but thought I would ask here in hopes of getting pointed in the right direction. Again, I know that I could React to do this, but the goal of my exercise is to learn / understand how to do this w/out React. Thank you!


Solution

  • Instead of inserting the HTML all at once, insert a <li> element on each iteration (without changing the HTML inside the same container, so as not to corrupt existing listeners). Once the <li> is inserted, select the <a> and add a listener to it with addEventListener.

    const render = () => {
      const parent = document.getElementById(article_list_id);
      parent.textContent = '';
      const ul = parent.appendChild(document.createElement('li'));
      for (const item of some_array_of_items) {
        const li = ul.appendChild(document.createElement('li'));
        li.className = "text-sm flex items-center";
        li.innerHTML = `<span><a href="#" class="list-checklist" data-id="${item.id}">${item.title}</a></span>`;
        li.querySelector('a').addEventListener('click', onclick_handler);
      }
    }
    

    If the input happens to be untrustworthy, this can result in arbitrary code execution, which is not desirable. In such a case, don't interpolate into the HTML, set the properties and contents after inserting the LI.

    const li = ul.appendChild(document.createElement('li'));
    li.className = "text-sm flex items-center";
    li.innerHTML = `<span><a href="#" class="list-checklist"></a></span>`;
    const a = li.querySelector('a');
    a.dataset.id = item.id;
    a.textContent = item.title;