Note: started this a month ago, still WIP, but definitely willing to continue soon ^^.
This is a tutorial about how to create a web application where your client logic is written in Elm. If you like the readability, composability and type-safety of Haskell programs combined with the powerful paradigms of functional reactive programming, then Elm might be for you. To learn more about the language you might want to read http://elm-lang.org/docs.
This document jumps right in to create a complete Elm web application from scratch, starting with a very simple static web site, which then gets scaled up into a modular web application.
Install the Elm binaries, if you are on a Unix platform, simply run
$ npm install --global elm
otherwise follow instructions on http://elm-lang.org/install.
Go in your application directory of choice, for example:
$ cd ~/apps
then create the project directory and change into it:
$ mkdir my-first-elm-app && cd $_
Now we are ready to create the first elm source code. Let's create a file named app.elm right in our projet root and put the following content in it:
main =
span
[class "welcome-message"]
[text "Hello, World!"]
That's enough content for now, we can extend this later once the environment is set up.
Note: If you don't have syntax highlighing for Elm, now would be a good time to install the according package. There are packages for Sublime, Atom, Vim etc. available.
Elm code is transpiled into JavaScript code. Let's do this with elm-make:
$ elm-make app.elm
Elm will probably tell you that some new packages are needed, like:
Some new packages are needed. Here is the upgrade plan.
Install:
elm-lang/core 2.1.0
Do you approve of this plan? (y/n)
Approve this and elm-make will install the elm-lang/core package.
Then the compiler will tell you that there are some errors in you source file:
## ERRORS in app.elm ###########################################################
-- NAMING ERROR -------------------------------------------------------- app.elm
Cannot find variable `span`
1| main = span [class "welcome-message"] [text "Hello, World!"]
^^^^
Maybe you want one of the following?
atan
sin
tan
Basics.atan
-- NAMING ERROR -------------------------------------------------------- app.elm
Cannot find variable `class`
1| main = span [class "welcome-message"] [text "Hello, World!"]
^^^^^
Maybe you want one of the following?
clamp
Basics.clamp
-- NAMING ERROR -------------------------------------------------------- app.elm
Cannot find variable `text`
1| main = span [class "welcome-message"] [text "Hello, World!"]
^^^^
Detected errors in 1 module.
That's because the compiler can't find the functions span
, class
and text
,
and tries to guess what you could mean instead. On the homepage there is no
mention that those functions are part of the package
elm-html that needs to be imported before
we can use its functions.
So let's go ahead and install the missing dependency:
$ elm-package install evancz/elm-html
and agree to install the package. Elm will also suggest to install the virtual-dom package which we also agree. Elm will download the packages and hopefully finish with
Downloading evancz/elm-html
Downloading evancz/virtual-dom
Packages configured successfully!
Now we can load the three functions in our app.elm source file. We have to pay
attention to the package structure. If we inspect the
package library
we can see that "class" is exposed by Html.Attributes
whereas the other two
functions are exposed by Html
directly. Now we can import the three functions
accordingly in our source file:
import Html exposing (span, text)
import Html.Attributes exposing (class)
main =
span
[class "welcome-message"]
[text "Hello, World!"]
Just for completeness, the code above will create the following HTML:
<span class="welcome-message">Hello, World!</span>
We will dive later into more complex examples of how to construct DOM trees with Elm function composition.
Now we can re-run our elm-make command from above
$ elm-make app.elm
and this time it will hopefully finish with something like
Success! Compiled 6 modules.
Successfully generated elm.js
Now as our program has been successfully compiled, we can include the generated JavaScript code in a HTML file to view it in a browser.
Let's create a index.html file with the following content:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1, user-scalable=no">
<title>Elm app</title>
</head>
<body>
<div id="app"></div>
</body>
<!-- load elm library + compiled application -->
<script src="elm.js"></script>
<!-- embed and start app -->
<script>
var appContainer = document.getElementById('app');
Elm.embed(Elm.Main, appContainer);
</script>
</html>
To explain the code above: The elm.js contains the complete Elm library source
code plus our compiled elm program (the app.elm). We are loading it via script
tag into our browser, but we still need to "start" the application. This can be
done with the function Elm.embed
(other options of embedding can be
found here). We are passing our module as
argument to the function, which is references as Elm.Main (Main is the default
module name as we have not specified one in our source file). We will have a
closer look at module at a later point.
Now let's open the file in a browser:
$ open index.html
Note: open should open the file in your default browser. If that's not the case then replace the command with firefox, google-chrome or whatever.
Now you should see the generate output of our elm program in the browser:
If we inspect our DOM tree we can see that Elm has appended the HTML snippet from above as child of the body tag:
Now we have a minimal complete Elm application running which compiles into JavaScript which can then be included into a HTML document and displayed in a web browser. This example can easily be extended to display a more complex DOM tree.
So far our little project consists of thre files:
- app.elm
- elm.js
- index.html
The Elm compiler also created an elm-package.json file which lists dependencies and project properties (similar to node's package.json) as well as an elm-stuff directory which contains packages that are used in our app.
As our project will grow it is good practice to invest some time to set up a good development environment (including deployment) to create a proper modular application structure and to have reccuring tasks automated. In this chapter we will take care of that.
If you are not interested by this or already have your own way managing your development process you can skip this chapter and jump right into the next chapter where we will extend our previously developed Elm application.
Currently all our files are in the project root. This will become very confusing as our application grows.
First, we could separate our source files from generated files. Let's create the following folder structure:
.
└── src
├── elm
│ └── app.elm
└── html
└── index.html
and move your app.elm and the index.html accordingly. You can also delete the previously generated elm.js file from your project root.
Our plan is to create a public directory with only the index.html and the app.js in it, which will be the only things that is deployed to production:
.
└── public
├── elm.js
└── index.html
Now we have to amend our build process a bit:
$ mkdir public
$ cp src/html/index.html public/
$ elm-make src/elm/app.elm --output public/elm.js
Now we can run open public/index.html
to see our application running as
before (we still can see "Hello, World!").
It is time to use a version control system so we can keep track of our changes in a history and work on the code from different places or via collaboration.
Inside your project root folder, initialize a new git repository:
$ git init
You may wanna create a .gitignore file and ignore the elm-stuff folder (those dependencies will be created via the elm package manager) as well as the public folder as it's content is copied or compiled and can be reproduced any time.
# .gitignore
elm-stuff
public
It is also good practice to always have a readme.md file, so we could create one now:
# My first elm app
Application to follow along https://github.com/j-hannes/elm-tutorial.
Now we can add our current program to the repository:
$ git add .
commit the changes with
$ git commit -v
and enter a commit message ("Generate first files" or something) to complete the commit. Your commit should contain the following files
# new file: .gitignore
# new file: elm-package.json
# new file: readme.md
# new file: src/elm/app.elm
# new file: src/html/index.html
If we want we can also create a new github repository and add it as remote repository to our project:
$ git remote add origin [email protected]:my-username/my-repository
And eventually push your code via:
$ git push --set-upstream origin master
From here our project is under version control which makes it easy to undo/redo changes and to add new features via branching etc.
If we look at the number of commands required for our build we can easily see that it would be nice to put that into an automated process / script. Another advantage is that everybody who checks out and uses our app will build things in the same way.
So let's go ahead and use Gulp as build system for this purpose (if you haven't already):
$ npm install --global gulp
And then we can create a gulpfile.js where we create some tasks. Let's start with the first task to remove the public folder:
// gulpfile.js
var del = require('del')
var gulp = require('gulp')
gulp.task('clean', function(callback) {
del(['public'], callback)
})
Before we can run this we need to install two npm dependencies. Let's first initialize a new package.json file with
$ echo "{}" > package.json
Now we can install the dependencies with
$ npm install --save gulp del
our package.json should now look like:
{
"dependencies": {
"del": "^1.2.0",
"gulp": "^3.9.0"
}
}
If you prefer simplicity you can leave it like that. If you prefer completeness
then you can add a name field, description field etc. (or initialize your
repository via npm init
).
Since we are not including external dependencies in our git repository, we need to add the node_modules folder to our .gitignore file.
Let's test our first gulp task:
$ gulp clean
The public folder should now be gone.
Now we can add another task to copy the index.html:
gulp.task('html', function() {
gulp
.src('./src/html/index.html')
.pipe(gulp.dest('./public/'))
})
Let's test our second gulp task:
$ gulp html
Now we should find a public folder again with our index.html in it.
Finally we need to create a task to compile the elm source code. We can use the gulp-elm plugin for that:
// ...
var elm = require('gulp-elm')
// ...
gulp.task('elm', function() {
gulp
.src('src/elm/app.elm')
.pipe(elm())
.pipe(gulp.dest('public'))
})
Don't forget to run
$ npm install --save gulp-elm
If we now run
$ gulp elm
we will also find the elm.js app.js in the public folder. Notice that the
output file has changed it's name to app.js. We are ok with that and will just
change the name in our index.html:
<!-- ... -->
<body></body>
<script src="app.js"></script>
<script>Elm.fullscreen(Elm.Main)</script>
and finally we can run
$ open public/index.html
and our application should work as before.
Now we can compose all three steps together into a build task:
gulp.task('build', ['clean', 'html', 'elm'])
And from here we can always create a clean build at any time with:
$ gulp build
Now we can recompile our application with one command and provided an option for all collaborators on this project to do this step consistently.
At the moment we would need to run gulp build
and refresh the browser every
time when we make changes on the Elm source code and want to see the resulting
output.
We can speed this process up quite a bit by serving the index.html by a small webserver and include a task that watches files for change and automatically refreshes the browser (live reload).
To achieve this we, first we need a task that runs a webserver:
var connect = require('gulp-connect')
// ...
gulp.task('connect', function() {
connect.server({
root: 'public',
livereload: true
})
})
We need to install our new dependency via npm install --save gulp-connect
.
Then we can create a task that watches our source files for change and triggers tasks on change accordingly:
gulp.task('watch', function() {
gulp.watch('src/elm/app.elm', ['elm'])
gulp.watch('src/html/index.html', ['html'])
})
Now we just need to add another step to each of the tasks elm and html:
.pipe(connect.reload())
Note: If you are getting confused where to put which code, you can have a look at the complete file.
Finally we can add a dev task for us to run the server with live-reload:
gulp.task('dev', ['build', 'connect', 'watch'])
Now we can run gulp dev to start our server as well as live-reload in the background. In your browser you can visit http://localhost:8080 to see your application running. If you now make any changes to the app.elm or the index.html your browser should reload and your changes can be seen immediately.
We could now add the automated tasks to our documentation (readme.md):
## Build
$ npm install --production
$ gulp build
## Development
$ npm install
$ gulp dev
And add our changes to the git repository. Remember to add the node_modules directory to the .gitignore file and then run:
$ git add .
$ git commit -v
TBC ... use start-app