In chapter 7, we built a single-page app for creating, reading, updating, and deleting posts from an HTTP server. What happens to the app when the server is unreachable? Let’s find out. Run the following commands from the beginning-elm
directory in separate terminal windows to launch the app.
Now go to http://localhost:8000
in a browser and you should see two posts.
Stop json-server
by pressing Ctrl + c
and reload the page at http://localhost:8000
. You should now see an error.
It would be great if we could display posts from the previous fetch as soon as our app loads even if the server is unreachable. As it turns out, all client-side web apps can in fact store data locally in a browser through the use of a technology called Web Storage which provides two different ways for saving data:
-
Session Storage - Data stored in session storage is available until the browser is closed.
-
Local Storage - Data stored in local storage is available even if the browser is closed and reopened.
Let’s store the state of our app in local storage. Unfortunately, Elm doesn’t provide a package for directly accessing local storage. We’ll have to go through JavaScript.
Note: The Elm development team is working hard to expand support for all technologies included in the Web Platform. Local storage is one of them. In the meantime, the recommended approach is to use JavaScript APIs through ports.
Creating an Outgoing Port
Let’s start by defining an outgoing port for sending the app state to JavaScript. Create a new file called Ports.elm
in the beginning-elm/post-app
directory and add the code below to it.
Our plan is to transform all Post
records in our app to a JSON string and then store that string in local storage. That’s why the storePosts
port takes a string as an input instead of a list of posts like this:
In the Model View Update - Part 1 section, we learned that the state of an app is typically represented by a model. So why are we storing only posts and not everything inside our main and page models? That’s because we just need to store enough data to bring the app back to a usable state. In our case, all we need is a list of posts.
Next we’ll write a function for transforming posts to a JSON string. Add the following code to the bottom of Post.elm
.
savePosts
not only transforms posts into JSON, but also calls the storePosts
port function. So to send posts through that port all we have to do is call savePosts
.
The Encode.list
decoder converts an Elm list into a JSON array. Here’s what its type signature looks like:
The Encode.encode
function converts a Value
into an actual JSON string. Here’s what its type signature looks like:
All encoders we’ve used so far produce a Value
. They don’t actually create a JSON string. That’s the job for encode
.
We didn’t have to use encode
when editing and creating a post in chapter 7 because the Http.request
and Http.post
functions converted an encoded Value
to a JSON string for us behind the scenes. Here we have to do that conversion ourselves because ports don’t do it. Let’s expose savePosts
and import the Ports
module in Post.elm
.
Sending Posts to JavaScript
Where in our app would be a good place to initiate the process of saving posts in local storage? Since we always end up fetching all posts from the ListPosts
page after creating a new post or editing an existing one, the PostsReceived response ->
branch inside ListPosts.elm
’s update
function is a good place to call the savePosts
function from.
Unfortunately that’s not going to work because response
is of type WebData (List Post)
. We need to pull posts out of WebData
. Luckily, the RemoteData
module provides a function called withDefault
that does exactly what we need here. Here’s what its type signature looks like:
Change the PostsReceived response ->
branch in update
to the following.
Although this implementation works, it introduces a subtle bug: An empty list is saved in local storage even if we failed to fetch posts from the server. We don’t want that. savePosts
shouldn’t be called at all if we don’t have actual posts. Let’s use the following implementation instead.
Now expose savePosts
when importing the Post
module in ListPosts.elm
.
Storing Posts in Local Storage
That’s all we need to do on the Elm side. Check the elm-live
window in terminal to make sure there are no errors. Now let’s turn our attention to JavaScript. Replace the contents of beginning-elm/index.html
with the following.
For comparison, here is the index.html
file we used to send data to JavaScript from a non-single-page Elm app:
Did you notice the difference? We had to embed our Elm app inside a div
in the latter case. We don’t have to do that in a single-page app because it takes over the entire HTML document. Simply loading elm.js
is enough.
Next we need to put the compiled code in elm.js
. Stop elm-live
by pressing Ctrl + c
and restart it by running the following command from the beginning-elm
directory in terminal.
In the previous sections of this chapter we used elm make
to produce elm.js
like this:
Why can’t we use elm make
here too? That’s because post-app
is a single-page app and we need the pushstate
feature from elm-live
to be able to navigate between pages. That being said, elm-live
actually calls elm make
behind the scenes to compile our code.
It’s important to note that elm-live
requires us to put all elm make
specific flags after the --
separator. If we don’t use the --output
flag, elm make
will automatically create a file called index.html
in the directory from where it’s run and put the compiled code in that file instead. We don’t want that.
Listening to the storePosts Port
Now it’s time to subscribe to the storePosts
port by adding the following code to the bottom of the <script>
tag in index.html
.
We need to make sure the posts sent by our Elm app actually made it to the JavaScript side before we save them in local storage. That’s why we’re printing them in console first. Make sure both elm-live
and json-server
are running. Then go to http://localhost:8000
and open the browser console. You should see the posts in raw JSON string format.
Now update the callback function in index.html
to actually save the posts in local storage.
We’re using the setItem
function in localStorage
to save the posts. setItem
takes a key value pair. post-app-save
is the key and the raw JSON string produced by JSON.stringify
is the value.
If you are using Chrome, you can check whether or not the data was indeed saved in local storage by opening the developer tools and going to the Application tab as shown in the figure below.
Summary
In this section, we learned how to save the state of our Elm app in browser’s local storage. Since Elm doesn’t provide a package for that, we had to send the app state through an outgoing port and use JavaScript API on the other end to actually save it. In the next section, we’ll learn how to restore the state when our app is being initialized.