In the last episode, we learned how to interact with LocalStorage from Elm. We just stored a single number, but obviously we expect to have slightly more interesting model to store. We're headed towards using LocalStorage with TodoMVC, so let's look at what it will take to serialize a more interesting model to JSON and back again.

Project

I've tagged the repo with before_episode_005.3 for a starting point. Our model consists of not just the count, but also the number of times that the increment and decrement buttons have been pressed. We can serialize the whole model, rather than just that one integer, and this will be a good proxy for serializing any complex data structure via localStorage or for interacting with an API. Let's start.

First, we now know that our Port should have the Model itself go out on it. Let's just try that:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Increment ->
      -- ...
        ( newModel
        , Cmd.batch
            [ increment ()
            , storage newModel
            ]
        )
    Decrement ->
      -- ...
        ( newModel
        , storage newModel
        )


port storage : Model -> Cmd msg

If we make these changes, things will compile. That's pretty good, and basically indicates we won't have a runtime error, right? But what is actually going to be STORED now? Let's run it:

elm-make Counter.elm --output elm.js
servedir

Visit http://localhost:9091/Counter.elm, increment it a few times, and check out the stored value:

localStorage.getItem("counter")
// -> "[object Object]"

...ok. So it tried to store something, but all that got stored was the stringified version of the object. Let's tweak our JavaScript code to turn whatever it's trying to store into JSON before storing it:

app.ports.storage.subscribe(function(data){
  localStorage.setItem('counter', JSON.stringify(data));
});

Alright, let's refresh. We get a runtime error, because from our JavaScript we're now trying to send the result of calling Number("[object Object]") to our port. Let's just comment that line out for now:

//app.ports.storageInput.send(Number(currentCount));

Refresh and try again. Click the buttons a few times, then check localStorage:

localStorage.getItem("counter");

OK, so our model is actually being sent across the port as a reasonable object without us having to know anything important about how to decode or encode JSON from Elm. Neat. Now let's just try the same trick for sending the data back in:

var currentModel = localStorage.getItem("counter");
app.ports.storageInput.send(currentModel);
type Msg
    = Increment
    | Decrement
    | Set Model
    | NoOp


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        -- ...
        Set newModel ->
            ( newModel, Cmd.none )

port storageInput : (Model -> msg) -> Sub msg

Could that possibly just work? Compile it and find out.

Spoiler alert: no. No, it won't. But for a really simple reason. We're fetching from the localStorage and sending the value into the port. But that's just a string, because localStorage is just storing a string. We need to parse it as JSON and send the object itself in. The fix is simple:

app.ports.storageInput.send(JSON.parse(currentModel));

With that, the application will store its state on the backend and reify it on load, sending it through the port so the Elm application can load it up. Pretty cool, huh?

Resources