window.Elm
elm-watch is basically just elm make
in watch mode, so the output format is using the good old window.Elm
global.
elm-watch even requires window.Elm
to exist. That global variable is key to make hot reloading work. (Technically, globalThis.Elm
is required to exist. See below.)
In short: Keep it simple and load the built Elm JS in its own <script>
tag and you’ll be fine.
If you’re coming from webpack, Parcel or Vite, you need to update your JavaScript entrypoint like so:
-import { Elm } from "./src/Main.elm";
const root = document.getElementById("root");
-const app = Elm.Main.init({ node: root });
+const app = window.Elm.Main.init({ node: root });
Regardless of whether you use a bundler or just standard import
s, don’t be tempted to import
the built Elm JS:
// 🚨 WRONG! Don’t do this!
import Elm from "./build/main.js";
// 🚨 WRONG! Don’t do this either!
import "./build/main.js";
Instead, load the built Elm JS in a separate script tag, without any type
attribute:
<!-- 🚨 WRONG! Don’t do this! -->
<script type="module" src="./build/main.js"></script>
<!-- ✅ Correct! No `type` is the way to go. -->
<script src="./build/main.js"></script>
Why? Because of scripts vs modules. Back in the day, only JavaScript scripts existed, but since the import
syntax came along we also have modules. They are essentially two different modes for JavaScript with slightly different behavior.
- Module mode is enabled via the
type="module"
attribute on the<script>
tag, or by usingimport
to load a file. - Script mode is used for everything else.
The built Elm JS is simply not made for module mode:
- It does not use the
export
keyword, so there’s nothing toimport
from it. - It uses an old-school technique to create the
window.Elm
global: It basically doesthis.Elm = stuff
. In script mode,this
refers to the global object, which usually iswindow
. The reason for usingthis
is to make Platform.worker programs usable in Web Workers and Node.js (wherewindow
does not exist). But in module mode, globalthis
is alwaysundefined
.
If you use a bundler, import
ing the built Elm JS can have additional downsides:
- The bundler might rewrite that global
this
mentioned above toexports
(a local object) in an attempt to supportimport
ing old-school packages. However, thenwindow.Elm
won’t be created. - The bundler might waste time parsing the whole built Elm JS file for nothing.
elm-watch could replace the this
in the built Elm JS with globalThis which the modern way of getting the global object no matter what environment. But elm-watch takes a very conservative approach: For elm-watch make
, the built Elm JS is just the output of elm make
with no modifications at all. Patching that output is not the job of elm-watch, and would lead to the question of where to stop. You can choose to make that modification in a postprocess script, though, if you really feel like it.
Having window.Elm.Main.init()
in your code might feel ugly and old-school compared to using import
, but I think it’s fine:
- It’s simple.
- It’s just going to affect one line of your code.
- It lets you decouple your Elm completely from all other JavaScript.
- It makes hot reloading work without any setup.
- And it can even be good for browser caching! Your Elm code might change very often, but some JavaScript code (perhaps using an npm package) might be very stable and can then be cached independently from the compiled Elm code.