From 581e47eb002f553deed4a9d5b57dbc29679aca99 Mon Sep 17 00:00:00 2001 From: Ryan Haskell-Glatz Date: Sun, 29 Mar 2020 20:51:10 -0500 Subject: [PATCH] add advanced module for non-html apps --- README.md | 49 ++++++-- elm.json | 5 +- src/Spa.elm | 45 +++---- src/Spa/Advanced.elm | 293 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 347 insertions(+), 45 deletions(-) create mode 100644 src/Spa/Advanced.elm diff --git a/README.md b/README.md index 8313bb2..52087d5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/elm.json b/elm.json index 4775c7b..01a4d1c 100644 --- a/elm.json +++ b/elm.json @@ -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": { diff --git a/src/Spa.elm b/src/Spa.elm index a281e79..a61bee2 100644 --- a/src/Spa.elm +++ b/src/Spa.elm @@ -209,6 +209,7 @@ You can check out 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 } - } + ) {-| diff --git a/src/Spa/Advanced.elm b/src/Spa/Advanced.elm new file mode 100644 index 0000000..65ef5d5 --- /dev/null +++ b/src/Spa/Advanced.elm @@ -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 + }