97

I am testing ES6 Modules and want to let the user access some imported functions using onclick:

test.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Module Test</title>
</head>
<body>
    <input type="button" value="click me" onclick="hello();"/>
    <script type="module">import {hello} from "./test.js";</script>
</body>
</html>

test.js:

export function hello() {console.log("hello");}

When I click the button, the developer console says: ReferenceError: hello is not defined. How can I import functions from modules so that they are available as onclick functions?

I am using Firefox 54.0 with dom.moduleScripts.enabled set to true.

Konrad Höffner
  • 11,100
  • 16
  • 60
  • 118
  • Can you open the console and execute hello manually after the page has loaded? – Gavin Jun 16 '17 at 13:26
  • This feature is marked as experimental, I would not use it right now. You'd better transpile (Babel?) your code to be sure it will work in any browser, don't you? – sjahan Jun 16 '17 at 13:40

4 Answers4

154

Module creates a scope to avoid name collisions.

You can use addEventListener to bind the 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>

Or you can expose your function to the window object, that is not recommended.

import {hello} from './test.js'
  
window.hello = hello
Yury Tarabanko
  • 44,270
  • 9
  • 84
  • 98
  • 12
    Definitely take option two. Globals are a pain to deal with at the best of times. – Quentin Mar 02 '20 at 16:09
  • 1
    Tempted to edit this answer and remove the `window` approach entirely or _at least_ add a warning. Apart from the reason above, this will still leave an `onclick` attribute there. These are [bad practice](/q/11737873/4642212), an [obsolete, cumbersome, and unintuitive](/a/43459991/4642212) way to listen for events. The last browser needing these reached EOL nearly two decades ago, so this approach has nothing to do with modern programming. _Always_ use [`addEventListener`](//developer.mozilla.org/en/docs/Learn/JavaScript/Building_blocks/Events#inline_event_handlers_%E2%80%94_dont_use_these). – Sebastian Simon Mar 18 '22 at 20:52
  • 1
    Yeah well what sucks about option 2 is that it harms readability. Instead of inspecting the button in HTML and having breadcrumbs linking it to the JS, now you have to grep through your code looking for `addEventListener` and/or element id until you stumble upon the right one. Basically, how do you answer the question "what happens when I click this button" when the handler binding is implemented purely in JS? Seems like there's no direct way to answer that question, you have to grep around. – Gillespie Jul 24 '23 at 15:59
  • 1
    @Gillespie The Inspector in your browsers developer tools should be able to handle this case. Well-structured, modularized code should be easily scannable in terms of user interactivity. – Sebastian Simon Jul 30 '23 at 15:59
  • @Quentin i suppose you could just do something like `window.__var_name__`, which is very unlikely, if not impossible to have a global name conflict. – The Empty String Photographer Aug 14 '23 at 13:14
  • 1
    @TheEmptyStringPhotographer you can come up with a naming conventions to make the collision unlikely, but it doesn't mean you should. :) Most of the time you want to isolate features in their own boundaries (aka "modules"). – Yury Tarabanko Aug 14 '23 at 20:21
38

Module scope in ES6 modules:

When you import a script in the following manner using type="module":

<script type="module">import {hello} from "./test.js";</script>

You are creating a certain scope called module scope. Here is where modules scope is relative to other levels of scope. Starting from global they are:

  1. Global scope: All declarations outside a module on the outermost level (i.e. not in a function, and for const and let not in a block) are accessible from everywhere in the Javascript runtime environment
  2. Module scope: All declarations inside a module are scoped to the module. When other javascipt tries to access these declarations it will throw a reference error.
  3. Function scope: All the variables declared inside (the top level of) a function body are function scoped
  4. Block scope: let and const variables are block scoped

You were getting the referenceError because the hello() function was declared in the module, which was module scoped. As we saw earlier declarations inside module scope are only available within that module, and you tried to use it ouside the module.

We can make declarations inside a module global when we explicitly put it on the window object so we can use it outside of the module. For example:

window.hello = hello;  // putting the hello function as a property on the window object
Willem van der Veen
  • 33,665
  • 16
  • 190
  • 155
  • Oh so does that mean that a function in a script tag without the module type is put on the global scope and can be used from within an external script? – Konrad Höffner Jan 11 '22 at 08:16
8

While the accepted answer is correct, it scales poorly once you start importing from multiple modules, or declaring multiple functions . Plus, as noted by @Quentin, it risks global name space pollution.

I prefer a slight modification

import { doThis, doThat } from './doStuff.js'
import { foo1, foo2 } from './foo.js'

function yetAnotherFunction() { ... }

window._allTheFns = { doThis, doThat, foo1, foo2, yetAnotherFunction }

// or, if you prefer, can subdivide

window._doStuffjs = { doThis, doThat }
window._foojs = { foo1, foo2 }
  1. Uses an "unusual" property name to (hopefully) avoid global namespace issues
  2. No need to immediately attach (or forget to attach) an EventListener.
  3. You can even put the listener in the HTML, e.g. <button onclick="window._foojs.foo1(this)">Click Me</button>
user949300
  • 15,364
  • 7
  • 35
  • 66
  • 1
    Shocking :) Hard to believe ES does not allow namespace qualifications to simplify this. One thing. Worth checking to see if the namespace exists before making the assignment in `window._allTheFns = { doThis, doThat, foo1, foo2, yetAnotherFunction }` – J Evans Mar 28 '23 at 22:19
1

As of 2022 I don't know of any other solution than the provided, except that you don't need to use window, but you can use globalThis

I don't know if this makes it any better.

advantages:

  • it is a tick more readable
  • you don't get a compiler error when checkJS is on (attribute "..." is not found on window etc...)

I'm even using LitHtml where you have @event notation, looks like this:

<element @click="${(e)=>console.log(e)}"></element>

but sadly there is no clean way to actually get a reference of the object that has the listener attached. the target, originalTarget and explicitOrOriginalTarget can be anything in the bubbling queue, which is super annoying and super confusing, but if you don't need that, LitHTML would be the way to go.

Eric Aya
  • 69,473
  • 35
  • 181
  • 253