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.
$ json-server --watch server/db.json -p 5019
$ elm-live post-app/Main.elm --pushstate
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.
port module Ports exposing (storePosts)
port storePosts : String -> Cmd msg
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:
port storePosts : List Post -> Cmd msg
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 : List Post -> Cmd msg
savePosts posts =
Encode.list postEncoder posts
|> Encode.encode 0
|> Ports.storePosts
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
.
-- Transform an Elm string into a JSON string value
string : String -> Value
-- Transform an Elm int into a JSON number value
int : Int -> Value
-- Transform an Elm list into a JSON array value
list : (a -> Value) -> List a -> Value
-- Convert a Value to an actual JSON string
encode : Int -> Value -> String
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
.
module Post exposing
.
.
, savePosts
)
import Ports
.
.
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.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
.
.
PostsReceived response ->
( { model | posts = response }
, savePosts response
)
.
.
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.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
.
.
PostsReceived response ->
let
actualPosts =
RemoteData.withDefault [] response
in
( { model | posts = response }
, savePosts actualPosts
)
.
.
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.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
.
.
PostsReceived response ->
let
savePostsCmd =
case response of
RemoteData.Success actualPosts ->
savePosts actualPosts
_ ->
Cmd.none
in
( { model | posts = response }, savePostsCmd )
.
.
Now expose savePosts
when importing the Post
module in ListPosts.elm
.
import Post exposing (Post, PostId, postsDecoder, savePosts)
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.
<!DOCTYPE html>
<html>
<body>
<script src="elm.js"></script>
<script>
var app = Elm.Main.init();
</script>
</body>
</html>
For comparison, here is the index.html
file we used to send data to JavaScript from a non-single-page Elm app:
<!DOCTYPE html>
<html>
<body>
<div id="elm-code-is-loaded-here"></div>
<script src="elm.js"></script>
<script>
var app = Elm.PortExamples.init({
node: document.getElementById("elm-code-is-loaded-here")
});
.
.
</script>
</body>
</html>
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.
$ elm-live post-app/Main.elm --pushstate -- --output=elm.js
In the previous sections of this chapter we used elm make
to produce elm.js
like this:
$ elm make src/PortExamples.elm --output elm.js
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
.
<script>
.
.
app.ports.storePosts.subscribe(function(posts) {
console.log("Posts: ", JSON.stringify(posts));
});
</script>
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.
<script>
.
.
app.ports.storePosts.subscribe(function(posts) {
if (posts.length > 0) {
var postsJson = JSON.stringify(posts);
localStorage.setItem('post-app-save', postsJson);
console.log("Saved state: ", postsJson);
}
});
</script>
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.