elm-spa

Pages

By default, there are four kinds of pages you can create with elm-spa. Always choose the simplest one for the job!

Static

A simple, static page that just returns a view.

page : Page Params Model Msg
page =
  Page.static
    { view = view
    }
view : Url Params -> Document Msg

Sandbox

A page that needs to maintain local state.

page : Page Params Model Msg
page =
  Page.sandbox
    { init = init
    , update = update
    , view = view
    }
init : Url Params -> Model
update : Msg -> Model -> Model
view : Model -> Document Msg

Element

A page that can make side effects with Cmd and listen for updates as Sub.

page : Page Params Model Msg
page =
  Page.element
    { init = init
    , update = update
    , view = view
    , subscriptions = subscriptions
    }
init : Url Params -> ( Model, Cmd Msg )
update : Msg -> Model -> ( Model, Cmd Msg )
view : Model -> Document Msg
subscriptions : Model -> Sub Msg

Application

A page that can read and write to the shared model.

page : Page Params Model Msg
page =
  Page.application
    { init = init
    , update = update
    , view = view
    , subscriptions = subscriptions
    , save = save
    , load = load
    }
init : Shared.Model -> Url Params -> ( Model, Cmd Msg )
update : Msg -> Model -> ( Model, Cmd Msg )
view : Model -> Document Msg
subscriptions : Model -> Sub Msg
save : Model -> Shared.Model -> Shared.Model
load : Shared.Model -> Model -> ( Model, Cmd Msg )

Working with the Shared.Model

Because save and load are both new concepts, here's a quick example of how to use them! Imagine this is your Shared.Model:

-- in Shared.elm
type alias Model =
  { key : Nav.Key
  , url : Url
  , user : Maybe User
  }

Let's implement a SignIn page together to understand how these functions interact.

init

If you're using Page.application, your page can tell if the user is already logged in on init:

type alias Model =
  { email : String
  , password : String
  , user : Maybe User
  }

init : Shared.Model -> Url Params -> ( Model, Cmd Msg )
init shared url =
  ( { email = ""
    , password = ""
    , user = shared.user
    }
  , Cmd.none
  )

load

On initialization, your page kept a local copy of user. This had a tradeoff: the rest of your page functions (update, view, and subscriptions) will be easy to implement and understand, but now it's possible for shared.user and your page's user to get out of sync.

Imagine the scenario where the navbar had a "Sign out" button. When that button is clicked, the shared.user would be signed out, but our page's Model would still show the user as logged in! This is where the load function comes in!

The load function gets called automatically whenever the Shared.Model changes. This allows you to respond to external changes to update your local state or send a command!

load : Shared.Model -> Model -> ( Model, Cmd Msg )
load shared model =
  ( { model | user = shared.user }
  , Cmd.none
  )

The load function lets you explicitly choose which updates from Shared.Model you care about, and provides an easy way to keep your Model in sync.

save

Earlier, when we initialized our page, we kept the user in our model. This makes implementing a sign in form easy, without worrying about the Shared.Model.

type Msg
  = UpdatedEmail String
  | UpdatedPassword String
  | AttemptedSignIn
  | GotUser (Maybe User)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    UpdatedEmail email ->
      ( { model | email = email }, Cmd.none )

    UpdatedPassword password ->
      ( { model | password = password }, Cmd.none )

    AttemptedSignIn ->
      ( model
      , Api.User.signIn
          { email = model.email
          , password = model.password
          , onResponse = GotUser
          }
      )

    GotUser user ->
      ( { model | user = user }
      , Cmd.none
      )

The only issue is that the user is only stored on the Sign In page. if we navigate away, we'd lose that data. That's where save comes in! Anytime your page's init or update is run, save is automatically called (by src/Main.elm). This allows you to persist local state to Shared.Model.

save : Model -> Shared.Model -> Shared.Model
save model shared =
  { shared | user = model.user }

That's it! Now if we navigate to another page, our user will still be signed in.


Let's take a deeper look at Shared together.