Home > Net >  Use parameterised functions defined in ES6 module directly in html
Use parameterised functions defined in ES6 module directly in html

Time:10-28

Functions defined inside an ES6 module embedded in an HTML script are not available to that script. Thus if you have a statement such as:

<button onclick="doSomething();">Do something</button>

in your HTML and your doSomething() function lives inside an ES6 module embedded in the HTML script, you will get a "doSomething() is undefined" error when you run the script.

Use functions defined in ES6 module directly in html suggests a great solution to the immediate problem, recommending that you "bind" your function to the button by amending your HTML thus:

<button id="dosomethingbutton">Do something</button>

and using the module itself to create a linkage thus:

document.getElementById('dosomethingbutton').addEventListener('click', doSomething);

This works fine, but what if your original button was a bit more sophisticated and was parameterised? For example:

<button onclick="doSomething('withThisString');">Do Something with String</button>

The most that the "binding" can provide seems to be limited to the circumstances relating to the event - I can find no way of associating it with data. I'm completely stuck trying to find a solution to this one and assistance would be much appreciated.

I'd like to add that, while this problem might seem a bit obscure, I think it will be of interest to anyone migrating to Firebase 9. Amongst other changes, migration requires you to move your javascript code into ES6 modules and so it's likely that the simplest HTML will immediately hit these issues. Google themselves don't mention the problem and it may be that this is because they expect the code to be "bundled" with Webpack. But I'm new to these concepts and haven't tried this yet as I'm still struggling to get a grip on the basics of the conversion process. Advice would be most welcome.

CodePudding user response:

This works fine, but what if your original button was a bit more sophisticated and was parameterised?

There are a couple of solutions to that:

  1. A data-* attribute:

    <button id="the-button" data-string="withThisString">Do Something with String</button>
    
    document.getElementById("the-button").addEventListener("click", function() {
        doSomething(this.getAttribute("data-string"));
    });
    

    (More on this below.)

    or

  2. Binding the string when you bind the event

    <button id="the-button">Do Something with String</button>
    
    document.getElementById("the-button").addEventListener("click", () => {
        doSomething("withThisString");
    });
    

There are lots of variations on the above, and if you use doSomething with multiple buttons with different strings you can do #1 with a class and a loop rather than with an id, but that's the general idea.


Re the data-* attribute thing: If you wanted to, you could make this process entirely HTML-driven via data-* attributes and a single function that hooks things up. For instance, say you had these buttons:

<button data-click="doThisx@module1">Do This</button>
<button data-click="doThat@module2">Do That</button>
<button data-click="doTheOther@module3">Do The Other</button>

You could have a single reusable function to hook those up:

class EventSetupError extends Error {
    constructor(element, msg) {
        if (typeof element === "string") {
            [element, msg] = [msg, element];
        }
        super(msg);
        this.element = element;
    }
}
export async function setupModuleEventHandlers(eventName) {
    try {
        const attrName = `data-${eventName}`;
        const elements = [...document.querySelectorAll(`[${attrName}]`)];
        await Promise.all(elements.map(async element => {
            const attrValue = element.getAttribute(`data-${eventName}`);
            const [fname, modname] = attrValue ? attrValue.split("@", 2) : [];
            if (!fname || !modname) {
                throw new EventSetupError(
                    element,
                    `Invalid '${attrName}' attribute "${attrValue}"`
                );
            }
            // It's fine if we do import() more than once for the same module,
            // the module loader will return the same module
            const module = await import(`./${modname}.js`);
            const fn = module[fname];
            if (typeof fn !== "function") {
                throw new EventSetupError(
                    element,
                    `Invalid '${attrName}': no '${fname}' on module '${modname}' or it isn't a function`
                );
            }
            element.addEventListener(eventName, fn);
        }));
    } catch (error) {
        console.error(error.message, error.element);
    }
}

Using it to find and hook up click handlers:

import { setupModuleEventHandlers } from "./data-attr-event-setup.js";
setupModuleEventHandlers("click")
.catch(error => {
    console.error(error.message, error.element);
});

It's one-time plumbing but gives you the same attribute-based experience in the HTML (the event handlers could still get parameter information from another data-* attribute, or you could bake that into your setup function). (That example relies on dynamic import, but that's supported by recent versions of all major browsers and, to varying degrees, bundlers.

There are a couple of dozen ways to spin that and I'm not promoting it, just giving an example of the kind of thing you can readily do if you want.

But really, this is where libraries like React, Vue, Ember, Angular, Lit, etc. come into play.

  • Related