Renamed to ryan-haskell/elm-spa. It is published under that name, and new versions come out there.
Versions published as ryannhg/elm-spa still install under that name. 13 versions of 13 come from the new location (see their notes).
Rerouted (renamed, verified). The GitHub user or repository was renamed. The tag is still there, at the new name. The Lamdera package server serves a copy recovered from https://github.com/ryan-haskell/elm-spa.
What we checked: The repo moved to github.com/ryan-haskell/elm-spa. Rebuilt with git archive from commit e4b882259035 of https://github.com/ryan-haskell/elm-spa (zip prefix ryannhg-elm-spa-be230fb/, America/Los_Angeles times): the zip is byte-identical to the one package.elm-lang.org recorded, SHA-1 80cdff9c192258e6cdb706da3a390cb96716d601.
The Lamdera compiler uses this copy and prints a one-line notice saying so. elm itself fails to download this version.
Details · package.elm-lang.org's zip: https://github.com/ryannhg/elm-spa/zipball/4.1.1/
When you create an app with the elm/browser package,
you can build anything from a static Html msg page to a fully-fledged web Browser.application.
elm-spa uses the existing design at the page-level, so you can quickly add new pages to your Elm application!
Just like Elm's intro "Hello!" example, sometimes you just want a page that renders some HTML, without having any state.
Note: Static pages don't store data, so Model is always an empty tuple: ()
When you're ready to keep track of state, like in Elm's "Counter" example, you can use a sandbox page.
Similar to Browser.sandbox, this allows you to init your model, and update it with messages!
import Spa exposing (Page)
page : Page Flags Model Msg globalModel globalMsg
page =
Spa.sandbox
{ init = init
, update = update
, view = view
}If you're ready to send HTTP requests or listen to events for updates, it's time to upgrade to an element page!
Additionally, an element will give you access to Flags, so you can access things like URL parameters.
page : Page Flags Model Msg globalModel globalMsg
page =
Spa.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}New to Cmd or Sub? I recommend checking out Elm's official guide , that's a great place to wrap your head around these two concepts.
If you need to access shared state across all pages, or need to send a global commands like signing in/signing out a user, a component page is what you'll need.
Component pages gain access to the Global.Model, and return an additional Cmd Global.Msg so you can read/update the global state of your application.
import Global
import Spa exposing (Page)
page : Page Flags Model Msg Global.Model Global.Msg
page =
Spa.component
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}Cool trick: Don't need access to Global.Model in all of your functions? Use always to skip the first argument.
In the following example, only the view function receives Global.Model:
import Global
import Spa exposing (Page)
page : Page Flags Model Msg Global.Model Global.Msg
page =
Spa.component
{ init = always init
, update = always update
, view = view -- not using "always"!
, subscriptions = always subscriptions
}Note: The elm-spa cli tool will generate all the upcoming code for you. You can continue reading to understand what it's doing under the hood!
What was the point of using the functions above? They all return the Page type,
and we can use the Spa.upgrade function on any of them!
(The following example will illustrate why that's a good thing)
Let's imagine we are building a website for a restaurant! This website has the following routes:
/home - the homepage/menu - the menu/faqs - frequently asked questionsWith elm-spa we'd create a module for each page, and import them together in one file.
module Pages exposing (Model, Msg, init, update, view, subscriptions)
import Pages.Home
import Pages.Menu
import Pages.FaqsIf we want to implement the Pages.init function, we'll need to return the same type of value. This means we need to make a shared model and a shared msg type to handle all the different pages in our application:
type Model
= Home_Model Pages.Home.Model
| Menu_Model Pages.Menu.Model
| Faqs_Model Pages.Faqs.Model
type Msg
= Home_Msg Pages.Home.Msg
| Menu_Msg Pages.Menu.Msg
| Faqs_Msg Pages.Faqs.MsgThese two types allow init to return ( Model, Cmd Msg, Cmd Global.Msg ) for any page that the user is looking at!
With the custom types above:
Home_Model can upgrade a Pages.Home.Model to a ModelHome_Msg can upgrade a Pages.Home.Msg to a MsgWe use the upgrade function to return a record that makes writing the combined init/update/view/subscriptions functions clean and easy!
For each page we export from our Pages.* modules, we should call the upgrade function with the corresponding Model and Msg variants, like this:
pages : UpgradedPages
pages =
{ home = Pages.Home.page |> Spa.upgrade Home_Model Home_Msg
, menu = Pages.Menu.page |> Spa.upgrade Menu_Model Menu_Msg
, faqs = Pages.Faqs.page |> Spa.upgrade Faqs_Model Faqs_Msg
}import Pages.Faqs
import Pages.Home
import Pages.Menu
import Spa
type alias UpgradedPage pageFlags pageModel pageMsg =
{ init : pageFlags -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
, update : pageMsg -> pageModel -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
, bundle : pageModel -> Global.Model -> Spa.Bundle Msg
}
type alias UpgradedPages =
{ home : UpgradedPage Pages.Home.Flags Pages.Home.Model Pages.Home.Msg
, menu : UpgradedPage Pages.Menu.Flags Pages.Menu.Model Pages.Menu.Msg
, faqs : UpgradedPage Pages.Faqs.Flags Pages.Faqs.Model Pages.Faqs.Msg
}
pages : UpgradedPages
pages =
{ home = Pages.Home.page |> Spa.upgrade Home_Model Home_Msg
, menu = Pages.Menu.page |> Spa.upgrade Menu_Model Menu_Msg
, faqs = Pages.Faqs.page |> Spa.upgrade Faqs_Model Faqs_Msg
}Now when we write init, we can use the upgraded pages variable to keep things easy to read.
initimport Route exposing (Route)
init : Route -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
init route =
case route of
Route.Home ->
pages.home.init ()
Route.Menu ->
pages.menu.init ()
Route.Faqs ->
pages.faqs.init ()updateupdate : Msg -> Model -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update bigMsg bigModel =
case ( bigMsg, bigModel ) of
( Home_Msg msg, Home_Model model ) ->
pages.home.update msg model
( Menu_Msg msg, Menu_Model model ) ->
pages.menu.update msg model
( Faqs_Msg msg, Faqs_Model model ) ->
pages.faqs.update msg model
_ ->
always ( bigModel, Cmd.none, Cmd.none )bundleWith elm-spa, we don't need to write out a case expression for both view and subscriptions.
We can just write one that bundles them together.
We can create view and subscriptions from this single function:
import Spa
bundle : Model -> Global.Model -> Spa.Bundle Msg
bundle bigModel =
case bigModel of
Home_Model model ->
pages.home.bundle model
Menu_Model model ->
pages.menu.bundle model
Faqs_Model model ->
pages.faqs.bundle model
view : Model -> Global.Model -> Document Msg
view model =
bundle model >> .view
subscriptions : Model -> Global.Model -> Sub Msg
subscriptions model =
bundle model >> .subscriptionsYou can check out https://elm-spa.dev or join #elm-spa-users on the official Elm slack channel for any questions ❤️