Dirk Harriman Banner Image

 

Notes Javascript - Modules

Sub Menu Here

 

 

Javascript Modules


Javascript use and the scale of its complexity has grown from its early inception. The need for the ability to modularize code became apparant.


 

Basic Example Structure

In our first example (see basic-modules) we have a file structure as follows:

index.html main.js modules/ canvas.js square.js

The modules directory's two modules are described below:


 

Exporting Module Features

The first thing you do to get access to module features is export them. This is done using the export statement.
The easiest way to use it is to place it in front of any items you want exported out of the module, for example:

export const name = "square"; export function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.fillRect(x, y, length, length); return { length, x, y, color }; }

You can export functions, var, let, const, and classes. They need to be top-level items; you can't use export inside a function, for example.
A more convenient way of exporting all the items you want to export is to use a single export statement at the end of your module file, followed by a comma-separated list of the features you want to export wrapped in curly braces. For example:

export { name, draw, reportArea, reportPerimeter };


 

Importing Features Into Your Script

Once you've exported some features out of your module, you need to import them into your script to be able to use them. The simplest way to do this is as follows:

import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";

You use the import statement, followed by a comma-separated list of the features you want to import wrapped in curly braces, followed by the keyword from, followed by the module specifier.

The module specifier provides a string that the JavaScript environment can resolve to a path to the module file. In a browser, this could be a path relative to the site root, which for our basic-modules example would be /js-examples/module-examples/basic-modules. However, here we are instead using the dot (.) syntax to mean "the current location", followed by the relative path to the file we are trying to find. This is much better than writing out the entire absolute path each time, as relative paths are shorter and make the URL portable - the example will still work if you move it to a different location in the site hierarchy.

So for example:

/js-examples/module-examples/basic-modules/modules/square.js

becomes

./modules/square.js

You can see such lines in action in main.js

import { create, createReportList } from './modules/canvas.js'; import { name, draw, reportArea, reportPerimeter } from './modules/square.js'; import randomSquare from './modules/square.js'; let myCanvas = create('myCanvas', document.body, 480, 320); let reportList = createReportList(myCanvas.id); let square1 = draw(myCanvas.ctx, 50, 50, 100, 'blue'); reportArea(square1.length, reportList); reportPerimeter(square1.length, reportList); // Use the default let square2 = randomSquare(myCanvas.ctx);

function create(id, parent, width, height) { let divWrapper = document.createElement('div'); let canvasElem = document.createElement('canvas'); parent.appendChild(divWrapper); divWrapper.appendChild(canvasElem); divWrapper.id = id; canvasElem.width = width; canvasElem.height = height; let ctx = canvasElem.getContext('2d'); return { ctx: ctx, id: id }; } function createReportList(wrapperId) { let list = document.createElement('ul'); list.id = wrapperId + '-reporter'; let canvasWrapper = document.getElementById(wrapperId); canvasWrapper.appendChild(list); return list.id; } export { create, createReportList };

const name = 'square'; function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.fillRect(x, y, length, length); return { length: length, x: x, y: y, color: color }; } function random(min, max) { let num = Math.floor(Math.random() * (max - min)) + min; return num; } function reportArea(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} area is ${length * length}px squared.` let list = document.getElementById(listId); list.appendChild(listItem); } function reportPerimeter(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} perimeter is ${length * 4}px.` let list = document.getElementById(listId); list.appendChild(listItem); } function randomSquare(ctx) { let color1 = random(0, 255); let color2 = random(0, 255); let color3 = random(0, 255); let color = `rgb(${color1},${color2},${color3})` ctx.fillStyle = color; let x = random(0, 480); let y = random(0, 320); let length = random(10, 100); ctx.fillRect(x, y, length, length); return { length: length, x: x, y: y, color: color }; } export { name, draw, reportArea, reportPerimeter }; export default randomSquare;

<!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <title>Basic JavaScript module example</title> <style> canvas { border: 1px solid black; } </style> <script type="module" src="main.js"></script> </head> <body> </body> </html>


 

Importing Modules Using Import Maps

Above we saw how a browser can import a module using a module specifier that is either an absolute URL, or a relative URL that is resolved using the base URL of the document:

import { name as squareName, draw } from "./shapes/square.js"; import { name as circleName } from "https://example.com/shapes/circle.js";

Import maps allow developers to instead specify almost any text they want in the module specifier when importing a module; the map provides a corresponding value that will replace the text when the module URL is resolved.

For example, the imports key in the import map below defines a "module specifier map" JSON object where the property names can be used as module specifiers, and the corresponding values will be substituted when the browser resolves the module URL. The values must be absolute or relative URLs. Relative URLs are resolved to absolute URL addresses using the base URL of the document containing the import map.

<script type="importmap"> { "imports": { "shapes": "./shapes/square.js", "shapes/square": "./modules/shapes/square.js", "https://example.com/shapes/": "/shapes/square/", "https://example.com/shapes/square.js": "./shapes/square.js", "../shapes/square": "./shapes/square.js" } } </script>

The import map is defined using a JSON object inside a <script> element with the type attribute set to importmap. There can only be one import map in the document, and because it is used to resolve which modules are loaded in both static and dynamic imports, it must be declared before any <script> elements that import modules.

With this map you can now use the property names above as module specifiers. If there is no trailing forward slash on the module specifier key then the whole module specifier key is matched and substituted. For example, below we match bare module names, and remap a URL to another path.

// BARE MODULE NAMES AS MODULE SPECIFIERS import { name as squareNameOne } from "shapes"; import { name as squareNameTwo } from "shapes/square"; // REMAP A URL TO ANOTHER URL import { name as squareNameThree } from "https://example.com/shapes/moduleshapes/square.js";

If the module specifier has a trailing forward slash then the value must have one as well, and the key is matched as a "path prefix". This allows remapping of whole classes of URLs.

// REMAP A URL AS A PREFIX ( https://example.com/shapes/) import { name as squareNameFour } from "https://example.com/shapes/square.js";

It is possible for multiple keys in an import map to be valid matches for a module specifier.
For example, a module specifier of shapes/circle/ could match the module specifier keys shapes/ and shapes/circle/. In this case the browser will select the most specific (longest) matching module specifier key.

Import maps allow modules to be imported using bare module names (as in Node.js), and can also simulate importing modules from packages, both with and without file extensions. While not shown above, they also allow particular versions of a library to be imported, based on the path of the script that is importing the module. Generally they let developers write more ergonomic import code, and make it easier to manage the different versions and dependencies of modules used by a site. This can reduce the effort required to use the same JavaScript libraries in both browser and server.

Feature Detection

You can check support for import maps using the HTMLScriptElement.supports() static method (which is itself broadly supported):

function runScript1() { var divResult = document.getElementById("results1"); if (HTMLScriptElement.supports?.("importmap")) { divResult.innerHTML = "Browser supports import maps."; } else { divResult.innerHTML = "Browser does not support import maps."; } }

Check Feature Detection
 

 

Importing Modules As Bare Names

In some JavaScript environments, such as Node.js, you can use bare names for the module specifier. This works because the environment can resolve module names to a standard location in the file system.
For example, you might use the following syntax to import the "square" module.

import { name, draw, reportArea, reportPerimeter } from "square";

To use bare names on a browser you need an import map, which provides the information needed by the browser to resolve module specifiers to URLs (JavaScript will throw a TypeError if it attempts to import a module specifier that can't be resolved to a module location).

Below you can see a map that defines a square module specifier key, which in this case maps to a relative address value.

<script type="importmap"> { "imports": { "square": "./shapes/square.js" } } </script>

With this map we can now use a bare name when we import the module:

import { name as squareName, draw } from "square";


 

Remapping Module Paths

Module specifier map entries, where both the specifier key and its associated value have a trailing forward slash (/), can be used as a path-prefix. This allows the remapping of a whole set of import URLs from one location to another. It can also be used to emulate working with "packages and modules", such as you might see in the Node ecosystem.

The trailing / indicates that the module specifier key can be substituted as part of a module specifier. If this is not present, the browser will only match (and substitute) the whole module specifier key.

Packages Of Modules

The following JSON import map definition maps lodash as a bare name, and the module specifier prefix lodash/ to the path /node_modules/lodash-es/ (resolved to the document base URL):

{ "imports": { "lodash": "/node_modules/lodash-es/lodash.js", "lodash/": "/node_modules/lodash-es/" } }

With this mapping you can import both the whole "package", using the bare name, and modules within it (using the path mapping):

import _ from "lodash"; import fp from "lodash/fp.js";

It is possible to import fp above without the .js file extension, but you would need to create a bare module specifier key for that file, such as lodash/fp, rather than using the path. This may be reasonable for just one module, but scales poorly if you wish to import many modules.


 

General URL Remapping

A module specifier key doesn't have to be path - it can also be an absolute URL (or a URL-like relative path like ./, ../, /). This may be useful if you want to remap a module that has absolute paths to a resource with your own local resources.

{ "imports": { "https://www.unpkg.com/moment/": "/node_modules/moment/" } }


 

Scoped Modules For Version Management

Ecosystems like Node use package managers such as npm to manage modules and their dependencies. The package manager ensures that each module is separated from other modules and their dependencies. As a result, while a complex application might include the same module multiple times with several different versions in different parts of the module graph, users do not need to think about this complexity.

You can also achieve version management using relative paths, but this is subpar because, among other things, this forces a particular structure on your project, and prevents you from using bare module names.

Import maps similarly allow you to have multiple versions of dependencies in your application and refer to them using the same module specifier. You implement this with the scopes key, which allows you to provide module specifier maps that will be used depending on the path of the script performing the import. The example below demonstrates this.

{ "imports": { "coolmodule": "/node_modules/coolmodule/index.js" }, "scopes": { "/node_modules/dependency/": { "coolmodule": "/node_modules/some/other/location/coolmodule/index.js" } } }

With this mapping, if a script with an URL that contains /node_modules/dependency/ imports coolmodule, the version in /node_modules/some/other/location/coolmodule/index.js will be used. The map in imports is used as a fallback if there is no matching scope in the scoped map, or the matching scopes don't contain a matching specifier. For example, if coolmodule is imported from a script with a non-matching scope path, then the module specifier map in imports will be used instead, mapping to the version in /node_modules/coolmodule/index.js.

Note that the path used to select a scope does not affect how the address is resolved. The value in the mapped path does not have to match the scopes path, and relative paths are still resolved to the base URL of the script that contains the import map.

Just as for module specifier maps, you can have many scope keys, and these may contain overlapping paths. If multiple scopes match the referrer URL, then the most specific scope path is checked first (the longest scope key) for a matching specifier. The browsers will fall back to the next most specific matching scoped path if there is no matching specifier, and so on. If there is no matching specifier in any of the matching scopes, the browser checks for a match in the module specifier map in the imports key.


 

Applying The Module To Your HTML

Now we just need to apply the main.js module to our HTML page. This is very similar to how we apply a regular script to a page, with a few notable differences.

First of all, you need to include type="module" in the <script> element, to declare this script as a module. To import the main.js script, we use this:

<script type="module" src="main.js"></script>

You can also embed the module's script directly into the HTML file by placing the JavaScript code within the body of the <script> element:

<script type="module"> /* JavaScript MODULE CODE HERE */ </script>

The script into which you import the module features basically acts as the top-level module. If you omit it, Firefox for example gives you an error of "SyntaxError: import declarations may only appear at top level of a module".

You can only use import and export statements inside modules, not regular scripts.


 

Differences Between Modules & Standard Scripts

Module-defined variables are scoped to the module unless explicitly attached to the global object. On the other hand, globally-defined variables are available within the module.
For example, given the following code:

<!DOCTYPE html> <html lang="en-US"> <head> <meta charset="UTF-8" /> <title></title> <link rel="stylesheet" href="" /> </head> <body> <div id="main"></div> <script> // A var statement creates a global variable. var text = "Hello"; </script> <script type="module" src="./render.js"></script> </body> </html>

/* render.js */ document.getElementById("main").innerText = text;

The page would still render Hello, because the global variables text and document are available in the module. (Also note from this example that a module doesn't necessarily need an import/export statement - the only thing needed is for the entry point to have type="module".)


 

Default Exports Versus Named Exports

The functionality we've exported so far has been comprised of named exports - each item (be it a function, const, etc.) has been referred to by its name upon export, and that name has been used to refer to it on import as well.

There is also a type of export called the default export - this is designed to make it easy to have a default function provided by a module, and also helps JavaScript modules to interoperate with existing CommonJS and AMD module systems.

Let's look at an example as we explain how it works. In our basic-modules square.js you can find a function called randomSquare() that creates a square with a random color, size, and position. We want to export this as our default, so at the bottom of the file we write this:

export default randomSquare;

Note the lack of curly braces.

We could instead prepend export default onto the function and define it as an anonymous function, like this:

export default function (ctx) { // ... }

Over in our main.js file, we import the default function using this line:

import randomSquare from "./modules/square.js";

Again, note the lack of curly braces. This is because there is only one default export allowed per module, and we know that randomSquare is it. The above line is basically shorthand for:

import { default as randomSquare } from "./modules/square.js";

Note: The as syntax for renaming exported items is explained below in the Renaming imports and exports section.


 

Avoiding Naming Conflicts

So far, our canvas shape drawing modules seem to be working OK. But what happens if we try to add a module that deals with drawing another shape, like a circle or triangle? These shapes would probably have associated functions like draw(), reportArea(), etc. too; if we tried to import different functions of the same name into the same top-level module file, we'd end up with conflicts and errors.

Fortunately there are a number of ways to get around this. We'll look at these in the following sections.


 

Renaming Imports And Exports

Inside your import and export statement's curly braces, you can use the keyword as along with a new feature name, to change the identifying name you will use for a feature inside the top-level module.

So for example, both of the following would do the same job, albeit in a slightly different way:

// inside module.js export { function1 as newFunctionName, function2 as anotherNewFunctionName }; // inside main.js import { newFunctionName, anotherNewFunctionName } from "./modules/module.js";

// inside module.js export { function1, function2 }; // inside main.js import { function1 as newFunctionName, function2 as anotherNewFunctionName, } from "./modules/module.js";

Let's look at a real example. In our renaming directory you'll see the same module system as in the previous example, except that we've added circle.js and triangle.js modules to draw and report on circles and triangles.

Inside each of these modules, we've got features with the same names being exported, and therefore each has the same export statement at the bottom:

export { name, draw, reportArea, reportPerimeter };

When importing these into main.js, if we tried to use

import { name, draw, reportArea, reportPerimeter } from "./modules/square.js"; import { name, draw, reportArea, reportPerimeter } from "./modules/circle.js"; import { name, draw, reportArea, reportPerimeter } from "./modules/triangle.js";

The browser would throw an error such as "SyntaxError: redeclaration of import name" (Firefox).

Instead we need to rename the imports so that they are unique:

import { name as squareName, draw as drawSquare, reportArea as reportSquareArea, reportPerimeter as reportSquarePerimeter, } from "./modules/square.js"; import { name as circleName, draw as drawCircle, reportArea as reportCircleArea, reportPerimeter as reportCirclePerimeter, } from "./modules/circle.js"; import { name as triangleName, draw as drawTriangle, reportArea as reportTriangleArea, reportPerimeter as reportTrianglePerimeter, } from "./modules/triangle.js";

Note that you could solve the problem in the module files instead, e.g.

// in square.js export { name as squareName, draw as drawSquare, reportArea as reportSquareArea, reportPerimeter as reportSquarePerimeter, };

// in main.js import { squareName, drawSquare, reportSquareArea, reportSquarePerimeter, } from "./modules/square.js";

And it would work just the same. What style you use is up to you, however it arguably makes more sense to leave your module code alone, and make the changes in the imports. This especially makes sense when you are importing from third party modules that you don't have any control over.

<!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <title>JavaScript module renaming example</title> <style> canvas { border: 1px solid black; } </style> <script type="module" src="main.js"></script> </head> <body> </body> </html>

import { create, createReportList } from './modules/canvas.js'; import { name as squareName, draw as drawSquare, reportArea as reportSquareArea, reportPerimeter as reportSquarePerimeter } from './modules/square.js'; import { name as circleName, draw as drawCircle, reportArea as reportCircleArea, reportPerimeter as reportCirclePerimeter } from './modules/circle.js'; import { name as triangleName, draw as drawTriangle, reportArea as reportTriangleArea, reportPerimeter as reportTrianglePerimeter } from './modules/triangle.js'; // create the canvas and reporting list let myCanvas = create('myCanvas', document.body, 480, 320); let reportList = createReportList(myCanvas.id); // draw a square let square1 = drawSquare(myCanvas.ctx, 50, 50, 100, 'blue'); reportSquareArea(square1.length, reportList); reportSquarePerimeter(square1.length, reportList); // draw a circle let circle1 = drawCircle(myCanvas.ctx, 75, 200, 100, 'green'); reportCircleArea(circle1.radius, reportList); reportCirclePerimeter(circle1.radius, reportList); // draw a triangle let triangle1 = drawTriangle(myCanvas.ctx, 100, 75, 190, 'yellow'); reportTriangleArea(triangle1.length, reportList); reportTrianglePerimeter(triangle1.length, reportList);

function create(id, parent, width, height) { let divWrapper = document.createElement('div'); let canvasElem = document.createElement('canvas'); parent.appendChild(divWrapper); divWrapper.appendChild(canvasElem); divWrapper.id = id; canvasElem.width = width; canvasElem.height = height; let ctx = canvasElem.getContext('2d'); return { ctx: ctx, id: id }; } function createReportList(wrapperId) { let list = document.createElement('ul'); list.id = wrapperId + '-reporter'; let canvasWrapper = document.getElementById(wrapperId); canvasWrapper.appendChild(list); return list.id; } export { create, createReportList };

const name = 'circle'; function degToRad(degrees) { return degrees * Math.PI / 180; }; function draw(ctx, radius, x, y, color) { ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, radius, degToRad(0), degToRad(360), false); ctx.fill(); return { radius: radius, x: x, y: y, color: color }; } function reportArea(radius, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} area is ${Math.round(Math.PI * (radius * radius))}px squared.` let list = document.getElementById(listId); list.appendChild(listItem); } function reportPerimeter(radius, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} circumference is ${Math.round(2 * Math.PI * radius)}px.` let list = document.getElementById(listId); list.appendChild(listItem); } export { name, draw, reportArea, reportPerimeter };

const name = 'square'; function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.fillRect(x, y, length, length); return { length: length, x: x, y: y, color: color }; } function reportArea(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} area is ${length * length}px squared.` let list = document.getElementById(listId); list.appendChild(listItem); } function reportPerimeter(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} perimeter is ${length * 4}px.` let list = document.getElementById(listId); list.appendChild(listItem); } export { name, draw, reportArea, reportPerimeter };

const name = 'triangle'; function degToRad(degrees) { return degrees * Math.PI / 180; }; function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + length, y); let triHeight = (length/2) * Math.tan(degToRad(60)); ctx.lineTo(x + (length/2), y + triHeight); ctx.lineTo(x, y); ctx.fill(); return { length: length, x: x, y: y, color: color }; } function reportArea(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} area is ${Math.round((Math.sqrt(3)/4)*(length * length))}px squared.` let list = document.getElementById(listId); list.appendChild(listItem); } function reportPerimeter(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} perimeter is ${length * 3}px.` let list = document.getElementById(listId); list.appendChild(listItem); } export { name, draw, reportArea, reportPerimeter };


 

Creating A Module Object

Import each module's features inside a module object. The following syntax form does that:

import * as Module from "./modules/module.js";

This grabs all the exports available inside module.js, and makes them available as members of an object Module, effectively giving it its own namespace. So for example:

Module.function1(); Module.function2();

<!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <title>JavaScript module object example</title> <style> canvas { border: 1px solid black; } </style> <script type="module" src="main.js"></script> </head> <body> </body> </html>

import * as Canvas from './modules/canvas.js'; import * as Square from './modules/square.js'; import * as Circle from './modules/circle.js'; import * as Triangle from './modules/triangle.js'; // create the canvas and reporting list let myCanvas = Canvas.create('myCanvas', document.body, 480, 320); let reportList = Canvas.createReportList(myCanvas.id); // draw a square let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue'); Square.reportArea(square1.length, reportList); Square.reportPerimeter(square1.length, reportList); // draw a circle let circle1 = Circle.draw(myCanvas.ctx, 75, 200, 100, 'green'); Circle.reportArea(circle1.radius, reportList); Circle.reportPerimeter(circle1.radius, reportList); // draw a triangle let triangle1 = Triangle.draw(myCanvas.ctx, 100, 75, 190, 'yellow'); Triangle.reportArea(triangle1.length, reportList); Triangle.reportPerimeter(triangle1.length, reportList);

function create(id, parent, width, height) { let divWrapper = document.createElement('div'); let canvasElem = document.createElement('canvas'); parent.appendChild(divWrapper); divWrapper.appendChild(canvasElem); divWrapper.id = id; canvasElem.width = width; canvasElem.height = height; let ctx = canvasElem.getContext('2d'); return { ctx: ctx, id: id }; } function createReportList(wrapperId) { let list = document.createElement('ul'); list.id = wrapperId + '-reporter'; let canvasWrapper = document.getElementById(wrapperId); canvasWrapper.appendChild(list); return list.id; } export { create, createReportList };

const name = 'circle'; function degToRad(degrees) { return degrees * Math.PI / 180; }; function draw(ctx, radius, x, y, color) { ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x, y, radius, degToRad(0), degToRad(360), false); ctx.fill(); return { radius: radius, x: x, y: y, color: color }; } function reportArea(radius, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} area is ${Math.round(Math.PI * (radius * radius))}px squared.` let list = document.getElementById(listId); list.appendChild(listItem); } function reportPerimeter(radius, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} circumference is ${Math.round(2 * Math.PI * radius)}px.` let list = document.getElementById(listId); list.appendChild(listItem); } export { name, draw, reportArea, reportPerimeter };

const name = 'square'; function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.fillRect(x, y, length, length); return { length: length, x: x, y: y, color: color }; } function reportArea(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} area is ${length * length}px squared.` let list = document.getElementById(listId); list.appendChild(listItem); } function reportPerimeter(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} perimeter is ${length * 4}px.` let list = document.getElementById(listId); list.appendChild(listItem); } export { name, draw, reportArea, reportPerimeter };

const name = 'triangle'; function degToRad(degrees) { return degrees * Math.PI / 180; }; function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + length, y); let triHeight = (length/2) * Math.tan(degToRad(60)); ctx.lineTo(x + (length/2), y + triHeight); ctx.lineTo(x, y); ctx.fill(); return { length: length, x: x, y: y, color: color }; } function reportArea(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} area is ${Math.round((Math.sqrt(3)/4)*(length * length))}px squared.` let list = document.getElementById(listId); list.appendChild(listItem); } function reportPerimeter(length, listId) { let listItem = document.createElement('li'); listItem.textContent = `${name} perimeter is ${length * 3}px.` let list = document.getElementById(listId); list.appendChild(listItem); } export { name, draw, reportArea, reportPerimeter };


 

Modules & Classes

You can also export and import classes; this is another option for avoiding conflicts in your code, and is especially useful if you've already got your module code written in an object-oriented style.

You can see an example of our shape drawing module rewritten with ES classes in our classes directory. As an example, the square.js file now contains all its functionality in a single class:

class Square { constructor(ctx, listId, length, x, y, color) { // ... } draw() { // ... } // ... }

which we then export:

export { Square };

Over in main.js, we import it like this:

import { Square } from "./modules/square.js";

And then use the class to draw our square:

const square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, "blue"); square1.draw(); square1.reportArea(); square1.reportPerimeter();

<!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <title>JavaScript module class example</title> <style> canvas { border: 1px solid black; } </style> <script type="module" src="main.js"></script> </head> <body> </body> </html>

import { Canvas } from './modules/canvas.js'; import { Square } from './modules/square.js'; import { Circle } from './modules/circle.js'; import { Triangle } from './modules/triangle.js'; // create the canvas and reporting list let myCanvas = new Canvas('myCanvas', document.body, 480, 320); myCanvas.create(); myCanvas.createReportList(); // draw a square let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue'); square1.draw(); square1.reportArea(); square1.reportPerimeter(); // draw a circle let circle1 = new Circle(myCanvas.ctx, myCanvas.listId, 75, 200, 100, 'green'); circle1.draw(); circle1.reportArea(); circle1.reportPerimeter(); // draw a triangle let triangle1 = new Triangle(myCanvas.ctx, myCanvas.listId, 100, 75, 190, 'yellow'); triangle1.draw(); triangle1.reportArea(); triangle1.reportPerimeter();

class Canvas { constructor(id, parent, width, height) { this.id = id; this.listId = null; this.parent = parent; this.width = width; this.height = height; this.ctx = null; } // new class stuff above here create() { if(this.ctx !== null) { console.log('Canvas already created!'); return; } else { let divWrapper = document.createElement('div'); let canvasElem = document.createElement('canvas'); this.parent.appendChild(divWrapper); divWrapper.appendChild(canvasElem); divWrapper.id = this.id; canvasElem.width = this.width; canvasElem.height = this.height; this.ctx = canvasElem.getContext('2d'); } } createReportList() { if(this.listId !== null) { console.log('Report list already created!'); return; } else { let list = document.createElement('ul'); list.id = this.id + '-reporter'; let canvasWrapper = document.getElementById(this.id); canvasWrapper.appendChild(list); this.listId = list.id; } } } export { Canvas };

function degToRad(degrees) { return degrees * Math.PI / 180; } class Circle { constructor(ctx, listId, radius, x, y, color) { this.ctx = ctx; this.listId = listId; this.radius = radius; this.x = x; this.y = y; this.color = color; this.name = 'circle'; } draw() { this.ctx.fillStyle = this.color; this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.radius, degToRad(0), degToRad(360), false); this.ctx.fill(); } reportArea() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} area is ${Math.round(Math.PI * (this.radius * this.radius))}px squared.` let list = document.getElementById(this.listId); list.appendChild(listItem); } reportPerimeter() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} circumference is ${Math.round(2 * Math.PI * this.radius)}px.` let list = document.getElementById(this.listId); list.appendChild(listItem); } } export { Circle };

class Square { constructor(ctx, listId, length, x, y, color) { this.ctx = ctx; this.listId = listId; this.length = length; this.x = x; this.y = y; this.color = color; this.name = 'square'; } draw() { this.ctx.fillStyle = this.color; this.ctx.fillRect(this.x, this.y, this.length, this.length); } reportArea() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} area is ${this.length * this.length}px squared.` let list = document.getElementById(this.listId); list.appendChild(listItem); } reportPerimeter() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} perimeter is ${this.length * 4}px.` let list = document.getElementById(this.listId); list.appendChild(listItem); } } export { Square };

function degToRad(degrees) { return degrees * Math.PI / 180; }; class Triangle { constructor(ctx, listId, length, x, y, color) { this.ctx = ctx; this.listId = listId; this.length = length; this.x = x; this.y = y; this.color = color; this.name = 'triangle'; } draw() { this.ctx.fillStyle = this.color; this.ctx.beginPath(); this.ctx.moveTo(this.x, this.y); this.ctx.lineTo(this.x + this.length, this.y); let triHeight = (this.length/2) * Math.tan(degToRad(60)); this.ctx.lineTo(this.x + (this.length/2), this.y + triHeight); this.ctx.lineTo(this.x, this.y); this.ctx.fill(); } reportArea() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} area is ${Math.round((Math.sqrt(3)/4)*(this.length * this.length))}px squared.` let list = document.getElementById(this.listId); list.appendChild(listItem); } reportPerimeter() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} perimeter is ${this.length * 3}px.` let list = document.getElementById(this.listId); list.appendChild(listItem); } } export { Triangle };


 

Aggregating Modules

There will be times where you'll want to aggregate modules together. You might have multiple levels of dependencies, where you want to simplify things, combining several submodules into one parent module. This is possible using export syntax of the following forms in the parent module:

export * from "x.js"; export { name } from "x.js";

For an example, see our module-aggregation directory. In this example we've got an extra module called shapes.js, which aggregates all the functionality from:

together.
 
We've also moved our submodules inside a subdirectory inside the modules directory called shapes. So the module structure in this example is:

modules/ canvas.js shapes.js shapes/ circle.js square.js triangle.js

In each of the submodules, the export is of the same form, e.g.

export { Square };

Next up comes the aggregation part. Inside shapes.js, we include the following lines:

export { Square } from "./shapes/square.js"; export { Triangle } from "./shapes/triangle.js"; export { Circle } from "./shapes/circle.js";

These grab the exports from the individual submodules and effectively make them available from the shapes.js module.

The exports referenced in shapes.js basically get redirected through the file and don't really exist there, so you won't be able to write any useful related code inside the same file.

So now in the main.js file, we can get access to all three module classes by replacing

import { Square } from "./modules/square.js"; import { Circle } from "./modules/circle.js"; import { Triangle } from "./modules/triangle.js";

with the following single line:

import { Square, Circle, Triangle } from "./modules/shapes.js";

<!DOCTYPE html> <html lang="en-US"> <head> <meta charset="utf-8"> <title>JavaScript module aggregation example</title> <style> canvas { border: 1px solid black; } </style> <script type="module" src="main.js"></script> </head> <body> </body> </html>

import { Canvas } from './modules/canvas.js'; import { Square, Circle, Triangle } from './modules/shapes.js'; // create the canvas and reporting list let myCanvas = new Canvas('myCanvas', document.body, 480, 320); myCanvas.create(); myCanvas.createReportList(); // draw a square let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue'); square1.draw(); square1.reportArea(); square1.reportPerimeter(); // draw a circle let circle1 = new Circle(myCanvas.ctx, myCanvas.listId, 75, 200, 100, 'green'); circle1.draw(); circle1.reportArea(); circle1.reportPerimeter(); // draw a triangle let triangle1 = new Triangle(myCanvas.ctx, myCanvas.listId, 100, 75, 190, 'yellow'); triangle1.draw(); triangle1.reportArea(); triangle1.reportPerimeter();

class Canvas { constructor(id, parent, width, height) { this.id = id; this.listId = null; this.parent = parent; this.width = width; this.height = height; this.ctx = null; } // new class stuff above here create() { if(this.ctx !== null) { console.log('Canvas already created!'); return; } else { let divWrapper = document.createElement('div'); let canvasElem = document.createElement('canvas'); this.parent.appendChild(divWrapper); divWrapper.appendChild(canvasElem); divWrapper.id = this.id; canvasElem.width = this.width; canvasElem.height = this.height; this.ctx = canvasElem.getContext('2d'); } } createReportList() { if(this.listId !== null) { console.log('Report list already created!'); return; } else { let list = document.createElement('ul'); list.id = this.id + '-reporter'; let canvasWrapper = document.getElementById(this.id); canvasWrapper.appendChild(list); this.listId = list.id; } } } export { Canvas };

export { Square } from './shapes/square.js'; export { Triangle } from './shapes/triangle.js'; export { Circle } from './shapes/circle.js';

function degToRad(degrees) { return degrees * Math.PI / 180; } class Circle { constructor(ctx, listId, radius, x, y, color) { this.ctx = ctx; this.listId = listId; this.radius = radius; this.x = x; this.y = y; this.color = color; this.name = 'circle'; } draw() { this.ctx.fillStyle = this.color; this.ctx.beginPath(); this.ctx.arc(this.x, this.y, this.radius, degToRad(0), degToRad(360), false); this.ctx.fill(); } reportArea() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} area is ${Math.round(Math.PI * (this.radius * this.radius))}px squared.` let list = document.getElementById(this.listId); list.appendChild(listItem); } reportPerimeter() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} circumference is ${Math.round(2 * Math.PI * this.radius)}px.` let list = document.getElementById(this.listId); list.appendChild(listItem); } } export { Circle };

class Square { constructor(ctx, listId, length, x, y, color) { this.ctx = ctx; this.listId = listId; this.length = length; this.x = x; this.y = y; this.color = color; this.name = 'square'; } draw() { this.ctx.fillStyle = this.color; this.ctx.fillRect(this.x, this.y, this.length, this.length); } reportArea() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} area is ${this.length * this.length}px squared.` let list = document.getElementById(this.listId); list.appendChild(listItem); } reportPerimeter() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} perimeter is ${this.length * 4}px.` let list = document.getElementById(this.listId); list.appendChild(listItem); } } export { Square };

function degToRad(degrees) { return degrees * Math.PI / 180; }; class Triangle { constructor(ctx, listId, length, x, y, color) { this.ctx = ctx; this.listId = listId; this.length = length; this.x = x; this.y = y; this.color = color; this.name = 'triangle'; } draw() { this.ctx.fillStyle = this.color; this.ctx.beginPath(); this.ctx.moveTo(this.x, this.y); this.ctx.lineTo(this.x + this.length, this.y); let triHeight = (this.length/2) * Math.tan(degToRad(60)); this.ctx.lineTo(this.x + (this.length/2), this.y + triHeight); this.ctx.lineTo(this.x, this.y); this.ctx.fill(); } reportArea() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} area is ${Math.round((Math.sqrt(3)/4)*(this.length * this.length))}px squared.` let list = document.getElementById(this.listId); list.appendChild(listItem); } reportPerimeter() { let listItem = document.createElement('li'); listItem.textContent = `${this.name} perimeter is ${this.length * 3}px.` let list = document.getElementById(this.listId); list.appendChild(listItem); } } export { Triangle };