new effect api

This commit is contained in:
Ryan Haskell-Glatz 2021-02-04 22:55:53 -06:00
parent 6c8e1f1429
commit 86798cf5ea
27 changed files with 793 additions and 159 deletions

View File

@ -1,3 +1,7 @@
[build]
publish = "public"
command = "npm i elm-spa@beta && npx elm-spa build"
[[redirects]]
from = "/*"
to = "/index.html"

View File

@ -0,0 +1 @@
# Examples

View File

@ -0,0 +1 @@
# User Authentication

View File

@ -3,7 +3,7 @@
The [official __elm-spa__ CLI tool](https://npmjs.org/elm-spa) has a few commands to help you build single page applications. As we saw in [the previous section](/guide/overview), you can use the CLI from your terminal by running:
```terminal
npm install -g elm-spa@latest
npm install -g elm-spa@beta
```
At any time running `elm-spa` or `elm-spa help` will show you the available commands:
@ -24,20 +24,22 @@ The `new` command creates a new project in the current folder:
elm-spa new
```
This command will only create a few files, so don't worry about getting overwhelmed with new files in your repo! Other than a `.gitignore`, there are only 2 new files created.
This command will only create a few files, so don't worry about getting overwhelmed with new files in your repo! Other than a `.gitignore`, there are only 3 new files created.
File | Description
--- | ---
`elm.json` | Your project's dependencies.
`src` | An empty folder for your Elm code.
`src/Pages/Home_.elm` | The homepage.
`public/index.html` | The entrypoint to your application.
```
your-project/
- elm.json
- src/
- public/
- index.html
|- elm.json
|- src/
| |- Pages/
| |- Home_.elm
|- public/
|- index.html
```
The `public` folder is a place for static assets! For example, a file at `./public/style.css` will be available at `/style.css` in your web browser.

View File

@ -65,8 +65,8 @@ pre {
background-color: white;
}
main .markdown {
animation: fadeIn 200ms 300ms ease-in forwards;
main {
animation: fadeIn 200ms 400ms ease-in forwards;
opacity: 0;
}

View File

@ -1,12 +1,14 @@
module Domain.Index exposing
( Index, decoder
, Link, search
, Section, sections
)
{-|
@docs Index, decoder
@docs Link, search
@docs Section, sections
-}
@ -81,3 +83,73 @@ search query index =
}
)
|> List.filter (\link -> Utils.String.caseInsensitiveContains query link.label)
-- SECTIONS
type alias Section =
{ header : String
, url : String
, pages : List SectionLink
}
type alias SectionLink =
{ label : String
, url : String
}
sections : Index -> List Section
sections index =
let
sectionOrder =
[ "Guide"
, "Examples"
]
toLabelUrls =
List.filterMap
(\doc ->
doc.headers
|> Dict.filter (\_ level -> level == 1)
|> Dict.toList
|> List.head
|> Maybe.map (Tuple.first >> (\label -> { label = label, url = doc.url }))
)
topLevelLabelUrls : List { label : String, url : String }
topLevelLabelUrls =
let
isOneLevelDeep doc =
List.length (String.split "/" doc.url) == 2
in
index
|> List.filter isOneLevelDeep
|> toLabelUrls
toSection top children =
{ header = top.label
, url = top.url
, pages = children
}
in
topLevelLabelUrls
|> List.map
(\top ->
index
|> List.filter (.url >> (\url -> String.startsWith top.url url && url /= top.url))
|> toLabelUrls
|> toSection top
)
|> List.sortBy
(\section ->
sectionOrder
|> List.indexedMap Tuple.pair
|> List.filter (Tuple.second >> (==) section.header)
|> List.map Tuple.first
|> List.head
|> Maybe.withDefault -1
)

View File

@ -1,4 +1,4 @@
module Pages.Guide.Section_.Article_ exposing (Model, Msg, page)
module Pages.Examples exposing (Model, Msg, page)
import Page exposing (Page)
import Request exposing (Request)

View File

@ -0,0 +1,19 @@
module Pages.Examples.Section_ exposing (Model, Msg, page)
import Page exposing (Page)
import Request exposing (Request)
import Shared
import UI.Docs
page : Shared.Model -> Request params -> Page Model Msg
page =
UI.Docs.page
type alias Model =
UI.Docs.Model
type alias Msg =
UI.Docs.Msg

View File

@ -6,7 +6,6 @@ import Request exposing (Request)
import Shared
import UI
import UI.Layout
import Url exposing (Url)
import View exposing (View)

View File

@ -6,7 +6,6 @@ import Request exposing (Request)
import Shared
import UI
import UI.Layout
import Url exposing (Url)
import View exposing (View)

View File

@ -21,9 +21,14 @@ type alias Flags =
type alias Model =
{ index : Index
, token : Maybe Token
}
type alias Token =
()
type Msg
= NoOp

View File

@ -1,6 +1,5 @@
module UI.Docs exposing (Model, Msg, page)
import Gen.Route as Route
import Http
import Page exposing (Page)
import Request exposing (Request)
@ -47,16 +46,6 @@ withDefault fallback fetchable =
fallback
toMaybe : Fetchable value -> Maybe value
toMaybe fetchable =
case fetchable of
Success value ->
Just value
_ ->
Nothing
init : Url -> ( Model, Cmd Msg )
init url =
( Model UI.Layout.init Loading

View File

@ -15,7 +15,6 @@ module UI.Layout exposing
import Html exposing (Html)
import Html.Attributes as Attr
import Html.Events as Events
import Page exposing (Page, shared)
import Request exposing (Request)
import Shared
@ -81,7 +80,9 @@ viewDocumentation options markdownContent view =
, url = options.url
}
]
, Html.main_ [ Attr.class "col flex" ] view
, Html.main_ [ Attr.class "flex" ]
[ UI.row.lg [ UI.align.top ]
[ Html.div [ Attr.class "col flex" ] view
, Html.div [ Attr.class "hidden-mobile sticky pad-y-lg", Attr.style "width" "16em" ]
[ UI.Sidebar.viewTableOfContents
{ content = markdownContent
@ -91,6 +92,8 @@ viewDocumentation options markdownContent view =
]
]
]
]
]
navbar :

View File

@ -12,27 +12,6 @@ import Url exposing (Url)
import Utils.String
sidebarSections : List Section
sidebarSections =
[ Section "Guide"
"/guide"
[ Link "Overview" "/guide"
, Link "The CLI" "/guide/cli"
, Link "Routing" "/guide/routing"
, Link "Pages" "/guide/pages"
, Link "Shared State" "/guide/shared-state"
, Link "Requests" "/guide/requests"
, Link "Views" "/guide/views"
]
, Section "Examples"
"/guide"
[ Link "User Authentication" "/guide/users"
, Link "Elm UI" "/guide/apis"
, Link "Page Transitions" "/guide/transitions"
]
]
parseTableOfContents : String -> List Section
parseTableOfContents =
Markdown.Parser.parse
@ -46,12 +25,12 @@ parseTableOfContents =
type alias Section =
{ header : String
, url : String
, links : List Link
, pages : List Link
}
type alias Link =
{ name : String
{ label : String
, url : String
}
@ -76,16 +55,16 @@ headersToSections =
in
case ( level, current ) of
( Heading2, Just existing ) ->
( sections ++ [ existing ], Just { header = text, url = url, links = [] } )
( sections ++ [ existing ], Just { header = text, url = url, pages = [] } )
( Heading2, Nothing ) ->
( sections, Just { header = text, url = url, links = [] } )
( sections, Just { header = text, url = url, pages = [] } )
( Heading3, Just existing ) ->
( sections, Just { existing | links = existing.links ++ [ { name = text, url = url } ] } )
( sections, Just { existing | pages = existing.pages ++ [ { label = text, url = url } ] } )
( Heading3, Nothing ) ->
( sections ++ [ { header = text, url = url, links = [] } ], Nothing )
( sections ++ [ { header = text, url = url, pages = [] } ], Nothing )
in
List.foldl loop ( [], Nothing )
>> (\( sections, maybe ) ->
@ -135,7 +114,7 @@ tableOfContentsRenderer =
viewSidebar : { url : Url, index : Index } -> Html msg
viewSidebar { url } =
viewSidebar { url, index } =
let
viewSidebarLink : Link -> Html msg
viewSidebarLink link__ =
@ -144,15 +123,15 @@ viewSidebar { url } =
viewSidebarSection : Section -> Html msg
viewSidebarSection section =
UI.col.sm []
[ Html.h4 [ Attr.class "h4 bold" ] [ Html.text section.header ]
, if List.isEmpty section.links then
[ Html.a [ Attr.href section.url, Attr.class "h4 bold" ] [ Html.text section.header ]
, if List.isEmpty section.pages then
Html.text ""
else
UI.col.md [ Attr.class "border-left pad-y-sm pad-x-md align-left" ] (List.map viewSidebarLink section.links)
UI.col.md [ Attr.class "border-left pad-y-sm pad-x-md align-left" ] (List.map viewSidebarLink section.pages)
]
in
UI.col.md [] (List.map viewSidebarSection sidebarSections)
UI.col.md [] (List.map viewSidebarSection (Domain.Index.sections index))
viewDocumentationLink : Bool -> Link -> Html msg
@ -162,7 +141,7 @@ viewDocumentationLink isActive link__ =
, Attr.classList [ ( "bold text-blue", isActive ) ]
, Attr.href link__.url
]
[ Html.text link__.name ]
[ Html.text link__.label ]
viewTableOfContents : { url : Url, content : String } -> Html msg
@ -175,13 +154,13 @@ viewTableOfContents { url, content } =
viewTocSection : Section -> Html msg
viewTocSection section =
Html.div [ Attr.class "col gap-xs align-left" ]
[ viewTableOfContentsLink { name = section.header, url = section.url }
, if List.isEmpty section.links then
[ viewTableOfContentsLink { label = section.header, url = section.url }
, if List.isEmpty section.pages then
Html.text ""
else
Html.div [ Attr.class "col pad-left-sm pad-xs gap-sm" ]
(section.links
(section.pages
|> List.map (\l -> Html.div [ Attr.class "h6" ] [ viewTableOfContentsLink l ])
)
]

View File

@ -3,11 +3,12 @@
"name": "ryannhg/elm-spa",
"summary": "Single page apps made easy.",
"license": "BSD-3-Clause",
"version": "5.0.0",
"version": "5.1.0",
"exposed-modules": [
"ElmSpa.Request",
"ElmSpa.Page",
"ElmSpa.Beta"
"ElmSpa.Beta",
"ElmSpa.Internals.Page"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {

View File

@ -0,0 +1,427 @@
module ElmSpa.Internals.Page exposing
( Page
, static, sandbox, element, advanced
, protected
, Bundle, bundle
)
{-|
# **Pages**
@docs Page
@docs static, sandbox, element, advanced
# **User Authentication**
@docs protected
# For generated code
@docs Bundle, bundle
-}
import Browser.Navigation exposing (Key)
import ElmSpa.Request
import Url exposing (Url)
{-| Pages are the building blocks of **elm-spa**.
Instead of importing this module, your project will have a `Page` module with a much simpler type:
module Page exposing (Page, ...)
type Page model msg
This makes all the generic `route`, `effect`, and `view` arguments disappear!
-}
type Page shared request route effect view model msg
= Page (Internals shared request route effect view model msg)
{-| A page that only needs to render a static view.
import Page
page : Page () Never
page =
Page.static
{ view = view
}
-- view : View Never
-}
static :
effect
->
{ view : view
}
-> Page shared request route effect view () msg
static none page =
Page
(\_ _ ->
Ok
{ init = \_ -> ( (), none )
, update = \_ _ -> ( (), none )
, view = \_ -> page.view
, subscriptions = \_ -> Sub.none
}
)
{-| A page that can keep track of application state.
( Inspired by [`Browser.sandbox`](https://package.elm-lang.org/packages/elm/browser/latest/Browser#sandbox) )
import Page
page : Page Model Msg
page =
Page.sandbox
{ init = init
, update = update
, view = view
}
-- init : Model
-- update : Msg -> Model -> Model
-- view : Model -> View Msg
-}
sandbox :
effect
->
{ init : model
, update : msg -> model -> model
, view : model -> view
}
-> Page shared request route effect view model msg
sandbox none page =
Page
(\_ _ ->
Ok
{ init = \_ -> ( page.init, none )
, update = \msg model -> ( page.update msg model, none )
, view = page.view
, subscriptions = \_ -> Sub.none
}
)
{-| A page that can handle effects like [HTTP requests or subscriptions](https://guide.elm-lang.org/effects/).
( Inspired by [`Browser.element`](https://package.elm-lang.org/packages/elm/browser/latest/Browser#element) )
import Page
page : Page Model Msg
page =
Page.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
-- init : ( Model, Cmd Msg )
-- update : Msg -> Model -> ( Model, Cmd Msg )
-- view : Model -> View Msg
-- subscriptions : Model -> Sub Msg
-}
element :
(Cmd msg -> effect)
->
{ init : ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, view : model -> view
, subscriptions : model -> Sub msg
}
-> Page shared request route effect view model msg
element fromCmd page =
Page
(\_ _ ->
Ok
{ init = \_ -> page.init |> Tuple.mapSecond fromCmd
, update = \msg model -> page.update msg model |> Tuple.mapSecond fromCmd
, view = page.view
, subscriptions = page.subscriptions
}
)
{-| A page that can handles **custom** effects like sending a `Shared.Msg` or other general user-defined effects.
import Effect
import Page
page : Page Model Msg
page =
Page.advanced
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
-- init : ( Model, Effect Msg )
-- update : Msg -> Model -> ( Model, Effect Msg )
-- view : Model -> View Msg
-- subscriptions : Model -> Sub Msg
-}
advanced :
{ init : ( model, effect )
, update : msg -> model -> ( model, effect )
, view : model -> view
, subscriptions : model -> Sub msg
}
-> Page shared request route effect view model msg
advanced page =
Page
(\_ _ ->
Ok
{ init = always page.init
, update = page.update
, view = page.view
, subscriptions = page.subscriptions
}
)
{-| Prefixing any of the four functions above with `protected` will guarantee that the page has access to a user. Here's an example with `sandbox`:
import Page
page : Page Model Msg
page =
Page.protected.sandbox
{ init = init
, update = update
, view = view
}
-- init : User -> Model
-- update : User -> Msg -> Model -> Model
-- update : User -> Model -> View Msg
-}
protected :
{ effectNone : effect
, fromCmd : Cmd msg -> effect
, user : shared -> request -> Maybe user
, route : route
}
->
{ static :
{ view : user -> view
}
-> Page shared request route effect view () msg
, sandbox :
{ init : user -> model
, update : user -> msg -> model -> model
, view : user -> model -> view
}
-> Page shared request route effect view model msg
, element :
{ init : user -> ( model, Cmd msg )
, update : user -> msg -> model -> ( model, Cmd msg )
, view : user -> model -> view
, subscriptions : user -> model -> Sub msg
}
-> Page shared request route effect view model msg
, advanced :
{ init : user -> ( model, effect )
, update : user -> msg -> model -> ( model, effect )
, view : user -> model -> view
, subscriptions : user -> model -> Sub msg
}
-> Page shared request route effect view model msg
}
protected options =
let
protect pageWithUser page =
Page
(\shared req ->
case options.user shared req of
Just user ->
Ok (pageWithUser user page)
Nothing ->
Err options.route
)
in
{ static =
protect
(\user page ->
{ init = \_ -> ( (), options.effectNone )
, update = \_ model -> ( model, options.effectNone )
, view = \_ -> page.view user
, subscriptions = \_ -> Sub.none
}
)
, sandbox =
protect
(\user page ->
{ init = \_ -> ( page.init user, options.effectNone )
, update = \msg model -> ( page.update user msg model, options.effectNone )
, view = page.view user
, subscriptions = \_ -> Sub.none
}
)
, element =
protect
(\user page ->
{ init = \_ -> page.init user |> Tuple.mapSecond options.fromCmd
, update = \msg model -> page.update user msg model |> Tuple.mapSecond options.fromCmd
, view = page.view user
, subscriptions = page.subscriptions user
}
)
, advanced =
protect
(\user page ->
{ init = \_ -> page.init user
, update = page.update user
, view = page.view user
, subscriptions = page.subscriptions user
}
)
}
-- UPGRADING FOR GENERATED CODE
type alias Request route params =
ElmSpa.Request.Request route params
{-| -}
type alias Bundle params model msg shared effect pagesModel pagesMsg pagesView =
{ init : params -> shared -> Url -> Key -> ( pagesModel, effect )
, update : params -> msg -> model -> shared -> Url -> Key -> ( pagesModel, effect )
, view : params -> model -> shared -> Url -> Key -> pagesView
, subscriptions : params -> model -> shared -> Url -> Key -> Sub pagesMsg
}
{-| This function is used by the generated code to connect your pages together.
It's big, spooky, and makes writing **elm-spa** pages really nice!
-}
bundle :
{ redirecting : { model : pagesModel, view : pagesView }
, toRoute : Url -> route
, toUrl : route -> String
, fromCmd : Cmd any -> pagesEffect
, mapEffect : effect -> pagesEffect
, mapView : view -> pagesView
, page : shared -> Request route params -> Page shared (Request route params) route effect view model msg
, toModel : params -> model -> pagesModel
, toMsg : msg -> pagesMsg
}
-> Bundle params model msg shared pagesEffect pagesModel pagesMsg pagesView
-- { init : params -> shared -> Url -> Key -> ( pagesModel, pagesEffect )
-- , update : params -> msg -> model -> shared -> Url -> Key -> ( pagesModel, pagesEffect )
-- , view : params -> model -> shared -> Url -> Key -> pagesView
-- , subscriptions : params -> model -> shared -> Url -> Key -> Sub pagesMsg
-- }
bundle { redirecting, toRoute, toUrl, fromCmd, mapEffect, mapView, page, toModel, toMsg } =
{ init =
\params shared url key ->
let
req =
ElmSpa.Request.create (toRoute url) params url key
in
case toResult page shared req of
Ok record ->
record.init ()
|> Tuple.mapBoth (toModel req.params) mapEffect
Err route ->
( redirecting.model, fromCmd <| Browser.Navigation.replaceUrl req.key (toUrl route) )
, update =
\params msg model shared url key ->
let
req =
ElmSpa.Request.create (toRoute url) params url key
in
case toResult page shared req of
Ok record ->
record.update msg model
|> Tuple.mapBoth (toModel req.params) mapEffect
Err route ->
( redirecting.model, fromCmd <| Browser.Navigation.replaceUrl req.key (toUrl route) )
, view =
\params model shared url key ->
let
req =
ElmSpa.Request.create (toRoute url) params url key
in
case toResult page shared req of
Ok record ->
record.view model
|> mapView
Err _ ->
redirecting.view
, subscriptions =
\params model shared url key ->
let
req =
ElmSpa.Request.create (toRoute url) params url key
in
case toResult page shared req of
Ok record ->
record.subscriptions model
|> Sub.map toMsg
Err _ ->
Sub.none
}
toResult :
(shared -> Request route params -> Page shared (Request route params) route effect view model msg)
-> shared
-> Request route params
-> Result route (PageRecord effect view model msg)
toResult toPage shared req =
let
(Page toResult_) =
toPage shared req
in
toResult_ shared req
-- INTERNALS
type alias Internals shared request route effect view model msg =
shared -> request -> Result route (PageRecord effect view model msg)
type alias PageRecord effect view model msg =
{ init : () -> ( model, effect )
, update : msg -> model -> ( model, effect )
, view : model -> view
, subscriptions : model -> Sub msg
}

View File

@ -1,6 +1,6 @@
{
"name": "elm-spa",
"version": "6.0.3--beta",
"version": "6.0.4--beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "elm-spa",
"version": "6.0.3--beta",
"version": "6.0.4--beta",
"description": "single page apps made easy",
"bin": "dist/src/index.js",
"scripts": {

View File

@ -2,6 +2,7 @@ import path from 'path'
const reserved = {
homepage: 'Home_',
redirecting: 'Redirecting_',
notFound: 'NotFound'
}
@ -35,6 +36,7 @@ const config = {
terser: `npx terser`
},
defaults: [
[ 'Effect.elm' ],
[ 'Main.elm' ],
[ 'Shared.elm' ],
[ `Pages`, `${reserved.notFound}.elm` ],

View File

@ -0,0 +1,80 @@
module Effect exposing
( Effect, none, map, batch
, fromCmd, fromShared
, toCmd
)
{-|
@docs Effect, none, map, batch
@docs fromCmd, fromShared
@docs toCmd
-}
import Shared
import Task
type Effect msg
= None
| Cmd (Cmd msg)
| Shared Shared.Msg
| Batch (List (Effect msg))
none : Effect msg
none =
None
map : (a -> b) -> Effect a -> Effect b
map fn effect =
case effect of
None ->
None
Cmd cmd ->
Cmd (Cmd.map fn cmd)
Shared msg ->
Shared msg
Batch list ->
Batch (List.map (map fn) list)
fromCmd : Cmd msg -> Effect msg
fromCmd =
Cmd
fromShared : Shared.Msg -> Effect msg
fromShared =
Shared
batch : List (Effect msg) -> Effect msg
batch =
Batch
-- Used by Main.elm
toCmd : ( Shared.Msg -> msg, pageMsg -> msg ) -> Effect pageMsg -> Cmd msg
toCmd ( fromSharedMsg, fromPageMsg ) effect =
case effect of
None ->
Cmd.none
Cmd cmd ->
Cmd.map fromPageMsg cmd
Shared msg ->
Task.succeed msg
|> Task.perform fromSharedMsg
Batch list ->
Cmd.batch (List.map (toCmd ( fromSharedMsg, fromPageMsg )) list)

View File

@ -2,9 +2,10 @@ module Main exposing (main)
import Browser
import Browser.Navigation as Nav exposing (Key)
import Effect
import Gen.Pages as Pages
import Gen.Route as Route
import Request exposing (Request)
import Request
import Shared
import Url exposing (Url)
import View
@ -40,14 +41,13 @@ init flags url key =
( shared, sharedCmd ) =
Shared.init (Request.create () url key) flags
( page, pageCmd, sharedPageCmd ) =
( page, effect ) =
Pages.init (Route.fromUrl url) shared url key
in
( Model url key shared page
, Cmd.batch
[ Cmd.map Shared sharedCmd
, Cmd.map Shared sharedPageCmd
, Cmd.map Page pageCmd
, Effect.toCmd ( Shared, Page ) effect
]
)
@ -79,14 +79,11 @@ update msg model =
ChangedUrl url ->
if url.path /= model.url.path then
let
( page, pageCmd, sharedPageCmd ) =
( page, effect ) =
Pages.init (Route.fromUrl url) model.shared url model.key
in
( { model | url = url, page = page }
, Cmd.batch
[ Cmd.map Page pageCmd
, Cmd.map Shared sharedPageCmd
]
, Effect.toCmd ( Shared, Page ) effect
)
else
@ -103,14 +100,11 @@ update msg model =
Page pageMsg ->
let
( page, pageCmd, sharedPageCmd ) =
( page, effect ) =
Pages.update pageMsg model.page model.shared model.url model.key
in
( { model | page = page }
, Cmd.batch
[ Cmd.map Page pageCmd
, Cmd.map Shared sharedPageCmd
]
, Effect.toCmd ( Shared, Page ) effect
)

View File

@ -1,26 +1,54 @@
module Page exposing
( Page
, static, sandbox, element, shared
, static, sandbox, element, advanced
, protected
)
{-|
@docs Page
@docs static, sandbox, element, shared
@docs static, sandbox, element, advanced
-}
import ElmSpa.Page
import Effect exposing (Effect)
import ElmSpa.Internals.Page as ElmSpa
import Gen.Route exposing (Route)
import Request exposing (Request)
import Shared
import View exposing (View)
-- PROTECTED OPTIONS
{-| Replace "()" with your User type
-}
type alias User =
()
{-| This function attempts to get your user from shared state.
-}
getUser : Shared.Model -> Request () -> Maybe User
getUser _ _ =
Nothing
{-| This is the route elm-spa redirects to when a user is not signed in on a protected page.
-}
unauthorizedRoute : Route
unauthorizedRoute =
Gen.Route.NotFound
-- PAGES
type alias Page model msg =
{ init : () -> ( model, Cmd msg, List Shared.Msg )
, update : msg -> model -> ( model, Cmd msg, List Shared.Msg )
, view : model -> View msg
, subscriptions : model -> Sub msg
}
ElmSpa.Page Shared.Model (Request ()) Gen.Route.Route (Effect msg) (View msg) model msg
static :
@ -28,7 +56,7 @@ static :
}
-> Page () Never
static =
ElmSpa.Page.static
ElmSpa.static Effect.none
sandbox :
@ -38,7 +66,7 @@ sandbox :
}
-> Page model msg
sandbox =
ElmSpa.Page.sandbox
ElmSpa.sandbox Effect.none
element :
@ -49,15 +77,50 @@ element :
}
-> Page model msg
element =
ElmSpa.Page.element
ElmSpa.element Effect.fromCmd
shared :
{ init : ( model, Cmd msg, List Shared.Msg )
, update : msg -> model -> ( model, Cmd msg, List Shared.Msg )
advanced :
{ init : ( model, Effect msg )
, update : msg -> model -> ( model, Effect msg )
, view : model -> View msg
, subscriptions : model -> Sub msg
}
-> Page model msg
shared =
ElmSpa.Page.shared
advanced =
ElmSpa.advanced
protected :
{ static :
{ view : User -> View msg
}
-> Page () msg
, sandbox :
{ init : User -> model
, update : User -> msg -> model -> model
, view : User -> model -> View msg
}
-> Page model msg
, element :
{ init : User -> ( model, Cmd msg )
, update : User -> msg -> model -> ( model, Cmd msg )
, view : User -> model -> View msg
, subscriptions : User -> model -> Sub msg
}
-> Page model msg
, advanced :
{ init : User -> ( model, Effect msg )
, update : User -> msg -> model -> ( model, Effect msg )
, view : User -> model -> View msg
, subscriptions : User -> model -> Sub msg
}
-> Page model msg
}
protected =
ElmSpa.protected
{ effectNone = Effect.none
, fromCmd = Effect.fromCmd
, user = getUser
, route = unauthorizedRoute
}

View File

@ -7,10 +7,8 @@ module Shared exposing
, update
)
import Browser.Navigation exposing (Key)
import Json.Decode as Json
import Request exposing (Request)
import Url exposing (Url)
type alias Flags =
@ -26,17 +24,17 @@ type Msg
init : Request () -> Flags -> ( Model, Cmd Msg )
init _ flags =
init _ _ =
( {}, Cmd.none )
update : Request () -> Msg -> Model -> ( Model, Cmd Msg )
update request msg model =
update _ msg model =
case msg of
NoOp ->
( model, Cmd.none )
subscriptions : Request () -> Model -> Sub Msg
subscriptions request model =
subscriptions _ _ =
Sub.none

View File

@ -1,4 +1,4 @@
module View exposing (View, map, placeholder, toBrowserDocument)
module View exposing (View, map, none, placeholder, toBrowserDocument)
import Browser
import Html exposing (Html)
@ -17,6 +17,11 @@ placeholder str =
}
none : View msg
none =
placeholder ""
map : (a -> b) -> View a -> View b
map fn view =
{ title = view.title

View File

@ -1,3 +1,4 @@
import config from "../config"
import {
pagesImports, paramsImports,
pagesModelDefinition,
@ -11,6 +12,6 @@ ${paramsImports(pages)}
${pagesImports(pages)}
${pagesModelDefinition(pages, options)}
${pagesModelDefinition([ [ config.reserved.redirecting ] ].concat(pages), options)}
`.trimLeft()

View File

@ -14,13 +14,15 @@ export default (pages : string[][], options : Options) : string => `
module Gen.Pages exposing (Model, Msg, init, subscriptions, update, view)
import Browser.Navigation exposing (Key)
import Request exposing (Request)
import Effect exposing (Effect)
import ElmSpa.Internals.Page
${paramsImports(pages)}
import Gen.Model as Model
import Gen.Msg as Msg
import Gen.Route as Route exposing (Route)
import Page exposing (Page)
${pagesImports(pages)}
import Request exposing (Request)
import Shared
import Task
import Url exposing (Url)
@ -35,12 +37,12 @@ type alias Msg =
Msg.Msg
init : Route -> Shared.Model -> Url -> Key -> ( Model, Cmd Msg, Cmd Shared.Msg )
init : Route -> Shared.Model -> Url -> Key -> ( Model, Effect Msg )
init route =
${pagesInitBody(pages)}
update : Msg -> Model -> Shared.Model -> Url -> Key -> ( Model, Cmd Msg, Cmd Shared.Msg )
update : Msg -> Model -> Shared.Model -> Url -> Key -> ( Model, Effect Msg )
update msg_ model_ =
${pagesUpdateBody(pages.filter(page => options.isStatic(page) === false), options)}
${pages.length > 1 ? pagesUpdateCatchAll : ''}
@ -67,48 +69,23 @@ ${pagesBundleDefinition(pages, options)}
type alias Bundle params model msg =
{ init : params -> Shared.Model -> Url -> Key -> ( Model, Cmd Msg, Cmd Shared.Msg )
, update : params -> msg -> model -> Shared.Model -> Url -> Key -> ( Model, Cmd Msg, Cmd Shared.Msg )
, view : params -> model -> Shared.Model -> Url -> Key -> View Msg
, subscriptions : params -> model -> Shared.Model -> Url -> Key -> Sub Msg
}
ElmSpa.Internals.Page.Bundle params model msg Shared.Model (Effect Msg) Model Msg (View Msg)
bundle :
(Shared.Model -> Request params -> Page model msg)
-> (params -> model -> Model)
-> (msg -> Msg)
-> Bundle params model msg
bundle page toModel toMsg =
let
mapTriple :
params
-> ( model, Cmd msg, List Shared.Msg )
-> ( Model, Cmd Msg, Cmd Shared.Msg )
mapTriple params ( model, cmd, sharedMsgList ) =
( toModel params model
, Cmd.map toMsg cmd
, sharedMsgList
|> List.map (Task.succeed >> Task.perform identity)
|> Cmd.batch
)
in
{ init =
\\params shared url key ->
(page shared (Request.create params url key)).init ()
|> mapTriple params
, update =
\\params msg model shared url key ->
(page shared (Request.create params url key)).update msg model
|> mapTriple params
, view =
\\params model shared url key ->
(page shared (Request.create params url key)).view model
|> View.map toMsg
, subscriptions =
\\params model shared url key ->
(page shared (Request.create params url key)).subscriptions model
|> Sub.map toMsg
ElmSpa.Internals.Page.bundle
{ redirecting =
{ model = Model.Redirecting_
, view = View.none
}
, toRoute = Route.fromUrl
, toUrl = Route.toHref
, fromCmd = Effect.fromCmd
, mapEffect = Effect.map toMsg
, mapView = View.map toMsg
, toModel = toModel
, toMsg = toMsg
, page = page
}
@ -118,8 +95,8 @@ type alias Static params =
static : View Never -> (params -> Model) -> Static params
static view_ toModel =
{ init = \\params _ _ _ -> ( toModel params, Cmd.none, Cmd.none )
, update = \\params _ _ _ _ _ -> ( toModel params, Cmd.none, Cmd.none )
{ init = \\params _ _ _ -> ( toModel params, Effect.none )
, update = \\params _ _ _ _ _ -> ( toModel params, Effect.none )
, view = \\_ _ _ _ _ -> View.map never view_
, subscriptions = \\_ _ _ _ _ -> Sub.none
}

View File

@ -167,7 +167,9 @@ const pageModuleName = (path : string[]) : string =>
export const pagesModelDefinition = (paths : string[][], options : Options) : string =>
customType('Model',
paths.map(path =>
options.isStatic(path)
path[0] === config.reserved.redirecting
? config.reserved.redirecting
: options.isStatic(path)
? `${modelVariant(path)} ${params(path)}`
: `${modelVariant(path)} ${params(path)} ${model(path)}`
)
@ -238,10 +240,10 @@ export const pagesUpdateBody = (paths: string[][], options : Options) : string =
export const pagesUpdateCatchAll =
`
_ ->
\\_ _ _ -> ( model_, Cmd.none, Cmd.none )`
\\_ _ _ -> ( model_, Effect.none )`
export const pagesViewBody = (paths: string[][], options : Options) : string =>
indent(caseExpression(paths, {
indent(caseExpressionWithRedirectingModel(`\\_ _ _ -> View.none`, paths, {
variable: 'model_',
condition: path => `${destructuredModel(path, options)}`,
result: path => `pages.${bundleName(path)}.view ${pageModelArguments(path, options)}`
@ -249,12 +251,23 @@ export const pagesViewBody = (paths: string[][], options : Options) : string =>
export const pagesSubscriptionsBody = (paths: string[][], options : Options) : string =>
indent(caseExpression(paths, {
indent(caseExpressionWithRedirectingModel(`\\_ _ _ -> Sub.none`, paths, {
variable: 'model_',
condition: path => `${destructuredModel(path, options)}`,
result: path => `pages.${bundleName(path)}.subscriptions ${pageModelArguments(path, options)}`
}))
const caseExpressionWithRedirectingModel = (fallback : string, items: string[][], options : { variable : string, condition : (item: string[]) => string, result: (item: string[]) => string }) =>
caseExpression([ [ config.reserved.redirecting ] ].concat(items), {
variable: options.variable,
condition: (item) => item[0] === config.reserved.redirecting
? `Model.${config.reserved.redirecting}`
: options.condition(item),
result: (item) => item[0] === config.reserved.redirecting
? fallback
: options.result(item)
})
const caseExpression = <T>(items: T[], options : { variable : string, condition : (item: T) => string, result: (item: T) => string }) =>
`case ${options.variable} of
${items.map(item => ` ${options.condition(item)} ->\n ${options.result(item)}`).join('\n\n')}`