If you love writing your backend services in Elixir, you might ask yourself "why can't i use Elixir for my frontend development as well?". The good news is: you can! Although ElixirScript is not a finished product and still has some limitations, Bryan Joseph has already achieved impressive results in building an Elixir to JavaScript compiler.
This tutorial shows you how to implement a (more or less) complete Todo Application with Elixir and ElixirScript and that ElixirScript might become an ideal functional language for frontend development, which can bring back the fun into browser development!
The complete source code of the tutorial application can be found in this GitHub repository.
This tutorial focuses mainly on the frontend part of the application. It shows how functional programming together with React and a Redux-style design can lead to an improved application architecture. This architecture also borrows from the language Elm, in that it doesn't use UI components, but simply calls functions to render the complete user interface into React's virtual DOM.
The frontend code can be found in the directory web/static/exjs and consists of the following modules:
main.ex | Main.start() is called when the application is loaded into the browser |
store.ex | Implements a Redux-style Flux store with reducers, subscribers and middlewares |
reducers.ex | Contains the reducer functions that implement the business logic |
views.ex | UI rendering functions and a single render() subscriber function |
middlewares.ex | Middleware functions for server synchronization |
channels.ex | Real-Time-Communication through Phoenix Channels |
todo.ex | Todo model functions |
react_ui.ex | Elixir macros that implement a DOM DSL (see https://github.com/bryanjos/elixirscript_react) |
A note on the usability of this application:
Elixir Todo App is an example tutorial application which you can use to manage your todos, but is is far from being feature complete. Currently there is no support for multiple users, and there is no authentication and no authorization implemented. It is not optimized for performance (which is only a problem if you have millions of todo items) and lacks a lot of the features, that you can find in other todo or task list management apps.
Here is a quick overview of the Elixir Todo App architecture and the technologies used for it's implementation:
- Server
- The server is built with Phoenix and provides an endpoint for the Web-Application as well as REST endpoints for the backend services.
- Database
- There is a single Todo model that is managed with Ecto and stored in a PostgreSQL database.
- Backend business logic
- Besides simple model validation, there is no server side business logic.
- REST API
- The backend services are provided through a REST API (/api/todos).
- Channels (WebSockets)
- Changes to the todo list from multiple browsers are synchronized through Phoenix channel messages.
- Frontend Rendering
- React is used to render the Web frontend. Whenever the application state changes, the complete UI is created as a React virtual DOM.
- Frontend business logic
- A custom Flux store implementation that uses a Redux-style reducer/subscriber pattern, is used to implement the frontend's business logic.
Elixir and Erlang are available as packages for most operating systems (and Linux distributions). With the latest instructions that are available on the Elixir web page at http://elixir-lang.org/install.html, you should be able to install Elixir and Erlang on your system in a few minutes. You also need a PostgreSQL database running on your workstation. And since the JavaScript packages are managed with NPM, you finally need to have the latest version of NPM (which comes with Node.js) installed on your system as well. Now clone the GitHub repository, retrieve the package dependencies, create the database and start the Elixir Todo App server:
$ git clone https://github.com/grappendorf/elixir_todo_app.git
$ cd elixir_todo_app
$ mix deps.get
$ npm install
$ mix ecto.reset
$ mix phoenix.server
...
[info] Running ElixirTodoApp.Endpoint with Cowboy using http://localhost:4000
Eventually you can open the Elixir Todo App in your browser by going to the url http://localhost:4000.
After playing around a little bit with the app, let us now examine the different parts of the system in more detail.
I won't talk too much about the backend. It uses Phoenix to handle the HTTP requests, implements a simple REST API for the todo items and stores the todo items in a PostgreSQL database with Ecto. There are already numerous tutorials out there, that explain how Phoenix and Ecto work, so please refer to them if you need to learn more about this.
Here are the available API functions:
Path | Method | Description |
---|---|---|
/api/todos | GET | Retrieve a list of all todo items |
/api/todos/:id | GET | Get a single todo item by it's id |
/api/todos | POST | Create a new todo item |
/api/todos/:id | PUT | Update a todo item |
/api/todos/:id | DELETE | Delete a todo item |
/api/todos/:id/states | POST | Change the state of a todo item (todo or done) |
As an example here is the implementation of the GET /api/todos function:
defmodule ElixirTodoApp.Todo do
use ElixirTodoApp.Web, :model
defenum Status, todo: 0, done: 1
schema "todos" do
field :text, :string
field :status, Status, default: :todo
timestamps
end
def latest_first todos do
from todo in todos,
order_by: [desc: todo.inserted_at]
end
end
defmodule ElixirTodoApp.TodoController do
use ElixirTodoApp.Web, :controller
alias ElixirTodoApp.{Repo, Todo}
def index conn, _params do
todos = Todo |> Todo.latest_first |> Repo.all
render conn, todos: todos
end
end
defmodule ElixirTodoApp.TodoView do
use ElixirTodoApp.Web, :view
def render "index.json", %{todos: todos} do
todos |> Enum.map(&todo_to_json/1)
end
defp todo_to_json todo do
%{
id: todo.id,
text: todo.text,
status: todo.status
}
end
For the Elixir Todo App i don't use React's Redux library, but have implemented my own version of the Redux architecture. The complete store implementation can be found in the Store module (web/static/exjs/store.ex).
The store provides the following functions:
Function | Description |
---|---|
new(initial_state) | Create a new store with an initial state. |
state(store) | Retrieve the current state of the store. |
reduce(store, reducer) | Add a reducer to the store. This is a pure function of type (State, Action) :: State that receives the current state of the store and the action that is dispatched and transforms the current state into a new state. |
subscribe(store, subscriber) | Add a subscriber to the store. This is a function of type (State, State) :: none() that receives the new and old state of the store and is called for its side effects. |
middleware(store, middleware) | Add a middleware to the store. This is a function of type (:pre|:post, State, State, Action) :: Action that receives the new and old state of the store, and the action that is dispatched. A middleware is called before and after the reducers have been applied. Middlewares have side effects and are called to modify or dispatch new actions. |
dispatch(store, action) | This is the main function that keeps the system running. When we dispatch an action, first the middlewares are executed (:pre), then the reducers are applied, then the middlewares are called again (:post) and then the subscribers are executed. |
This store together with the reducers and the subscribers reduces (no pun intended) our application architecture to this simple diagram:
Actions are coming in either from user activities or some other external events, reducers transform the current application state depending on these actions into a new state and subscribers perform side effects that will for example reflect these state changes back into the user interface.
First of all when the application starts up, the store is initialized in the applications start() function and an initial action is dispatched to load the todos from the server:
defmodule Main do
def start _, _ do
create_store()
|> Store.middleware(&Middlewares.api/4)
|> Store.reduce(&Reducers.reduce/2)
|> Store.subscribe(&Views.render/2)
|> Store.start
Store.dispatch {:load_todos}
end
end
Let us now look in detail at how the {:load_todos} action is handled. There is another version of this action {:load_todos, todos}, that contains the todos to displays. But initially we don't have any todos, so we must call the REST API to fetch them from the server.
defmodule Middlewares do
def api :pre, _, _, {:load_todos} do
do_get "/api/todos", fn json ->
todos = Enum.map json, &(Todo.new_from_json &1)
Store.dispatch {:load_todos, todos}
end
{}
end
end
The middleware function handles the action {:load_todos}. It calls window.fetch("/api/todos") in the do_get() function to retrieve the todos and returns the empty action {}. At this point no reducer will be applied and the store state doesn't change (but you could also return another action like {:show_loading_spinner} for displaying some kind of "loading" state in the UI).
When do_get() succeeds, the todo list is generated from the JSON response and a new action {:load_todos, todos} is dispatched.
As another more complicated example take a look at this middleware function that puts a modified todo onto the server:
defmodule Middlewares do
def api :post, %{todos: todos}, %{todos: old_todos}, action = {:update_todo, id, _} do
todo = Todo.find_by_id(todos, id)
todo_json = %{"text" => todo.text, "status" => Atom.to_string(todo.status)}
do_put "/api/todos/#{id}", todo_json, fn _ -> end, fn _ ->
show_error_toast "Server error while updating the todo."
Store.dispatch {:undo_todo, Todo.find_by_id(old_todos, id)}
end
action
end
end
This middleware function is called after all reducers have been applied. So the old state contains the old version of the todo and the new state contains the modified one. In case of a server error, me must undo the state change. So in the error function we dispatch a new action {:undo_todo, old_todo} that replaces the modified todo with the original one.
Everything a program does can be described as a collection of reducer functions. They define how the application state changes, when a specific action (or event) has occurred. (You can even describe each step of an algorithm in a function as a single reducer, Dave Thomas ("The Pragmatic Programmer" and "Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun") has done a nice presentation on this topic)
Elixir's pattern matching allows us to implement reducers in a clean and expressive way. The following reducer function handles the {:load_todos, todos} action and sets the todos in the application state:
defmodule Reducers do
def reduce state, {:load_todos, todos} do
%{state | todos: todos}
end
end
Another reducer for example handles the deletion of a todo:
defmodule Reducers do
def reduce state = %{todos: todos}, {:delete_todo, id} do
%{state | todos: Todo.delete(todos, id)}
end
end
If we strive for high cohesion and a loose coupling between the reducers, most of them will be simple one-liners like these.
You need to have some knowledge of how React works to understand the frontend code of the Elixir Todo App. So please read the React Tutorial if you haven't already done so.
Whenever the application state changes, the Views.render(new_state, old_state) function is called with the current application state as a parameter. Views.render() then calls various other functions of the Views module to render the UI into a virtual DOM. At the end, ReactDOM.render() renders the virtual DOM into actual DOM. This process is highly optimized, because React first creates a diff between the old virtual DOM and the new one and only updates the parts of the real browser DOM which have actually changed.
defmodule Views do
use ReactUI
def render state, _ do
state
|> Views.page
|> ReactDOM.render(:document.getElementById("app"))
end
def page state do
ReactUI.div do
page_title state
todo_input state
todo_list state
help state
end
end
# ...more functios below...
end
This is the function that renders a single todo item:
def todo_item todo, edit_todo do
if edit_todo && edit_todo.id == todo.id do
todo_item_editor todo, edit_todo
else
todo_item_text todo
end
end
We check if the todo item is currently edited by the user. We then either call a function todo_item_editor() that renders the todo item as an editor or a function todo_item_text() that renders the todo with plain text.
In todo_item_editor() the HTML structure of our user interface is defined by a DSL provided through some Elixir macros. These macros directly translate into calls of the React.createElement() function, so in essence this is similar to using JSX in React. We can emit DOM elements, define their properties and specify event handlers as anonymous Elixir functions.
def todo_item_editor todo, edit_todo do
tr key: todo.id do
td className: "width-100" do
input value: edit_todo.text, autoFocus: true, className: "form-control",
onChange: fn event ->
Store.dispatch {:edit_todo_text, event.target.value}
end,
onKeyUp: fn event, _ ->
if event.keyCode == @key_enter && edit_todo.text != "", do:
Store.dispatch {:update_todo, edit_todo.id, edit_todo.text}
if event.keyCode == @key_escape, do: Store.dispatch {:cancel_edit_todo}
end
ReactUI.div className: "pull-right todo-actions" do
i className: "fa btn fa-save btn-success",
disabled: edit_todo.text == "",
onClick: fn _, _ -> Store.dispatch {:update_todo, edit_todo.id, edit_todo.text} end
i className: "fa btn fa-remove btn-info",
onClick: fn _, _ -> Store.dispatch {:cancel_edit_todo} end
i className: "fa btn #{done_button_class todo}",
onClick: fn _, _ -> Store.dispatch {:toggle_todo, todo.id} end
i className: "fa fa-trash btn btn-danger",
onClick: fn _, _ ->
confirm "Do you really want to delete the todo \"#{edit_todo.text}\"?",
fn -> Store.dispatch {:delete_todo, todo.id} end,
fn -> Store.dispatch {:cancel_edit_todo} end
end
end
end
end
end
In todo_item_text() we again use these DSL macros to define a simple span that contains the todo text.
def todo_item_text todo do
tr key: todo.id do
td className: "width-100",
onClick: fn _, _ -> Store.dispatch {:edit_todo, todo.id} end do
span className: todo_text_class(todo) do
todo.text
end
end
end
end
And finally here is the todo_list(state) function that creates a table that contains a row for each todo item. This is done by simply mapping the todo list with the function that creates a table row for a single todo item. Also the list is filtered if the user only wants to display the active todos.
def todo_list state do
ReactUI.div do
# ...some code for other ui elements ommitted...
table className: "table table-bordered table-striped tutor-container" do
tbody do
state.todos
|> Enum.filter(fn todo -> !state.hide_done || todo.status != :done end)
|> Enum.map(fn todo -> todo_item todo, state.edit_todo end)
end
# ...some code for other ui elements ommitted...
end
end
end
When the todo list data was modified through some external event (you can for example open another browser window with your todo list and change some todos there), we need to synchronize this information with all clients. This is done through messages that are sent over Phoenix channels. These messages simply transport actions that are dispatched on the client.
For example when a new todo is created through the REST API, the TodoController broadcasts a :todo_was_created message to the clients:
defmodule ElixirTodoApp.TodoController do
use ElixirTodoApp.Web, :controller
def create conn, _ do
with {:ok, todo} <- create_model(Todo.changeset %Todo{}, conn.body_params) do
ClientChannel.publish_todo_created todo, conn.body_params["temp_id"]
conn |> put_status(:created) |> json(%{id: todo.id})
else
{:error_create, errors} ->
conn |> put_status(:bad_request) |> render_model_errors(errors)
end
end
end
defmodule ElixirTodoApp.ClientChannel do
alias ElixirTodoApp.Endpoint
def publish_todo_created todo, temp_id do
Endpoint.broadcast("client", "action", %{
action: :todo_was_created, params: [temp_id, todo.id, todo.text, todo.status]})
end
end
On the client side a WebSocket connection is established in app.js and listens to messages on the client channel. When a new message is received, it is converted into an action that can be dispatched by the store:
defmodule Channels do
def dispatchMessage msg do
action = [String.to_existing_atom(msg.action) | msg.params] |> List.to_tuple
Store.dispatch action
end
end
And there you have it. Except for a few configuration and boiler plate files, that's the whole application. It has a clean and simple architecture with only a few architectural elements to remember. Yes, it is a small application that is missing a lot of features, so we need to gather more experience when we create bigger systems.
Were are the frontend tests?
I'm glad you ask. This is the next item on my todo list. First of all we could test the frontend with JavaScript testing frameworks. But since our frontend is written in Elixir, we could also test it with Elixir's test frameworks if only we could stub out the browser specific APIs. I will update this tutorial if i have done some experiments on this.
I want to use ElixirScript, is it ready for production?
Let me cite from the ElixirScript's FAQ:
You can use ElixirScript on your front ends and have it work and interoperate with JavaScript modules. The problem is since most of the standard library is incomplete.
What about performance?
In the render() function the complete user interface is rendered into a virtual DOM whenever any value in the store state changes. Elm for example uses "lazy functions" to optimize this. These functions memoize their results and only recreate the DOM whenever the function parameters differ. Since we are using Reacts virtual DOM and React already optimizes the handling of updates to the real DOM (by creating a diff between the virtual DOM), we may get away with it. Also even in a very big application, only parts of the user interface are visible at a specific point in time and so most of the rendering functions are not called at all. But sure, more experiments and maybe some performance optimizations are needed for production grade applications.