add advanced module for non-html apps

This commit is contained in:
Ryan Haskell-Glatz 2020-03-29 20:51:10 -05:00
parent 2cc0bbfaea
commit 581e47eb00
4 changed files with 347 additions and 45 deletions

View File

@ -8,11 +8,13 @@ When you create an app with the [elm/browser](https://package.elm-lang.org/packa
__elm-spa__ uses that design at the page-level, so you can quickly add new pages to your Elm application!
Make your page as simple as you need:
✅ Automatically generate routes and pages
✅ Read and update global state across pages
## static pages
```elm
module Pages.Home exposing (page)
-- can render a static page
page : Page Flags Model Msg
page =
@ -21,9 +23,9 @@ page =
}
```
```elm
module Pages.About exposing (page)
## sandbox pages
```elm
-- can keep track of page state
page : Page Flags Model Msg
page =
@ -34,9 +36,9 @@ page =
}
```
```elm
module Pages.Posts exposing (page)
## element pages
```elm
-- can perform side effects
page : Page Flags Model Msg
page =
@ -48,9 +50,9 @@ page =
}
```
```elm
module Pages.SignIn exposing (page)
## component pages
```elm
-- can read and update global state
page : Page Flags Model Msg
page =
@ -62,7 +64,14 @@ page =
}
```
### putting your pages together is super easy!
## easily put together pages!
The reason we return the same `Page` type is to make it super
easy to write top-level `init`, `update`, `view`, and `susbcriptions` functions.
(And if you're using the [official cli tool](https://npmjs.org/elm-spa), this code will be automatically generated for you)
### `init`
```elm
init : Route -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
@ -74,6 +83,8 @@ init route =
Route.SignIn -> pages.signIn.init ()
```
### `update`
```elm
update : Msg -> Model -> Global.Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update bigMsg bigModel =
@ -94,6 +105,8 @@ update bigMsg bigModel =
always ( bigModel, Cmd.none, Cmd.none )
```
### `view` + `subscriptions`
```elm
-- handle view and subscriptions in one case expression!
bundle : Model -> Global.Model -> { view : Document Msg, subscriptions : Sub Msg }
@ -127,10 +140,22 @@ elm install ryannhg/elm-spa
### rather see an example?
This repo comes with an example project that you can
play around with. add in some pages and see how it works!
play around with. Add in some pages and see how it works!
#### html example
```
git clone https://github.com/ryannhg/elm-spa
cd example
cd elm-spa/examples/html
npm start
```
#### elm-ui example
```
git clone https://github.com/ryannhg/elm-spa
cd elm-spa/examples/elm-ui
npm start
```
The __elm-spa__ will be running at http://localhost:8000

View File

@ -3,9 +3,10 @@
"name": "ryannhg/elm-spa",
"summary": "a way to build single page apps with Elm",
"license": "BSD-3-Clause",
"version": "4.0.0",
"version": "4.1.0",
"exposed-modules": [
"Spa"
"Spa",
"Spa.Advanced"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {

View File

@ -209,6 +209,7 @@ You can check out <https://elm-spa.dev> or join #elm-spa-users on the official E
import Browser exposing (Document)
import Html
import Spa.Advanced as Advanced
@ -242,12 +243,8 @@ static :
{ view : Document msg
}
-> Page flags () msg globalModel globalMsg
static options =
{ init = \_ _ -> ( (), Cmd.none, Cmd.none )
, update = \_ _ model -> ( model, Cmd.none, Cmd.none )
, view = \_ _ -> options.view
, subscriptions = \_ _ -> Sub.none
}
static =
Advanced.static
{-|
@ -269,12 +266,8 @@ sandbox :
, view : model -> Document msg
}
-> Page flags model msg globalModel globalMsg
sandbox options =
{ init = \_ _ -> ( options.init, Cmd.none, Cmd.none )
, update = \_ msg model -> ( options.update msg model, Cmd.none, Cmd.none )
, view = always options.view
, subscriptions = \_ _ -> Sub.none
}
sandbox =
Advanced.sandbox
{-|
@ -296,12 +289,8 @@ element :
, subscriptions : model -> Sub msg
}
-> Page flags model msg globalModel globalMsg
element page =
{ init = \_ flags -> page.init flags |> (\( model, cmd ) -> ( model, cmd, Cmd.none ))
, update = \_ msg model -> page.update msg model |> (\( model_, cmd ) -> ( model_, cmd, Cmd.none ))
, subscriptions = always page.subscriptions
, view = always page.view
}
element =
Advanced.element
{-|
@ -327,7 +316,7 @@ component :
}
-> Page flags model msg globalModel globalMsg
component =
identity
Advanced.component
{-| For each page we export from our `Pages.*` modules, we should call the `upgrade` function with the corresponding `Model` and `Msg` variants, like this:
@ -349,19 +338,13 @@ upgrade :
, update : pageMsg -> pageModel -> globalModel -> ( model, Cmd msg, Cmd globalMsg )
, bundle : pageModel -> globalModel -> Bundle msg
}
upgrade toModel toMsg page =
{ init =
\flags global ->
page.init global flags |> (\( model, cmd, globalCmd ) -> ( toModel model, Cmd.map toMsg cmd, globalCmd ))
, update =
\msg model global ->
page.update global msg model |> (\( model_, cmd, globalCmd ) -> ( toModel model_, Cmd.map toMsg cmd, globalCmd ))
, bundle =
\model global ->
{ view = page.view global model |> (\doc -> { title = doc.title, body = List.map (Html.map toMsg) doc.body })
, subscriptions = page.subscriptions global model |> Sub.map toMsg
upgrade =
Advanced.upgrade
(\toMsg doc ->
{ title = doc.title
, body = List.map (Html.map toMsg) doc.body
}
}
)
{-|

293
src/Spa/Advanced.elm Normal file
View File

@ -0,0 +1,293 @@
module Spa.Advanced exposing
( Page
, static
, sandbox
, element
, component
, upgrade
, Bundle
)
{-|
## prefer elm-ui?
If you'd rather use something like [elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/)
or have your page's `view` functions return something besides `Html`, this module
extends the `Spa.Page` module's API with one additional parameter.
@docs Page
## avoid doing this
Instead of having all the pages in your app specify all 6 parameters
_every time_:
import Browser exposing (Document)
import Global
import Spa
page : Spa.Page Flags Model Msg Global.Model Global.Msg (Document Msg)
page =
Spa.static
{ view = view
}
## try this instead!
It's recommended to create a single `src/Page.elm` file for your project, just
[like the one created by `elm-spa init`](https://github.com/ryannhg/elm-spa/blob/master/cli/projects/new/src/Page.elm).
The `Page` module exposes type aliases and functions that know which `Global.Model` and `Global.Msg`
to use, which allow the compiler to provide better error messages.
Additionally, it make your type annotations way easier to read!
import Page exposing (Document, Page)
page : Page Flags Model Msg
page =
Page.static
{ view = view
}
(If you're doing things correctly you should only see `import Spa` or
`import Spa.Advanced` once in your entire project!)
## compatible with the cli tool!
Using the technique describe above means the [cli tool](https://npmjs.org/elm-spa)
will be able to work
These links are full `src/Page.elm` implementations you can drop
into your app:
- [Using Html (with Browser.Document)](https://gist.github.com/ryannhg/914f45a83a980d7c765d62a093ad6f38)
- [Using Elm UI](https://gist.github.com/ryannhg/c501f9a31727c4917fccd669ffbd9ef3)
# static pages
@docs static
# sandbox pages
@docs sandbox
# element pages
@docs element
# component pages
@docs component
## upgrading pages
The `Page` module discussed above should also export
an `upgrade` function. This means providing an extra function
to map one view to another.
### an example with elm/browser
import Browser
import Html
type alias Document msg =
Browser.Document msg
upgrade =
Spa.Advanced.upgrade
(\fn doc ->
{ title = doc.title
, body = List.map (Html.map fn) doc.body
}
)
### an example with mdgriffith/elm-ui
import Element exposing (Element)
type alias Document msg =
{ title : String
, body : List (Element msg)
}
upgrade =
Spa.Advanced.upgrade
(\fn doc ->
{ title = doc.title
, body = List.map (Element.map fn) doc.body
}
)
@docs upgrade
@docs Bundle
-}
-- PAGE
{-| Just like [Spa.Page](https://package.elm-lang.org/packages/ryannhg/elm-spa/latest/Spa#Page),
but the `view` function returns `view_msg` instead of enforcing the use of
[Browser.Document msg](https://package.elm-lang.org/packages/elm/browser/latest/Browser#Document)
-}
type alias Page flags model msg globalModel globalMsg view_msg =
{ init : globalModel -> flags -> ( model, Cmd msg, Cmd globalMsg )
, update : globalModel -> msg -> model -> ( model, Cmd msg, Cmd globalMsg )
, view : globalModel -> model -> view_msg
, subscriptions : globalModel -> model -> Sub msg
}
{-|
import Page exposing (Page)
page : Page Flags Model Msg
page =
Page.static
{ view = view
}
-}
static :
{ view : view_msg
}
-> Page flags () msg globalModel globalMsg view_msg
static options =
{ init = \_ _ -> ( (), Cmd.none, Cmd.none )
, update = \_ _ model -> ( model, Cmd.none, Cmd.none )
, view = \_ _ -> options.view
, subscriptions = \_ _ -> Sub.none
}
{-|
import Page exposing (Page)
page : Page Flags Model Msg
page =
Page.sandbox
{ init = init
, update = update
, view = view
}
-}
sandbox :
{ init : model
, update : msg -> model -> model
, view : model -> view_msg
}
-> Page flags model msg globalModel globalMsg view_msg
sandbox options =
{ init = \_ _ -> ( options.init, Cmd.none, Cmd.none )
, update = \_ msg model -> ( options.update msg model, Cmd.none, Cmd.none )
, view = always options.view
, subscriptions = \_ _ -> Sub.none
}
{-|
import Page exposing (Page)
page : Page Flags Model Msg
page =
Page.element
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-}
element :
{ init : flags -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> view_msg
, subscriptions : model -> Sub msg
}
-> Page flags model msg globalModel globalMsg view_msg
element page =
{ init = \_ flags -> page.init flags |> (\( model, cmd ) -> ( model, cmd, Cmd.none ))
, update = \_ msg model -> page.update msg model |> (\( model_, cmd ) -> ( model_, cmd, Cmd.none ))
, subscriptions = always page.subscriptions
, view = always page.view
}
{-|
import Page exposing (Page)
page : Page Flags Model Msg
page =
Page.component
{ init = init
, update = update
, subscriptions = subscriptions
, view = view
}
-}
component :
{ init : globalModel -> flags -> ( model, Cmd msg, Cmd globalMsg )
, update : globalModel -> msg -> model -> ( model, Cmd msg, Cmd globalMsg )
, view : globalModel -> model -> view_msg
, subscriptions : globalModel -> model -> Sub msg
}
-> Page flags model msg globalModel globalMsg view_msg
component =
identity
{-| Same as [Spa.upgrade](https://package.elm-lang.org/packages/ryannhg/elm-spa/latest/Spa#upgrade), but needs an extra map function as the first argument
to upgrade one view to another!
-}
upgrade :
((pageMsg -> msg) -> view_pageMsg -> view_msg)
-> (pageModel -> model)
-> (pageMsg -> msg)
-> Page pageFlags pageModel pageMsg globalModel globalMsg view_pageMsg
->
{ init : pageFlags -> globalModel -> ( model, Cmd msg, Cmd globalMsg )
, update : pageMsg -> pageModel -> globalModel -> ( model, Cmd msg, Cmd globalMsg )
, bundle : pageModel -> globalModel -> Bundle msg view_msg
}
upgrade viewMap toModel toMsg page =
{ init =
\flags global ->
page.init global flags |> (\( model, cmd, globalCmd ) -> ( toModel model, Cmd.map toMsg cmd, globalCmd ))
, update =
\msg model global ->
page.update global msg model |> (\( model_, cmd, globalCmd ) -> ( toModel model_, Cmd.map toMsg cmd, globalCmd ))
, bundle =
\model global ->
{ view = page.view global model |> viewMap toMsg
, subscriptions = page.subscriptions global model |> Sub.map toMsg
}
}
{-| Bundle behaves the same as [Spa.Bundle](https://package.elm-lang.org/packages/ryannhg/elm-spa/latest/Spa#Bundle),
but supports more than just `Browser.Document msg` for the view's return type!
-}
type alias Bundle msg view_msg =
{ view : view_msg
, subscriptions : Sub msg
}