Episode 1: Project setup for the Elm Tetris clone
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();