2

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.

Danny
  • 475
  • 6
  • 19
  • 1
    `document` likely isn't accessible due to the fact that the site content is being statically generated server side. As far as using scripts, there's a pretty thorough section on [JavaScript Data Files](https://www.11ty.dev/docs/data-js/). – Brian Lee Nov 24 '20 at 06:23

1 Answers1

7

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.

MoritzLost
  • 2,611
  • 2
  • 18
  • 32
  • 1
    Thanks, @moritzlost. This is a great description of both the solution and how to understand and approach _build-time_ vs _client-side at runtime_ in JavaScript. The _client-side at runtime_ situation blocks __data/fruits.json_ from being accessible in its current form. I'd like that data to also be used in the Eleventy for loop but this is out of the scope of my question so can ask it elsewhere. – Danny Nov 25 '20 at 07:03
  • 2
    @Danny You just need to make sure the fruits.json gets moved to the output directory as well, then you can load it dynamically on the client-side using JavaScript. [This snippet](https://stackoverflow.com/a/50835527/3167371) should work fine in your case (adjust the filename to match the location of the file in your output directory). – MoritzLost Nov 25 '20 at 08:18