This is a port of the TodoMVC app, implemented with an Elm frontend and an Elixir/Phoenix/Absinthe/GraphQL backend.
The original serverless Elm frontend code for this project was written by Evan Czaplicki, and is available at https://github.com/evancz/elm-todomvc
This version adds data persistence via the Elixir backend. The Elm frontend communicates with the backend over Phoenix channels (distributed WebSockets with defined message and broadcast protocols) in both directions.
To compile the Elm frontend, you will obviously need to install the
Elm system tools on your machine
and then install the Elm package dependencies specified in assets/elm/elm-package.json
.
Since one of those dependencies, elm-phoenix, is an effect manager it is at the moment (Elm v0.18) not available via elm-package. Thus the recommended way to install the elm-phoenix package is to use the elm-github-install package manager.
Then, to rebuild the Elm frontend manually:
cd assets/elm
elm-github-install
elm-make --debug --warn --output=../js/elm-main.js src/Todo.elm
Or just use the Elixir project's brunch build tool (which is automatically invoked when
starting the dev server--see the next section of this README file). The brunch configuration at
assets/brunch-config.js
will require installation of the
npm elm-brunch plugin. After installation,
verify that there is an elm-brunch
line in the devDependencies
of assets/package.json
.
If elm-brunch
is missing from the package.json file, running brunch will ignore
the elmBrunch
plugin configuration silently and will not compile the elm/src/Todo.elm
source file.
If you want your Elm application to be served by Phoenix, it's pretty simple to move your existing Elm-only project into an Elixir Phoenix project:
- The layout for the your existing index.html gets moved to the Phoenix layout template at
lib/todo_absinthe_web/templates/layout/app.html.eex
. Specify the title and any included .js or .css files here. In our case we use the Phoenix-standardjs/app.js
andcss/app.css
files. - The content for your layout, contained within your existing index.html file, gets moved
to the Phoenix index template at
lib/todo_absinthe_web/templates/page/index.html.eex
. If you are making Elm fullscreen, this file can be empty! If you are embedding Elm inside adiv
, put just thediv
here. - Move your CSS styles to
assets/css/app.css
and your embedding/fullscreen startup code and any Javascript ports toassets/js/app.js
. In the app.js file, add this require to build in your Elm frontend:import Elm from "./elm-main";
(or whatever your Elm frontend output js file is).
To start your Phoenix server:
- Install dependencies with
mix deps.get
- Create and migrate your database with
mix ecto.create && mix ecto.migrate
- Install Node.js and brunch dependencies with
cd assets && npm install
- Start Phoenix endpoint with
mix phx.server
Now you can visit localhost:4000
from your browser.
Ready to run in production? Please check the Phoenix deployment guides.
- Data is stored in a PostgreSQL database with a single
todos
table. See the schema definition inlib/todo_absinthe/todo/item.ex
and the migration inpriv/repo/migrations/
. - UUIDs are used for the
id
field. See the:migration_primary_key
configuration option inconfig/config.exs
. - An auto-incremented (
:serial
) integer is used for theorder
field, part of the TodoMVC spec. In the original Elm TodoMVC implementation, this field was not used. - The PostgreSQL-specific
:read_after_writes
option is used to return updated DB values after insert/update operations, so that we can return the auto-generatedorder
field value in the Absinthe reply correctly.
The operations exposed by Absinthe are defined in lib/todo_absinthe_web/schema.ex
and
the input and return object definitions are in lib/todo_absinthe_web/schema/content_types.ex
.
The resolver code in lib/todo_absinthe_web/resolvers/todo_resolver.ex
publishes changes
to three subscriptions: "itemsCreated", "itemsUpdated" and "itemsDeleted", which allows
for realtime updates to subscription clients.
While GraphQL servers are most commonly accessed by clients over HTTP, the Elm frontend in this project uses a WebSocket transport, implemented with functions in the courtesy of the elm-phoenix project (see Elm Notes section above).
On the server side we set up a DocChannel
channel module for the topic "*"
that is largely based on (meaning lots of code copying from) Absinthe.Phoenix.Channel
.
The DocChannel has pubsub enabled, so that subscriptions can also be subscribed to it
by Elm (and GraphiQL).
See the lib/todo_absinthe_web/channels/doc_channel.ex
source file for more information.
On the Elm side, the frontend creates and subscribes to a Phoenix channel with the "*" topic at startup. This channel is configured with callbacks that monitor status changes and errors.
To make an Absinthe GraphQL query, mutation, or subscription request, Elm pushes a "doc" event
to the "*" channel. The payload is JSON encoded with GraphQL variables and the operation
document. The reply constructed by Absinthe is received as an Elm message
and the payload, containing data
and/or errors
components are decoded if
necessary.
When the "*" channel is first joined, the Elm frontend creates and subscribes to additional channels for the GraphQL subscriptions.
Following the protocol used by GraphiQL, Elm can subscribe to GraphQL subscription messages as follows:
- Push a subscription document (see below), using the subscription name
(e.g. "itemsCreated"), and specifying the fields of interest, to the "*" topic.
The payload of the reply will contain a
subscriptionId
string, like "__absinthe__:doc:87829607". - Create a new client channel, in addition to the original "*" channel,
using the
subscriptionId
string as the topic, and listening forsubscription:data
events. - Receive and decode incoming
subscription:data
payloads.
The subscription:data
payloads include two components:
subscriptionId
- the same id used for the topic.result
- a JSON value containing the standard GraphQLdata
reply.
To unsubscribe from a subscription, Elm pushes an "unsubscribe" event to the "*" channel with a payload specifying the subscription ID.
A clickable link was added to the original footer in the Elm user interface in order to exercise pubsub operations. Clicking the link will either subscribe to the subscriptions "itemsCreated", "itemsUpdated" and "itemsDeleted", or unsubscribe from them.
For more information, see the comments in the single Elm source file located in
the repository at assets/elm/src/Todo.elm
.
The original Elm TodoMVC persisted updates in browser local storage, which is nice for persistence between browser sessions. What is the best strategy for an offline app that can go back online? First, load from local storage, then query the backend for more recent changes, and finally keep things in sync by using Absinthe subscriptions if the backend store is modified by other users.
Hoping to find a clean implementation of this kind of syncing somewhere.
Currently, the code does monitor for changes using GraphQL subscriptions and updates the frontend model state if backend changes (adds, updates and deletes) from other clients are detected, but does not detect network disconnects, nor does it attempt to sync items modified when offline back to the server.