Storage
Source code: GitHub
Let's start by creating a new project with the elm-spa CLI:
elm-spa new
Creating a stateful page
Let's create a simple interactive app, based on the official Elm counter example. The elm-spa add command will make this a breeze:
elm-spa add / sandbox
This will stub out the init, update, and view function for us, and wire them together with Page.sandbox like this:
-- src/Pages/Home_.elm page : Shared.Model -> Request -> Page.With Model Msg page = Page.sandbox { init = init , update = update , view = view }
Let's add in the implementation from the counter example to get a working app!
init
-- src/Pages/Home_.elm type alias Model = { counter : Int } init : Model init = { counter = 0 }
update
-- src/Pages/Home_.elm type Msg = Increment | Decrement update : Msg -> Model -> Model update msg model = case msg of Increment -> { model | counter = model.counter + 1 } Decrement -> { model | counter = model.counter - 1 }
view
-- src/Pages/Home_.elm view : Model -> View Msg view model = { title = "Homepage" , body = [ Html.h1 [] [ Html.text "Local storage" ] , Html.button [ Html.Events.onClick Increment ] [ Html.text "+" ] , Html.p [] [ Html.text ("Count: " ++ String.fromInt model.counter) ] , Html.button [ Html.Events.onClick Decrement ] [ Html.text "-" ] ] }
After these functions are in place, we can spin up our server with the elm-spa CLI:
elm-spa server
And this is what we should see at http://localhost:1234:

Playing with the counter
As we click the "+" and "-" buttons, the counter value should be working great. When we refresh the page, the counter value is 0 again.
Let's use local storage to keep the counter value around!
The JS side
To do this, we'll be using flags and ports, a typesafe way to work with JavaScript without causing runtime errors in our Elm application!
Let's edit public/index.html as a starting point:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <script src="/dist/elm.js"></script> <!-- EDIT THIS LINE --> <script src="/main.js"></script> </body> </html>
Here we replace the inline Elm.Main.init() script generated by the elm-spa new command with a reference to a new file we'll create in public/main.js
// public/main.js const app = Elm.Main.init() // ...
At this point, nothing has changed yet, but we now have access to app– which will allow us to interact with our Elm app from the JS file!
Let's add in some ports like this:
// public/main.js const app = Elm.Main.init({ flags: JSON.parse(localStorage.getItem('storage')) }) app.ports.save.subscribe(storage => { localStorage.setItem('storage', JSON.stringify(storage)) app.ports.load.send(storage) })
This JS code is doing a few things:
- When our Elm app starts up, we pass in the current value of
localStoragevia flags. Initially, this will pass innull, because no data has been stored yet.
- We subscribe to the
saveport for events from Elm, which we'll wire up on the Elm side shortly.
- When Elm sends a
saveevent, we'll store the data in localStorage (making it ready for the next time the app starts up!) as well as send a message back to Elm via theloadport.
The Elm side
None of this code is working yet, because we need to define these save and load ports on the Elm side too!
Let's create a new file at src/Storage.elm that defines the ports referenced on the JS side:
port module Storage exposing (..) import Json.Decode as Json port save : Json.Value -> Cmd msg port load : (Json.Value -> msg) -> Sub msg
Above, we've created a port module that defines our save and load ports. Next, we'll describe the data we want to store, as well as how to convert it to and from JSON:
port module Storage exposing ( Storage, fromJson, onChange , increment, decrement ) import Json.Encode as Encode -- ... port definitions from before ... type alias Storage = { counter : Int } -- Converting to JSON toJson : Storage -> Json.Value toJson storage = Encode.object [ ( "counter", Encode.int storage.counter ) ] -- Converting from JSON fromJson : Json.Value -> Storage fromJson value = value |> Json.decodeValue decoder |> Result.withDefault initial decoder : Json.Decoder Storage decoder = Json.map Storage (Json.field "counter" Json.int) initial : Storage initial = { counter = 0 }
If this decoder stuff is new to you, please check out the JSON section of the Elm guide. It will lay a solid foundation for understanding decoders and encode functions!
Sending data to JS
For this example, we're going to define increment and decrement as side-effects because they change the state of the world. We'll be using the save port to send these events to JS:
-- src/Storage.elm increment : Storage -> Cmd msg increment storage = { storage | counter = storage.counter + 1 } |> toJson |> save decrement : Storage -> Cmd msg decrement storage = { storage | counter = storage.counter - 1 } |> toJson |> save
This should look pretty similar to how our homepage handled the Increment and Decrement messages, but this time we use toJson and save to send an event for JS to handle.
( As a last step, we'll revisit Home_.elm and swap out the old behavior with the new )
Listening for data from JS
We're going to add one final function to Storage.elm that will allow us to subscribe to events from the load port, that use's our fromJson function to safely parse the message we get back:
onChange : (Storage -> msg) -> Sub msg onChange fromStorage = load (\json -> fromJson json |> fromStorage)
Here, the onChange function will allow the outside world to handle the load event without having to deal with raw JSON values by hand.
That's it for this file- now we're ready to use our Storage module in our app!
Wiring up the shared state
Let's eject Shared.elm by moving it from .elm-spa/defaults into our src folder. This will allow us to make local changes to it, as explained in the shared state section of the guide.
Our first step is to add Storage to our Shared.Model, so we can access storage from any page in our application:
-- src/Shared.elm import Storage type alias Model = { storage : Storage }
The Shared.init function is the only place we have access to Flags, which is how JS passed in our initial value earlier. We can use Storage.fromJson to convert that raw JSON into our nice Storage type.
-- src/Shared.elm init : Request -> Flags -> ( Model, Cmd Msg ) init _ flags = ( { storage = Storage.fromJson flags } , Cmd.none )
Now let's listen for those load events from JS, so we can update the Shared.Model as soon as we get them. This code will use the Storage.onChange function we made to send a Shared.Msg to our Shared.update function:
-- src/Shared.elm subscriptions : Request -> Model -> Sub Msg subscriptions _ _ = Storage.onChange StorageUpdated type Msg = StorageUpdated Storage update : Request -> Msg -> Model -> ( Model, Cmd Msg ) update _ msg model = case msg of StorageUpdated storage -> ( { model | storage = storage } , Cmd.none )
That's all for src/Shared.elm. The last step is to upgrade our homepage to send side-effects instead of changing the data locally.
Upgrading Home_.elm
To gain access to Cmd msg, we'll start by using Page.element instead of Page.sandbox. The signature of our init and update functions will need to change to handle the new capabilities:
Our Model no longer needs to track the state of the application. This means the Home_.init function won't be doing much at all:
-- src/Pages/Home_.elm type alias Model = {} init : ( Model, Cmd Msg ) init = ( {}, Cmd.none )
This time around, the update function will need access to the current Storage value and use Storage.increment and Storage.decrement to send commands to the JS side.
-- src/Pages/Home_.elm type Msg = Increment | Decrement update : Storage -> Msg -> Model -> ( Model, Cmd Msg ) update storage msg model = case msg of Increment -> ( model , Storage.increment storage ) Decrement -> ( model , Storage.decrement storage )
When the load event comes in from JS, it triggers our Storage.onChange subscription. This updates the storage for us, meaning the storage.counter we get in our view will be the latest counter value.
-- src/Pages/Home_.elm view : Storage -> Model -> View Msg view storage _ = { title = "Homepage" , body = [ Html.h1 [] [ Html.text "Local storage" ] , Html.button [ Html.Events.onClick Increment ] [ Html.text "+" ] , Html.p [] [ Html.text ("Count: " ++ String.fromInt storage.counter) ] , Html.button [ Html.Events.onClick Decrement ] [ Html.text "-" ] ] }
We can use Page.element to wire all these things up, and even pass Storage into our view and update functions, which depend on the current value to do their thing:
-- src/Pages/Home_.elm page : Shared.Model -> Request -> Page.With Model Msg page shared _ = Page.element { init = init , update = update shared.storage , view = view shared.storage , subscriptions = \_ -> Sub.none }
Here, I've stubbed out
subscriptionswith an inline function, we won't be needing it, because Shared.subscriptions listens toStorage.onChangefor us.
Hooray!
In the browser, we now have a working counter app that persists on refresh. Even if you close the browser and open it up again, you'll see your previous counter value on the screen.
As a reminder, all the source code for this example is available on GitHub
Next up: User Authentication