What's the best way to perform small client-side JavaScript in Eleventy?

1.5k views Asked by At

Problem

I'd like to add a small bit of client-side JavaScript to my Eleventy website. I can't seem to access document. using Eleventy which means I can't access elements and listen to them for events. Example of what doesn't work:

const formElement = document.querySelector("form")

The error message I receive from Eleventy:

ReferenceError: document is not defined

Question

How do I work with Eleventy in order to listen to document element changes and make page changes?

Example:

formElement.addEventListener("change", function () {
    // Update nearby paragraph element based on form value
});

My real-world scenario: I would like to have a paragraph element display which of the form's input type="radio" has the value checked.

Approach so far

I have a file in /data called fruits.json:

{
  "items": [
    {
      "name": "Apple"
    },
    {
      "name": "Banana"
    },
    {
      "name": "Strawberry"
    },
    {
      "name": "Mango"
    },
    {
      "name": "Peach"
    },
    {
      "name": "Watermelon"
    },
    {
      "name": "Blueberry"
    }
  ]
}

And a HTML file in /_includes/layouts based on my base.html file:

{% extends "layouts/base.html" %}

{% block content %}

<form>
 {% for item in fruits.items %}
 {# Create a radio button for each, with the first one checked by default #}
 <input type="radio" name="screen" id="{{ item.name | slug }}" value="{{ item.name | slug }}" {% if loop.index === 1 %} checked {% endif %}>
    <label for="{{ item.name | slug }}">{{ item.name }}</label>
 {% endfor %}

{% set selectedFruit = helpers.getSelectedFruit() %}
<p>Currently selected item from above is: {{ selectedFruit }}</p>
</form>

{% endblock %}

Note that thee variable called selectedFruit is assigned to a helper function:

{% set selectedScreen = helpers.getSelectedScreen() %}

That getSelectedScreen() function looks like:

getSelectedScreen() {
    const formEl = document.querySelector("form")
    console.log(formEl)
}

Aside from not being able to work with .document, I feel like this approach is probably 'against the grain' of Eleventy, static site generators in other ways:

  • The script is being called mid-document
  • The script is one-off and away from its context

I wonder if I'm approaching this wrong in the first place, or if I just need to do something to allow .document access.

1

There are 1 answers

2
MoritzLost On BEST ANSWER

There are some misconceptions here — the most important distinction for your JavaScript code is whether it's executed at build time or client-side at runtime.

The Eleventy core as well as your .eleventy.js configuration file are written in JavaScript which is executed once during the build step, i.e. when your static site is being generated. This happens in a NodeJS environment, not in a browser, which is why there's no document variable and and no DOM.

If you want to dynamically change something on your site in response to user interaction, you need to write a separate JavaScript file which is copied to the output directory of your static site. Then you can include it in the HTML template for your static sites so it's included during normal page visits after your site is deployed.

First, modify your template to only generate a placeholder element for your JavaScript function to add text to later:

{% extends "layouts/base.html" %}

{% block content %}

<form id="fruits-form">
 {% for item in fruits.items %}
 {# Create a radio button for each, with the first one checked by default #}
 <input type="radio" name="screen" id="{{ item.name | slug }}" value="{{ item.name | slug }}" {% if loop.index === 1 %} checked {% endif %}>
    <label for="{{ item.name | slug }}">{{ item.name }}</label>
 {% endfor %}

  <p id="selected-fruits-output"></p>
</form>

{% endblock %}

Then, create a JavaScript file which reacts to change events on the form:

// fruit-form.js
const fruitForm = document.getElementById('fruits-form');
const formOutput = document.getElementById('selected-fruits-output');
fruitForm.addEventListener('change', e => {
    // update the formOutput with the list of selected fruits
});

Now you need to make sure this javascript file is copied to your output directory, using Passthrough file copy:

eleventyConfig.addPassthroughCopy("path/to/fruit-form.js");

Finally, make sure to include the script element in your HTML template (make sure the path is an absolute path to the output as specified above):

{# layouts/base.html #}
<script src="/path/to/fruit-form.js" defer></script>

Now it should work as expected. In general, make sure to understand the difference between build-time and runtime JavaScript, so you can decide which will work best in different situations.