axelerator.de

Episode 1: Project setup for the Elm Tetris clone

1 minute
#elm
30 June 2021

In this episode I shared my hot code replace setup for Elm explorations with you. After struggling with the limitations of elm reactor I build this little boiler plate setup to shorten my feedback loop.

Here is the link to the “coming from JavaScript of the official guide” showing the diffrences between Elm and JavaScript syntax.

If you want to use the boilerplate you can use the hot_elm folder from my “Elm nano examples” directory. Just execute the ./bin/build.sh (or ./bin/build_linux) in the project folder. It will listen to changes until you cancel it with CTRL+C

In the shell should look something like this:

--> ./bin/build.sh
Compiling ⚔️
Compiled without errors
^C
`--> 

You should then be able to open the index.html in the browser directly from disk and it will reload the Elm app and display build errors if present.

The main driver of the setup is a shell script that watches the source directory for changes and triggers a recompile if necessary.

build.sh

Visit build.sh in context with the other files for this nano example on Github

#!/bin/sh
trap ctrl_c INT

function ctrl_c() {
        exit 0
}

function append() {
ASSETS="$ASSETS\n$1"
}

function report() {
  touch tmp/build.log
  ERRORS=`cat tmp/build.log`
  if [ -n "$ERRORS" ]; then
    echo "Comiled with errors"
    # to also print errors in console we just compile a second time
    elm make src/Main.elm
    VALUE=`date -r tmp/build.log`
    printf "refresh('" > tmp/timestamp.js
    printf "$VALUE" >>  tmp/timestamp.js
    printf "', " >>  tmp/timestamp.js
    cat tmp/build.log >> tmp/timestamp.js
    printf ");" >> tmp/timestamp.js
  else
    echo "Compiled without errors"
    VALUE=`date -r elm.js`
    TIMESTAMP_JS_TEMPLATE="refresh('${VALUE}')"
    INTERPOLATED=`echo "${TIMESTAMP_JS_TEMPLATE}" | sed "s/VALUE/${VALUE}/" | sed "s/ERROR//" `
    echo "$INTERPOLATED" > tmp/timestamp.js
  fi
}


function buildCode() {
  echo "Compiling ⚔️"
  elm make src/Main.elm --output=elm.js --report=json 2> tmp/build.log
  report
}

while true; do
  buildCode
  fswatch  --event PlatformSpecific src/ assets/ -1
done

The other part is a vanilla JavaScript file that keeps polling a file that is written on each build and contains a timestamp and potential error messages.

loader.js

Visit loader.js in context with the other files for this nano example on Github


function loadElm() {
  window.Elm = undefined;
  const scriptTag = mkScriptTag('elm-include', 'elm.js');
  scriptTag.addEventListener('load', function() {
    initElm();
  });
}

function initElm() {
  const main = document.querySelector('main')
  while (main.firstChild) {
    main.removeChild(main.lastChild);
  }
  var main_content = document.createElement("div");
  main_content.setAttribute('id', main_content);
  main.appendChild(main_content);

  const app = Elm.Main.init({
    node: main_content
  });
}

const hotReloadInterval = window.setInterval(checkTimestamp, 1000)
window.lastTimestamp = '';
const parsedUrl = new URL(window.location.href);
const pathPrefix = parsedUrl.pathname.replace('/index.html', '');


function buildError(error) {
  const outerDiv = document.createElement("div"); 
  const path = error.path.replace(pathPrefix, '');
  const newContent = document.createTextNode(path);
  outerDiv.appendChild(newContent);

  const problems = document.createElement("ul"); 
  error.problems.forEach(function(p){
    const li = document.createElement('li');
    const pre = document.createElement('pre');
    li.appendChild(pre);
    p.message.forEach(function(message) {
      if ((typeof message) == 'string') {
        pre.appendChild(document.createTextNode(message));
      } else {
        const span = document.createElement('span');
        span.setAttribute("style", "color:" + message.color);
        span.appendChild(document.createTextNode(message.string));
        pre.appendChild(span);
      }
    });
    problems.appendChild(li);
  });

  outerDiv.appendChild(problems);
  return outerDiv;
}

function showErrors(errors) {
  const oldScript = document.getElementById('build-errors');
  if (oldScript) {
    oldScript.remove();
  }
  if (!errors) return;
  const newScript = document.createElement("div"); 
  newScript.setAttribute("id", "build-errors");

  errors.errors.forEach(function(e){
    newScript.appendChild(buildError(e));
  });
  // add the newly created element and its content into the DOM 
  document.getElementById('build-info').appendChild(newScript);
}

function refresh(timestamp, errors) {
  if (window.lastTimestamp == '') {
    if (errors) { 
      showErrors(errors);
    }
  } else if (window.lastTimestamp != timestamp) {
    showErrors(errors);
    if (!errors) {
      loadElm();
    }
  }
  window.lastTimestamp = timestamp;
}

function mkScriptTag(id, filename) {
  const oldScript = document.getElementById(id);
  if (oldScript) {
    oldScript.remove();
  }
  const newScript = document.createElement("script"); 

  newScript.setAttribute("id", id);
  newScript.setAttribute("type", "text/javascript");
  newScript.setAttribute("src", filename );

  // add the newly created element and its content into the DOM 
  document.body.appendChild(newScript);
  return newScript;
}

function checkTimestamp () { 
  mkScriptTag('timestampjs',"tmp/timestamp.js");
}
  
loadElm();