ss1
ss1

Reputation: 1191

How to run JavaScript in <script> tag when content is injected via innerHTML?

I'm trying to write a JavaScript based pagination for a gallery, which loads HTML via an onClick-event from the server. The HTML is simply appended via innerHtml. The only difficulty is that I need to populate an array with thumbnail URLs for previews. The data for this is provided via the same HTTP request as the HTML.

I tried to add it via a <script> tag to the HTML, but these are not executed when inserted via innerHtml (see here).

Constraints:

Possible Solutions

Due to my limited knowledge of JavaScript I only see 2 solutions, of which none seems right:

  1. Send HTML and the array data as JSON. So it would be possible to append the HTML via innerHTML and the array data could be passed to a function, when the HTTP request returns.
  2. "Internal web scraping": I could theoretically keep the array data as JSON string in the <script> element and extract its contents via some murky JS tricks. But I'm sure I will get beaten up for this.

Any suggestion is highly appreciated.

For whom it may concern some code:

function loadNextPage(uri) {
  var xhr = new XMLHttpRequest();
  xhr.open('GET', uri);
  xhr.onload = function() {
    if (xhr.status === 200) {
      var loadNextButton = document.getElementById("load-next-button");
      loadNextButton.parentNode.removeChild(loadNextButton);
      var vbw = document.getElementById("vbw");
      vbw.innerHTML = vbw.innerHTML + xhr.response;
    } else {
      conosole.log('Failed: ' + xhr.status);
    }
  };
  xhr.send();
}
<div id="vbw">
  <script type="application/javascript">
    let thumbNailStore = {
      concat: function() {
        console.log('Concat has been ran');
      },
    };

    function addThumbnailListeners() {
      console.log('addThumbnailListeners  has been ran');
    }
    thumbNailStore.concat(); // This produces a JSON which is appended to an array in the <head> of the page
    addThumbnailListeners();
  </script>
  <button id="load-next-button" onclick="loadNextPage('10')">Load more</button>
</div>

And the template:

@standardtemplate.commons.gallery(videoList, LandingPage.empty()) // That produces the actual gallery HTML

@if(pagination.getNextPath.isPresent) {
    <script type="application/javascript">
        thumbNailStore.concat(@Html(TemplateHelper.thumbListToJson(videoList))); // This produces a JSON which is appended to an array in the <head> of the page
        addThumbnailListeners();
    </script>
    <button id="load-next-button" onclick="loadNextPage('@pagination.getNextPath.get()')">Load more</button>
}

Thanks!

Upvotes: 0

Views: 157

Answers (2)

Teocci
Teocci

Reputation: 8855

Well your solution it is almost right, I would replace vbw.innerHTML = vbw.innerHTML + xhr.response; with the .appendChild() method. But I also notice that you are adding multiples <script type="application/javascript">...</script>. So I think if you press the button Load More 2 times you will end up with 3 <script> tags in your code.

How to fix this?

Well, first extract the inline script inlineScript and append it to your current script. Then, create a new document using the DOMParser().parseFromString() method. This will parse your xhr.response then get the <button> element, and finally append it to your <div>.

Here is an example:

let matches, clickCount = 0;
const regexScript = /<script[\s\S]*?>([\s\S]*?)<\/script>/gi;
const regexButton = /<button[\s\S]*?>[\s\S]*?<\/button>/gi;
let nodeCounter = document.getElementById('counter');

function getResponse() {
  // To simulate response from your server
  return `&lt;script type=&quot;application/javascript&quot;&gt;let thumbNailStore={concat:function(){console.log(&#39;Concat for videolist ${clickCount} &#39;)}};function addThumbnailListeners(){console.log(&#39;addThumbnailListeners called ${clickCount}&#39;)}thumbNailStore.concat(),addThumbnailListeners();&lt;/script&gt;&lt;button id=&quot;load-next-button&quot; onclick=&quot;loadNextPage(&#39;https://jsonplaceholder.typicode.com/users/1&#39;)&quot;&gt;Load more&lt;/button&gt;`;
}

function htmlDecode(input) {
  var e = document.createElement('div');
  e.innerHTML = input;
  return e.childNodes[0].nodeValue;;
}

function loadNextPage(uri) {
  let xhr = new XMLHttpRequest();
  xhr.open('GET', uri);
  xhr.onload = function() {
    if (xhr.status === 200) {
      // Im' using responseTest you should use xhr.response
      let rawdata = htmlDecode(getResponse());
      clickCount++

      // Script matches
      matches = Array.from(rawdata.matchAll(regexScript));
      let rawScript = matches[0][1];
      let inlineScript = document.createTextNode(rawScript);

      // Button matches
      matches = Array.from(rawdata.matchAll(regexButton));
      let rawButton = matches[0];

      let script = document.getElementById('loaded-script');
      script.innerHTML = '';
      script.appendChild(inlineScript);

      let loadNextButton = document.getElementById('load-next-button');
      loadNextButton.parentNode.removeChild(loadNextButton);
      let vbw = document.getElementById('vbw');

      let doc = new DOMParser().parseFromString(rawButton, 'text/html');
      //let script = doc.body.childNodes;
      let button = doc.getElementById('load-next-button');
      // vbw.innerHTML = vbw.innerHTML + xhr.response;
      vbw.appendChild(button);
      nodeCounter.innerHTML = 'Request processed: ' + clickCount;
      eval(script.innerHTML);
    } else {
      conosole.log('Failed: ' + xhr.status);
    }
  };
  xhr.send();
}
<div id="vbw">
  <script id="loaded-script" type="application/javascript">
    let thumbNailStore = {
      concat: function() {
        console.log('Concat original');
      },
    };

    function addThumbnailListeners() {
      console.log('addThumbnailListeners');
    }
    thumbNailStore.concat(); // This produces a JSON which is appended to an array in the <head> of the page
    addThumbnailListeners();
  </script>
  <button id="load-next-button" onclick="loadNextPage('https://jsonplaceholder.typicode.com/users/1')">Load more</button>
</div>
<div id="counter"></div>

Upvotes: 1

Alexander Guyer
Alexander Guyer

Reputation: 2203

If jQuery is an option, load() will execute any script elements contained in the loaded content as well as modify the inner HTML, which sounds like what you need.

Otherwise, you can use eval. You just need some way of retrieving the script element (perhaps the last script element child of vbw, by some predetermined id, or even by creating an element out of the response data and accessing the script subElement from there). Either way, once you have the script element object:

eval(scriptElement.innterHtml);

Upvotes: 1

Related Questions