Es6 Modules: Undefined Onclick Function After Import

ES6 Modules: Undefined onclick function after import

Module creates a scope to avoid name collisions.

Either expose your function to window object

import {hello} from './test.js'

window.hello = hello

Or use addEventListener to bind handler. Demo

<button type="button" id="hello">Click Me</button>
<script type="module">
import {hello} from './test.js'

document.querySelector('#hello').addEventListener('click', hello)
</script>

How to call ES6 module function from onclick

The only way to do it would be to assign the imported module function to the window object so that it can be referenced by the inline handler:

  <script type="module">
import { Es6Module2 } from "./js2.js";
window.Es6Module2 = Es6Module2;
</script>

But that defeats the purpose of modules.

Regardless, inline handlers should never, ever be used. They have way too many problems, including this one (requiring the function to be called to be in the global scope). Best to avoid them and use addEventListener instead, to add the listener properly.

If you want to add a listener which calls a function with a parameter, then just call the function with the parameter in the listener:

import { fn } from './script.js';
document.querySelector('button').addEventListener('click', () => {
fn('foo');
});

How to call a function declared in a javascript module (type=module) from an html page

First of all you have to explicitly export you function:

export function greet() {
alert("Hello from module");
}

Secondly, a module has it's own scope (this is the whole point of modules) thus you need to add the function to the global scope. So, to do it you have to run a script which imports the function and adds it to the window object:

<script type="module">
import { greet } from "./app.js";
window.greetFromModule = greet;
</script>

Now you don't need this part <script type="module" src="app.js"></script>

Alternatively you can create an empty obj and add your modules stuff to it, this is what it would look like:

<html>
<head></head>
<body>
<button onclick="greetFromHtml();">greetFromHtml</button>
<button onclick="module.greet()">greetFromModule</button>
<script type="text/javascript">
function greetFromHtml() {
alert("Hello");
}
const module = {};
</script>
<script type="module">
import { greet } from "./app.js";
module.greet = greet;
</script>
</body>
</html>

Use functions defined in ES6 module directly in html

Each module has its own scope. They are not sharing the global scope like "normal" scripts do. That means hello is only accessible inside the main.js module/file itself.

If you explicitly want to create a global variable, you can achieve that by creating a property on the global object, window:

function hello()
{
console.log(mod());
}
window.hello = hello;

See also Define global variable in a JavaScript function


Having said that, it's good practice to avoid global variables. Instead you can restructure the HTML to load the modules after the button was created and bind the event handler via JavaScript:

<!DOCTYPE html>
<html>
<body>
<button name="next-button">Obi-Wan abandons the high ground to salute you</button>
<script type="module" src="module.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>

and

import { mod } from './module.js';

function hello()
{
console.log(mod());
}
document.querySelector('button').addEventListener('click', hello);

Use parameterised functions defined in ES6 module directly in html

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.

ES6 module Uncaught ReferenceError: function is not defined at HTMLButtonElement.onclick

Functions used in onxyz-attribute-style handlers must be globals (it's one of the many reasons not to use them). Your function isn't a global, it's local to the module in which you're importing it. (Remember: Your main script is a module too; if it weren't, you couldn't use import in it.)

You could make it a global by assigning to a window property:

window.createNotification = createNotification;

but it would be much better to use modern event handling:

document.querySelector("#container button").addEventListener("click", createNotification);

Live example on plnkr, obviously will only work on cutting-edge browsers with module support.


Side note: As Andreas points out, type="error" isn't valid for button elements. Valid types are button, submit, or reset, with submit being the default. (I've changed it to button in the plnkr.)



Related Topics



Leave a reply



Submit