ejs

Rendering user.ejs file into dashbord.ejs file but user.ejs file's javascript is not working after rendering it in dashbord.ejs


I am making a web based chat app like whatsapp web. When clicking on an user then I want to render user.ejs file inside dashbord.ejs.

I am able to load the html and css from user.ejs but js is not working.

I don't use any frontend framework.

I think problem might be with script tag of dashbord.ejs Which is already loaded thats why user.ejs js is unable to load.

How can i fix this problem?

I have tried linking js externally in user.ejs according to other solution but it still not working.

In this section, which is part of dashboard.ejs, the rendered HTML of user.ejs will be inserted.

<div class="webPageCont"> </div>

internal javascript of user.ejs

const connectBtn = document.querySelectorAll('.connect-btn');

for (let i = 0; i < connectBtn.length; i++) {
    connectBtn[i].addEventListener('click', async () => {
        console.log("hello");
        const response = await fetch('/dashbord/loadAllUsers/sentReq', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ sentTo: document.querySelectorAll('.username')[i].value })
        })

    })
}

user.ejs

<% for(let i=0; i<user.length ; i++) { %>
    <div class="user">
        <img src="<%= user[i].profile%>" alt="">
        <span>
            <%= user[i].name%>
        </span>
        <input type="hidden" value="<%= user[i]._id%>" class="username">
    </div>
    <button class="connect-btn">connect</button>
<% } %>

When I am clicking on button then js is not working.

Backend code for rendering

loadAllUsers = async (req, res) => {
    try {
        const user = await users.find({ _id: { $ne: req.user } }).select('-password');
        const loginUser = await users.findById(req.user).select('-password');

        ejs.renderFile(path.resolve(__dirname, '../views/user.ejs'), { user: user, loginUser: loginUser }, function (err, str) {
            if (!err) { res.send(str); }
            else { console.log(err) }
        });
    } catch (err) {
        console.log(err);
        res.status(500).send({ message: 'internal server error.pls try again later.' })
    }
}

Solution

  • I have looked up and tested different approaches and will list them here, you can then decide what works best for you.

    The Problem

    But first of all, why does it not work?
    I'm assuming you add the HTML string either with container.innerHTML = string or container.insertAdjacentHTML(string). The problem here, is that HTML inserted with innerHTML will not execute scripts.

    As W3C describes (source):

    Note: script elements inserted using innerHTML do not execute when they are inserted.

    And this also goes for insertAdjacentHTML() as that function uses innerHTML.

    But there are ways to execute javascript this way though, which is considered a cross-site scripting attack.

    MDN writes (source):

    However, there are ways to execute JavaScript without using elements, so there is still a security risk whenever you use innerHTML to set strings over which you have no control. For example:

    const name = "<img src='x' onerror='alert(1)'>";
    el.innerHTML = name; // shows the alert
    

    So it is generally recommended to use textContent instead of innerHTML to prevent security risks, unless you 100% know the HTML is not malicious or can be sanitized beforehand.

    Solutions

    1. Appending DocumentFragments (source)

    This is probably one of the best and straight forward approaches, since you can simply take the whole string, in contrast to other solutions, and insert it with 2 simple lines of code:

    const fragment = document.createRange().createContextualFragment(string);
    document.querySelector('.webPageCont').appendchild(fragment);
    

    I'm not sure what the differences to innerHTML are, as they both utilize the HTMLparser. But I can assume that the one innerHTML uses has a limitation on parsing scripts. Also the fourth step of the fragment parsing algorithm that createContextualFragment() utilizes says:

    1. Append each node in new children to fragment (in tree order)

    which might also play a role in this.


    2. Seperately insert script element

    This approach is very simple. Put the javascript into a seperate file (e.g. user.js) and manually add a script with the file as src. So it would look something like this:

    // get the HTML string by fetch or other means and add to page
    document.querySelector('.webPageCont').insertAdjacentHTML('beforeend', HTMLstring_without_script);
    // create script element with javascript as source
    let script_el = document.createElement('script');
    script_el.src = "/user.js"; // adjust depending on file hierachy
    // either append to the HEAD
    document.querySelector('head').appendChild(script_el);
    // or anywhere in the BODY
    document.body.appendChild(script_el);
    

    If interested, one can read more on dynamic script loading here.


    3. Extract script tags from string

    This approach, just like the one before, is pretty simple. When receiving the HTML string on the client-side, extract any script tags from the string and insert them as individual element:

    while(/<script>((?:.*?\n*\s*)+?)<\/script>/.test(text)) {
      let reg_match = text.match(/<script>((?:.*?\n*\s*)+?)<\/script>/);
      let script = reg_match[1];
      text = text.replace(reg_match[1], '');
      let s = document.createElement('script');
      s.textContent = script;
      document.querySelector('head').appendChild(s);
    }
    
    document.querySelector('.container-content').insertAdjacentHTML('beforeend', text);
    

    But, depending on how the javascript is written, you might not want to load the scripts before the HTML. Then you can just safe each string representing a script tag in an array and add them all after adding the HTML.


    4. Script replacer (source)

    You could say that this approach is the more advanced version of the one before. You define a method that recursively goes through all the children of a provided element and replaces all not-parsed scripts with functional ones. The method is defined as:

    function nodeScriptReplace(node) {
            if ( nodeScriptIs(node) === true ) {
                    node.parentNode.replaceChild( nodeScriptClone(node) , node );
            }
            else {
                    var i = -1, children = node.childNodes;
                    while ( ++i < children.length ) {
                          nodeScriptReplace( children[i] );
                    }
            }
    
            return node;
    }
    function nodeScriptClone(node){
            var script  = document.createElement("script");
            script.text = node.innerHTML;
    
            var i = -1, attrs = node.attributes, attr;
            while ( ++i < attrs.length ) {                                    
                  script.setAttribute( (attr = attrs[i]).name, attr.value );
            }
            return script;
    }
    
    function nodeScriptIs(node) {
            return node.tagName === 'SCRIPT';
    }
    

    Which then can be utlized as:

    document.querySelector('.webPageCont').insertAdjacentHTML('beforeend', string);
    nodeScriptReplace(document.querySelector('.webPageCont'));