mirror of
https://github.com/ryannhg/elm-spa.git
synced 2024-11-22 17:52:33 +03:00
refactoring and documentation
This commit is contained in:
parent
21a03d5db7
commit
18676881f3
931
README.md
931
README.md
@ -1,484 +1,505 @@
|
|||||||
# ryannhg/elm-app
|
# ryannhg/elm-app
|
||||||
> a way to build single page apps with Elm.
|
> a way to build single page apps with Elm.
|
||||||
|
|
||||||
__Note:__ This package is still under design/development. (but one day, it may become an actual package!)
|
## installing
|
||||||
|
|
||||||
|
|
||||||
## try it out
|
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://github.com/ryannhg/elm-app
|
elm install ryannhg/elm-app
|
||||||
cd elm-app/examples/basic
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Mess around with the files in `examples/basic/src` and change things!
|
## motivation
|
||||||
|
|
||||||
|
Every time I try and create a single page application from scratch in Elm, I end up repeating a few first steps from scratch:
|
||||||
|
|
||||||
|
- __Routing__ - Implementing `onUrlChange` and `onUrlRequest` for my `update` function.
|
||||||
|
|
||||||
|
- __Page Transitions__ - Fading in the whole app on load and fading in/out the pages (not the persistent stuff) on route change.
|
||||||
|
|
||||||
|
- __Wiring up pages__ - Every page has it's own `Model`, `Msg`, `init`, `update`, `view`, and `subscriptions`. I need to bring those together at the top level.
|
||||||
|
|
||||||
|
- __Sharing app model__ - In addition to updating the _page model_, I need a way for pages to update the _shared app model_ used across pages (signed in users).
|
||||||
|
|
||||||
|
This package is an attempt to create a few abstractions on top of `Browser.application` to make creating single page applications focus on what makes __your app unique__.
|
||||||
|
|
||||||
|
|
||||||
## overview for readme scrollers
|
## is it real?
|
||||||
|
|
||||||
this package is a wrapper around Elm's `Browser.application`, adding in page transitions and utilities for adding in new pages and routes.
|
- A working demo is available online here: [https://elm-app-demo.netlify.com/](https://elm-app-demo.netlify.com/)
|
||||||
|
|
||||||
here's what it looks like to use it:
|
- And you can play around with an example yourself in the repo: [https://github.com/ryannhg/elm-app/tree/master/examples/basic](https://github.com/ryannhg/elm-app/tree/master/examples/basic) around with the files in `examples/basic/src` and change things!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## examples are helpful!
|
||||||
|
|
||||||
|
Let's walk through the package together, at a high-level, with some code!
|
||||||
|
|
||||||
|
|
||||||
### src/Main.elm
|
### src/Main.elm
|
||||||
> Uses `Application.create`
|
|
||||||
|
|
||||||
This is the entrypoint to the app, it imports a few things:
|
```
|
||||||
|
our-project/
|
||||||
|
elm.json
|
||||||
|
src/
|
||||||
|
Main.elm ✨
|
||||||
|
```
|
||||||
|
|
||||||
- `Application` - (this package)
|
This is the __entrypoint__ to the application, and connects all the parts of our `Application` together:
|
||||||
- `App` - the top level `Model`, `Msg`, `init`, `update`, `subscriptions`, and `view`
|
|
||||||
- `Context` - the shared state between pages.
|
```elm
|
||||||
- `Route` - the routes for your application
|
module Main exposing (main)
|
||||||
- `Flags` - the initial JSON sent into the app
|
|
||||||
|
import Application
|
||||||
|
|
||||||
|
main =
|
||||||
|
Application.create
|
||||||
|
{ routing = -- TODO
|
||||||
|
, layout = -- TODO
|
||||||
|
, pages = -- TODO
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, `Application.create` is a function that takes in a `record` with three properties:
|
||||||
|
|
||||||
|
1. __routing__ - handles URLs and page transitions
|
||||||
|
|
||||||
|
2. __layout__ - the app-level `init`, `update`, `view`, etc.
|
||||||
|
|
||||||
|
3. __pages__ - the page-level `init`, `update`, `view`, etc.
|
||||||
|
|
||||||
|
|
||||||
|
#### routing
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Main exposing (main)
|
||||||
|
|
||||||
|
import Application
|
||||||
|
import Route ✨
|
||||||
|
|
||||||
|
main =
|
||||||
|
Application.create
|
||||||
|
{ routing =
|
||||||
|
{ fromUrl = Route.fromUrl ✨
|
||||||
|
, toPath = Route.toPath ✨
|
||||||
|
, transitionSpeed = 200 ✨
|
||||||
|
}
|
||||||
|
, layout = -- TODO
|
||||||
|
, pages = -- TODO
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The record for `routing` only has three properties:
|
||||||
|
|
||||||
|
1. __fromUrl__ - a function that turns a `Url` into a `Route`
|
||||||
|
|
||||||
|
2. __toPath__ - a function that turns a `Route` into a `String` used for links.
|
||||||
|
|
||||||
|
3. __transitionSpeed__ - number of __milliseconds__ it takes to fade in/out pages.
|
||||||
|
|
||||||
|
The implementation for `fromUrl` and `toPath` don't come from the `src/Main.elm`. Instead we create a new file called `src/Route.elm`, which handles all this for us in one place!
|
||||||
|
|
||||||
|
We'll link to that in a bit!
|
||||||
|
|
||||||
|
#### layout
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Main exposing (main)
|
||||||
|
|
||||||
|
import Application
|
||||||
|
import Route
|
||||||
|
import Components.Layout as Layout ✨
|
||||||
|
|
||||||
|
main =
|
||||||
|
Application.create
|
||||||
|
{ routing =
|
||||||
|
{ fromUrl = Route.fromUrl
|
||||||
|
, toPath = Route.toPath
|
||||||
|
, transitionSpeed = 200
|
||||||
|
}
|
||||||
|
, layout =
|
||||||
|
{ init = Layout.init ✨
|
||||||
|
, update = Layout.update ✨
|
||||||
|
, view = Layout.view ✨
|
||||||
|
, subscriptions = Layout.subscriptions ✨
|
||||||
|
}
|
||||||
|
, pages = -- TODO
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `layout` property introduces four new pieces:
|
||||||
|
|
||||||
|
1. __init__ - how to initialize the shared state.
|
||||||
|
|
||||||
|
2. __update__ - how to update the app-level state (and routing commands).
|
||||||
|
|
||||||
|
3. __view__ - the app-level view (and where to render our page view)
|
||||||
|
|
||||||
|
4. __subscriptions__ - app-level subscriptions (regardless of which page we're on)
|
||||||
|
|
||||||
|
Just like before, a new file `src/Components/Layout.elm` will contain all the functions we'll need for the layout, so that `Main.elm` is relatively focused.
|
||||||
|
|
||||||
|
#### pages
|
||||||
|
|
||||||
|
|
||||||
```elm
|
```elm
|
||||||
module Main exposing (main)
|
module Main exposing (main)
|
||||||
|
|
||||||
import App
|
|
||||||
import Application exposing (Application)
|
|
||||||
import Context
|
|
||||||
import Flags exposing (Flags)
|
|
||||||
import Route exposing (Route)
|
|
||||||
|
|
||||||
|
|
||||||
main : Application Flags Context.Model Context.Msg App.Model App.Msg
|
|
||||||
main =
|
|
||||||
Application.create
|
|
||||||
{ transition = 200
|
|
||||||
, context =
|
|
||||||
{ init = Context.init
|
|
||||||
, update = Context.update
|
|
||||||
, view = Context.view
|
|
||||||
, subscriptions = Context.subscriptions
|
|
||||||
}
|
|
||||||
, page =
|
|
||||||
{ init = App.init
|
|
||||||
, update = App.update
|
|
||||||
, bundle = App.bundle
|
|
||||||
}
|
|
||||||
, route =
|
|
||||||
{ fromUrl = Route.fromUrl
|
|
||||||
, toPath = Route.toPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### src/Pages/Homepage.elm
|
|
||||||
> uses `Application.Page.static`
|
|
||||||
|
|
||||||
The homepage is just a static page, so it's just a `view` and a `title` for the browser tab:
|
|
||||||
|
|
||||||
```elm
|
|
||||||
module Pages.Homepage exposing
|
|
||||||
( title
|
|
||||||
, view
|
|
||||||
)
|
|
||||||
|
|
||||||
import Html exposing (..)
|
|
||||||
|
|
||||||
|
|
||||||
title : String
|
|
||||||
title =
|
|
||||||
"Homepage"
|
|
||||||
|
|
||||||
|
|
||||||
view : Html Never
|
|
||||||
view =
|
|
||||||
div []
|
|
||||||
[ h1 [] [ text "Homepage!" ]
|
|
||||||
, p [] [ text "It's boring, but it works!" ]
|
|
||||||
]
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### src/Pages/Counter.elm
|
|
||||||
> uses `Application.Page.Sandbox`
|
|
||||||
|
|
||||||
The counter page doesn't has a `Model` to maintain, so it needs an `init` and an `update`:
|
|
||||||
|
|
||||||
```elm
|
|
||||||
module Pages.Counter exposing
|
|
||||||
( Model
|
|
||||||
, Msg
|
|
||||||
, init
|
|
||||||
, title
|
|
||||||
, update
|
|
||||||
, view
|
|
||||||
)
|
|
||||||
|
|
||||||
import Html exposing (..)
|
|
||||||
import Html.Events as Events
|
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
|
||||||
{ counter : Int
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= Increment
|
|
||||||
| Decrement
|
|
||||||
|
|
||||||
|
|
||||||
title : Model -> String
|
|
||||||
title model =
|
|
||||||
"Counter: " ++ String.fromInt model.counter ++ " | elm-app"
|
|
||||||
|
|
||||||
|
|
||||||
init : Model
|
|
||||||
init =
|
|
||||||
{ counter = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
update : Msg -> Model -> Model
|
|
||||||
update msg model =
|
|
||||||
case msg of
|
|
||||||
Decrement ->
|
|
||||||
{ model | counter = model.counter - 1 }
|
|
||||||
|
|
||||||
Increment ->
|
|
||||||
{ model | counter = model.counter + 1 }
|
|
||||||
|
|
||||||
|
|
||||||
view : Model -> Html Msg
|
|
||||||
view model =
|
|
||||||
div []
|
|
||||||
[ h1 [] [ text "Counter!" ]
|
|
||||||
, p [] [ text "Even the browser tab updates!" ]
|
|
||||||
, div []
|
|
||||||
[ button [ Events.onClick Decrement ] [ text "-" ]
|
|
||||||
, text (String.fromInt model.counter)
|
|
||||||
, button [ Events.onClick Increment ] [ text "+" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/App.elm
|
|
||||||
|
|
||||||
There's one file where you wire these pages up, and the `Application.Page` module has a bunch of helpers for dealing with pages of different shapes and complexities.
|
|
||||||
|
|
||||||
(In theory, this file could be generated, but typing it isn't too hard either!)
|
|
||||||
|
|
||||||
```elm
|
|
||||||
module App exposing
|
|
||||||
( Model
|
|
||||||
, Msg
|
|
||||||
, bundle
|
|
||||||
, init
|
|
||||||
, update
|
|
||||||
)
|
|
||||||
|
|
||||||
import Application.Page as Page exposing (Context)
|
|
||||||
import Browser
|
|
||||||
import Context
|
|
||||||
import Flags exposing (Flags)
|
|
||||||
import Html exposing (Html)
|
|
||||||
import Pages.Counter
|
|
||||||
import Pages.Homepage
|
|
||||||
import Pages.NotFound
|
|
||||||
import Route exposing (Route)
|
|
||||||
|
|
||||||
|
|
||||||
type Model
|
|
||||||
= HomepageModel ()
|
|
||||||
| CounterModel Pages.Counter.Model
|
|
||||||
| NotFoundModel ()
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= HomepageMsg Never
|
|
||||||
| CounterMsg Pages.Counter.Msg
|
|
||||||
| NotFoundMsg Never
|
|
||||||
|
|
||||||
|
|
||||||
pages =
|
|
||||||
{ homepage =
|
|
||||||
Page.static
|
|
||||||
{ title = Pages.Homepage.title
|
|
||||||
, view = Pages.Homepage.view
|
|
||||||
, toModel = HomepageModel
|
|
||||||
}
|
|
||||||
, counter =
|
|
||||||
Page.sandbox
|
|
||||||
{ title = Pages.Counter.title
|
|
||||||
, init = Pages.Counter.init
|
|
||||||
, update = Pages.Counter.update
|
|
||||||
, view = Pages.Counter.view
|
|
||||||
, toModel = CounterModel
|
|
||||||
, toMsg = CounterMsg
|
|
||||||
}
|
|
||||||
, notFound =
|
|
||||||
Page.static
|
|
||||||
{ title = Pages.NotFound.title
|
|
||||||
, view = Pages.NotFound.view
|
|
||||||
, toModel = NotFoundModel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
init :
|
|
||||||
Context Flags Route Context.Model
|
|
||||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
|
||||||
init context =
|
|
||||||
case context.route of
|
|
||||||
Route.Homepage ->
|
|
||||||
Page.init
|
|
||||||
{ page = pages.homepage
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
Route.Counter ->
|
|
||||||
Page.init
|
|
||||||
{ page = pages.counter
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
Route.NotFound ->
|
|
||||||
Page.init
|
|
||||||
{ page = pages.notFound
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
update :
|
|
||||||
Context Flags Route Context.Model
|
|
||||||
-> Msg
|
|
||||||
-> Model
|
|
||||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
|
||||||
update context appMsg appModel =
|
|
||||||
case ( appModel, appMsg ) of
|
|
||||||
( HomepageModel model, HomepageMsg msg ) ->
|
|
||||||
Page.update
|
|
||||||
{ page = pages.homepage
|
|
||||||
, msg = msg
|
|
||||||
, model = model
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
( HomepageModel _, _ ) ->
|
|
||||||
( appModel
|
|
||||||
, Cmd.none
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
( CounterModel model, CounterMsg msg ) ->
|
|
||||||
Page.update
|
|
||||||
{ page = pages.counter
|
|
||||||
, msg = msg
|
|
||||||
, model = model
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
( CounterModel _, _ ) ->
|
|
||||||
( appModel
|
|
||||||
, Cmd.none
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
( NotFoundModel model, NotFoundMsg msg ) ->
|
|
||||||
Page.update
|
|
||||||
{ page = pages.notFound
|
|
||||||
, msg = msg
|
|
||||||
, model = model
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
( NotFoundModel _, _ ) ->
|
|
||||||
( appModel
|
|
||||||
, Cmd.none
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
bundle :
|
|
||||||
Context Flags Route Context.Model
|
|
||||||
-> Model
|
|
||||||
-> Page.Bundle Msg
|
|
||||||
bundle context appModel =
|
|
||||||
case appModel of
|
|
||||||
HomepageModel model ->
|
|
||||||
Page.bundle
|
|
||||||
{ page = pages.homepage
|
|
||||||
, model = model
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
CounterModel model ->
|
|
||||||
Page.bundle
|
|
||||||
{ page = pages.counter
|
|
||||||
, model = model
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
|
|
||||||
NotFoundModel model ->
|
|
||||||
Page.bundle
|
|
||||||
{ page = pages.notFound
|
|
||||||
, model = model
|
|
||||||
, context = context
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### src/Context.elm
|
|
||||||
|
|
||||||
This is the "component" that wraps your whole single page app.
|
|
||||||
|
|
||||||
It's `update` function has access to `navigateTo`, allowing page navigation.
|
|
||||||
|
|
||||||
The `view` function also has access to your page, so you can insert it where you like in the layout!
|
|
||||||
|
|
||||||
```elm
|
|
||||||
module Context exposing
|
|
||||||
( Model
|
|
||||||
, Msg(..)
|
|
||||||
, init
|
|
||||||
, subscriptions
|
|
||||||
, update
|
|
||||||
, view
|
|
||||||
)
|
|
||||||
|
|
||||||
import Application
|
import Application
|
||||||
import Data.User exposing (User)
|
import Route
|
||||||
import Flags exposing (Flags)
|
import Components.Layout as Layout
|
||||||
import Html exposing (..)
|
import Pages ✨
|
||||||
import Html.Attributes as Attr exposing (class)
|
|
||||||
import Html.Events as Events
|
|
||||||
import Route exposing (Route)
|
|
||||||
import Utils.Cmd
|
|
||||||
|
|
||||||
|
main =
|
||||||
type alias Model =
|
Application.create
|
||||||
{ user : Maybe User
|
{ routing =
|
||||||
|
{ fromUrl = Route.fromUrl
|
||||||
|
, toPath = Route.toPath
|
||||||
|
, transitionSpeed = 200
|
||||||
|
}
|
||||||
|
, layout =
|
||||||
|
{ init = Layout.init
|
||||||
|
, update = Layout.update
|
||||||
|
, view = Layout.view
|
||||||
|
, subscriptions = Layout.subscriptions
|
||||||
|
}
|
||||||
|
, pages =
|
||||||
|
{ init = Pages.init ✨
|
||||||
|
, update = Pages.update ✨
|
||||||
|
, bundle = Pages.bundle ✨
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= SignIn (Result String User)
|
|
||||||
| SignOut
|
|
||||||
|
|
||||||
|
|
||||||
init : Route -> Flags -> ( Model, Cmd Msg )
|
|
||||||
init route flags =
|
|
||||||
( { user = Nothing }
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
update :
|
|
||||||
Application.Messages Route msg
|
|
||||||
-> Route
|
|
||||||
-> Msg
|
|
||||||
-> Model
|
|
||||||
-> ( Model, Cmd Msg, Cmd msg )
|
|
||||||
update { navigateTo } route msg model =
|
|
||||||
case msg of
|
|
||||||
SignIn (Ok user) ->
|
|
||||||
( { model | user = Just user }
|
|
||||||
, Cmd.none
|
|
||||||
, navigateTo Route.Homepage
|
|
||||||
)
|
|
||||||
|
|
||||||
SignIn (Err _) ->
|
|
||||||
Utils.Cmd.pure model
|
|
||||||
|
|
||||||
SignOut ->
|
|
||||||
Utils.Cmd.pure { model | user = Nothing }
|
|
||||||
|
|
||||||
|
|
||||||
view :
|
|
||||||
{ route : Route
|
|
||||||
, context : Model
|
|
||||||
, toMsg : Msg -> msg
|
|
||||||
, viewPage : Html msg
|
|
||||||
}
|
|
||||||
-> Html msg
|
|
||||||
view { context, route, toMsg, viewPage } =
|
|
||||||
div [ class "layout" ]
|
|
||||||
[ Html.map toMsg (viewNavbar route context)
|
|
||||||
, div [ class "container" ] [ viewPage ]
|
|
||||||
, Html.map toMsg (viewFooter context)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
viewNavbar : Route -> Model -> Html Msg
|
|
||||||
viewNavbar currentRoute model =
|
|
||||||
header [ class "navbar" ]
|
|
||||||
[ div [ class "navbar__links" ]
|
|
||||||
(List.map
|
|
||||||
(viewLink currentRoute)
|
|
||||||
[ Route.Homepage, Route.Counter, Route.Random ]
|
|
||||||
)
|
|
||||||
, case model.user of
|
|
||||||
Just _ ->
|
|
||||||
button [ Events.onClick SignOut ] [ text <| "Sign out" ]
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
a [ Attr.href "/sign-in" ] [ text "Sign in" ]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
viewLink : Route -> Route -> Html msg
|
|
||||||
viewLink currentRoute route =
|
|
||||||
a
|
|
||||||
[ class "navbar__link-item"
|
|
||||||
, Attr.href (Route.toPath route)
|
|
||||||
, Attr.style "font-weight"
|
|
||||||
(if route == currentRoute then
|
|
||||||
"bold"
|
|
||||||
|
|
||||||
else
|
|
||||||
"normal"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
[ text (linkLabel route) ]
|
|
||||||
|
|
||||||
|
|
||||||
linkLabel : Route -> String
|
|
||||||
linkLabel route =
|
|
||||||
case route of
|
|
||||||
Route.Homepage ->
|
|
||||||
"Home"
|
|
||||||
|
|
||||||
Route.Counter ->
|
|
||||||
"Counter"
|
|
||||||
|
|
||||||
Route.SignIn ->
|
|
||||||
"Sign In"
|
|
||||||
|
|
||||||
Route.Random ->
|
|
||||||
"Random"
|
|
||||||
|
|
||||||
Route.NotFound ->
|
|
||||||
"Not found"
|
|
||||||
|
|
||||||
|
|
||||||
viewFooter : Model -> Html Msg
|
|
||||||
viewFooter model =
|
|
||||||
footer [ Attr.class "footer" ]
|
|
||||||
[ model.user
|
|
||||||
|> Maybe.map Data.User.username
|
|
||||||
|> Maybe.withDefault "not signed in"
|
|
||||||
|> (++) "Current user: "
|
|
||||||
|> text
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
subscriptions : Route -> Model -> Sub Msg
|
|
||||||
subscriptions route model =
|
|
||||||
Sub.none
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Still reading?
|
Much like the last property, `pages` is just a few functions.
|
||||||
|
|
||||||
Oh wow. Maybe you should just check out the [basic example](./examples/basic) included in the repo.
|
The `init` and `update` parts are fairly the same, but there's a new property that might look strange: `bundle`.
|
||||||
|
|
||||||
Just clone, `npm install` and `npm run dev` for a hot-reloading magical environment.
|
The "bundle" is a combination of `view`, `title`, `subscriptions` that allows our new `src/Pages.elm` file to reduce a bit of boilerplate! (There's a better explanation in the `src/Pages.elm` section of the guide.)
|
||||||
|
|
||||||
Add a page or something- and let me know how it goes!
|
#### that's it for Main.elm!
|
||||||
|
|
||||||
|
As the final touch, we can update our import statements to add in a type annotation for the `main` function:
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Main exposing (main)
|
||||||
|
|
||||||
|
import Application exposing (Application) ✨
|
||||||
|
import Flags exposing (Flags) ✨
|
||||||
|
import Global ✨
|
||||||
|
import Route exposing (Route) ✨
|
||||||
|
import Components.Layout as Layout
|
||||||
|
import Pages
|
||||||
|
|
||||||
|
main : Application Flags Route Global.Model Global.Msg Pages.Model Pages.Msg ✨
|
||||||
|
main =
|
||||||
|
Application.create
|
||||||
|
{ routing =
|
||||||
|
{ fromUrl = Route.fromUrl
|
||||||
|
, toPath = Route.toPath
|
||||||
|
, transitionSpeed = 200
|
||||||
|
}
|
||||||
|
, layout =
|
||||||
|
{ init = Layout.init
|
||||||
|
, update = Layout.update
|
||||||
|
, view = Layout.view
|
||||||
|
, subscriptions = Layout.subscriptions
|
||||||
|
}
|
||||||
|
, pages =
|
||||||
|
{ init = Pages.init
|
||||||
|
, update = Pages.update
|
||||||
|
, bundle = Pages.bundle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead of main being the traditional `Program Flags Model Msg` type, here we use `Application Flags Route Global.Model Global.Msg Pages.Model Pages.Msg`, which is very long and spooky!
|
||||||
|
|
||||||
|
This is caused by the fact that our `Application.create` needs to know more about the `Flags`, `Route`, `Global`, and `Pages` types so it can do work for us.
|
||||||
|
|
||||||
|
But enough of that– let's move on to routing next!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### src/Route.elm
|
||||||
|
|
||||||
|
```
|
||||||
|
our-project/
|
||||||
|
elm.json
|
||||||
|
src/
|
||||||
|
Main.elm
|
||||||
|
Route.elm ✨
|
||||||
|
```
|
||||||
|
|
||||||
|
in our new file, we need to create a [custom type](#custom-type) to handle all the possible routes.
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Route exposing (Route(..))
|
||||||
|
|
||||||
|
type Route
|
||||||
|
= Homepage
|
||||||
|
```
|
||||||
|
|
||||||
|
For now, there is only one route called `Homepage`.
|
||||||
|
|
||||||
|
__Note:__ Our `exposing` statement has `Route(..)` instead of `Route`. so we can access `Route.Homepage` outside of this module (We'll come back to that later)
|
||||||
|
|
||||||
|
For `src/Main.elm` to work, we also need to create and expose `fromUrl` and `toPath` so our application handles routing and page navigation correctly!
|
||||||
|
|
||||||
|
For that, we need to install the official `elm/url` package:
|
||||||
|
|
||||||
|
```
|
||||||
|
elm install elm/url
|
||||||
|
```
|
||||||
|
|
||||||
|
And use the newly installed `Url` and `Url.Parser` modules like this:
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Route exposing
|
||||||
|
( Route(..)
|
||||||
|
, fromUrl ✨
|
||||||
|
, toPath ✨
|
||||||
|
)
|
||||||
|
|
||||||
|
import Url exposing (Url) ✨
|
||||||
|
import Url.Parser as Parser exposing (Parser) ✨
|
||||||
|
|
||||||
|
type Route
|
||||||
|
= Homepage
|
||||||
|
|
||||||
|
fromUrl : Url -> Route ✨
|
||||||
|
-- TODO
|
||||||
|
|
||||||
|
toPath : Route -> String ✨
|
||||||
|
-- TODO
|
||||||
|
```
|
||||||
|
|
||||||
|
#### fromUrl
|
||||||
|
|
||||||
|
Let's get started on implementing `fromUrl` by using the `Parser` module:
|
||||||
|
|
||||||
|
```elm
|
||||||
|
type Route
|
||||||
|
= Homepage
|
||||||
|
| NotFound ✨ -- see note #2
|
||||||
|
|
||||||
|
fromUrl : Url -> Route
|
||||||
|
fromUrl url =
|
||||||
|
let
|
||||||
|
router =
|
||||||
|
Parser.oneOf
|
||||||
|
[ Parser.map Homepage Parser.top ✨ -- see note #1
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Parser.parse router url
|
||||||
|
|> Maybe.withDefault NotFound ✨ -- see note #2
|
||||||
|
```
|
||||||
|
|
||||||
|
__Notes__
|
||||||
|
|
||||||
|
1. Here we're matching the top url `/` with our `Homepage`
|
||||||
|
|
||||||
|
2. Because `Parser.parse` returns a `Maybe Route` because it not find a match in our `router`. That means we need to add a `NotFound` case (good catch, Elm!)
|
||||||
|
|
||||||
|
#### toPath
|
||||||
|
|
||||||
|
It turns out `toPath` is really easy, its just a case expression:
|
||||||
|
|
||||||
|
```elm
|
||||||
|
toPath : Route -> String ✨
|
||||||
|
toPath route =
|
||||||
|
case route of
|
||||||
|
Homepage -> "/"
|
||||||
|
NotFound -> "/not-found"
|
||||||
|
```
|
||||||
|
|
||||||
|
### that's it for Route.elm!
|
||||||
|
|
||||||
|
here's the complete file we made.
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Route exposing
|
||||||
|
( Route(..)
|
||||||
|
, fromUrl
|
||||||
|
, toPath
|
||||||
|
)
|
||||||
|
|
||||||
|
import Url exposing (Url)
|
||||||
|
import Url.Parser as Parser exposing (Parser)
|
||||||
|
|
||||||
|
type Route
|
||||||
|
= Homepage
|
||||||
|
| NotFound
|
||||||
|
|
||||||
|
fromUrl : Url -> Route
|
||||||
|
fromUrl url =
|
||||||
|
let
|
||||||
|
router =
|
||||||
|
Parser.oneOf
|
||||||
|
[ Parser.map Homepage Parser.top
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Parser.parse router url
|
||||||
|
|> Maybe.withDefault NotFound
|
||||||
|
|
||||||
|
toPath : Route -> String
|
||||||
|
toPath route =
|
||||||
|
case route of
|
||||||
|
Homepage -> "/"
|
||||||
|
NotFound -> "/not-found"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can learn how to add more routes by looking at:
|
||||||
|
|
||||||
|
1. __the `elm/url` docs__ - https://package.elm-lang.org/packages/elm/url/latest
|
||||||
|
|
||||||
|
2. __the example in this repo__ -https://github.com/ryannhg/elm-app/blob/master/examples/basic/src/Route.elm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### src/Flags.elm
|
||||||
|
|
||||||
|
```
|
||||||
|
our-project/
|
||||||
|
elm.json
|
||||||
|
src/
|
||||||
|
Main.elm
|
||||||
|
Route.elm
|
||||||
|
Flags.elm ✨
|
||||||
|
```
|
||||||
|
|
||||||
|
For this app, we don't actually have flags, so we return an empty tuple!
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Flags exposing (Flags)
|
||||||
|
|
||||||
|
type alias Flags = ()
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's move onto something more interesting!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### src/Global.elm
|
||||||
|
|
||||||
|
```
|
||||||
|
our-project/
|
||||||
|
elm.json
|
||||||
|
src/
|
||||||
|
Main.elm
|
||||||
|
Route.elm
|
||||||
|
Flags.elm
|
||||||
|
Global.elm ✨
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's create `src/Global.elm` to define the `Model` and `Msg` types we'll share across pages and use in our layout functions:
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Global exposing ( Model, Msg(..) )
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ isSignedIn : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= SignIn
|
||||||
|
| SignOut
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we create a simple record to keep track of the user's sign in status.
|
||||||
|
|
||||||
|
Let's use `Global.Model` and `Global.Msg` in our layout:
|
||||||
|
|
||||||
|
|
||||||
|
### src/Components/Layout.elm
|
||||||
|
|
||||||
|
```
|
||||||
|
our-project/
|
||||||
|
elm.json
|
||||||
|
src/
|
||||||
|
Main.elm
|
||||||
|
Route.elm
|
||||||
|
Flags.elm
|
||||||
|
Global.elm
|
||||||
|
Components/
|
||||||
|
Layout.elm ✨
|
||||||
|
```
|
||||||
|
|
||||||
|
To implement an app-level layout, we'll need a new file:
|
||||||
|
|
||||||
|
```elm
|
||||||
|
module Components.Layout exposing (init, update, view, subscriptions)
|
||||||
|
|
||||||
|
import Global
|
||||||
|
import Route exposing (Route)
|
||||||
|
|
||||||
|
-- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This file needs to export the following four functions:
|
||||||
|
|
||||||
|
#### init
|
||||||
|
|
||||||
|
```elm
|
||||||
|
init :
|
||||||
|
{ navigateTo : Route -> Cmd msg
|
||||||
|
, route : Route
|
||||||
|
, flags : Flags
|
||||||
|
}
|
||||||
|
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
|
||||||
|
init _ =
|
||||||
|
( { isSignedIn = False }
|
||||||
|
, Cmd.none
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Initially, our layout takes in three inputs:
|
||||||
|
|
||||||
|
- __messages__ - not used here, but allows programmatic navigation to other pages.
|
||||||
|
|
||||||
|
- __route__ - the current route
|
||||||
|
|
||||||
|
- __flags__ - the initial JSON passed in with the app.
|
||||||
|
|
||||||
|
For our example, we set `isSignedIn` to `False`, don't perform any `Global.Msg` side effects, nor use `messages.navigateTo` to change to another page.
|
||||||
|
|
||||||
|
#### update
|
||||||
|
|
||||||
|
```elm
|
||||||
|
update :
|
||||||
|
{ navigateTo : Route -> Cmd msg
|
||||||
|
, route : Route
|
||||||
|
, flags : Flags
|
||||||
|
}
|
||||||
|
-> Global.Msg
|
||||||
|
-> Global.Model
|
||||||
|
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
|
||||||
|
update { navigateTo } msg model =
|
||||||
|
case msg of
|
||||||
|
Global.SignIn ->
|
||||||
|
( { model | isSignedIn = True }
|
||||||
|
, Cmd.none
|
||||||
|
, navigateTo Route.Homepage
|
||||||
|
)
|
||||||
|
|
||||||
|
Global.SignOut ->
|
||||||
|
( { model | isSignedIn = False }
|
||||||
|
, Cmd.none
|
||||||
|
, navigateTo Route.SignIn
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, our layout's update takes four inputs:
|
||||||
|
|
||||||
|
- __messages__ - allows programmatic navigation to other pages.
|
||||||
|
|
||||||
|
- __route__ - the current route
|
||||||
|
|
||||||
|
- __flags__ - the initial JSON passed in with the app.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## uh... still writing the docs 😬
|
||||||
|
|
||||||
|
dont look at me... dont look at me!!!
|
||||||
|
10
elm.json
10
elm.json
@ -3,17 +3,17 @@
|
|||||||
"name": "ryannhg/elm-app",
|
"name": "ryannhg/elm-app",
|
||||||
"summary": "an experiment for making single page apps with Elm",
|
"summary": "an experiment for making single page apps with Elm",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"exposed-modules": [
|
"exposed-modules": [
|
||||||
"Application",
|
"Application",
|
||||||
"Application.Page"
|
"Application.Page"
|
||||||
],
|
],
|
||||||
"elm-version": "0.19.0 <= v < 0.20.0",
|
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"elm/browser": "1.0.1",
|
"elm/browser": "1.0.0 <= v < 2.0.0",
|
||||||
"elm/core": "1.0.2",
|
"elm/core": "1.0.0 <= v < 2.0.0",
|
||||||
"elm/html": "1.0.0",
|
"elm/html": "1.0.0 <= v < 2.0.0",
|
||||||
"elm/url": "1.0.0"
|
"elm/url": "1.0.0 <= v < 2.0.0"
|
||||||
},
|
},
|
||||||
"test-dependencies": {}
|
"test-dependencies": {}
|
||||||
}
|
}
|
@ -68,7 +68,7 @@ label div {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.layout__page {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
padding: 2rem 0;
|
padding: 2rem 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
21
examples/basic/src/Components/Footer.elm
Normal file
21
examples/basic/src/Components/Footer.elm
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
module Components.Footer exposing (view)
|
||||||
|
|
||||||
|
import Data.User exposing (User)
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes as Attr
|
||||||
|
|
||||||
|
|
||||||
|
type alias Options =
|
||||||
|
{ user : Maybe User
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
view : Options -> Html msg
|
||||||
|
view { user } =
|
||||||
|
footer [ Attr.class "footer" ]
|
||||||
|
[ user
|
||||||
|
|> Maybe.map Data.User.username
|
||||||
|
|> Maybe.withDefault "not signed in"
|
||||||
|
|> (++) "Current user: "
|
||||||
|
|> text
|
||||||
|
]
|
81
examples/basic/src/Components/Layout.elm
Normal file
81
examples/basic/src/Components/Layout.elm
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
module Components.Layout exposing
|
||||||
|
( init
|
||||||
|
, subscriptions
|
||||||
|
, update
|
||||||
|
, view
|
||||||
|
)
|
||||||
|
|
||||||
|
import Application
|
||||||
|
import Components.Footer
|
||||||
|
import Components.Navbar
|
||||||
|
import Data.User exposing (User)
|
||||||
|
import Flags exposing (Flags)
|
||||||
|
import Global
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes as Attr exposing (class)
|
||||||
|
import Html.Events as Events
|
||||||
|
import Route exposing (Route)
|
||||||
|
import Utils.Cmd
|
||||||
|
|
||||||
|
|
||||||
|
init :
|
||||||
|
{ messages : Application.Messages Route msg
|
||||||
|
, route : Route
|
||||||
|
, flags : Flags
|
||||||
|
}
|
||||||
|
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
|
||||||
|
init _ =
|
||||||
|
( { user = Nothing }
|
||||||
|
, Cmd.none
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
update :
|
||||||
|
{ messages : Application.Messages Route msg
|
||||||
|
, route : Route
|
||||||
|
, flags : Flags
|
||||||
|
}
|
||||||
|
-> Global.Msg
|
||||||
|
-> Global.Model
|
||||||
|
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
|
||||||
|
update { messages } msg model =
|
||||||
|
case msg of
|
||||||
|
Global.SignIn (Ok user) ->
|
||||||
|
( { model | user = Just user }
|
||||||
|
, Cmd.none
|
||||||
|
, messages.navigateTo Route.Homepage
|
||||||
|
)
|
||||||
|
|
||||||
|
Global.SignIn (Err _) ->
|
||||||
|
Utils.Cmd.pure model
|
||||||
|
|
||||||
|
Global.SignOut ->
|
||||||
|
Utils.Cmd.pure { model | user = Nothing }
|
||||||
|
|
||||||
|
|
||||||
|
view :
|
||||||
|
{ route : Route
|
||||||
|
, toMsg : Global.Msg -> msg
|
||||||
|
, viewPage : Html msg
|
||||||
|
}
|
||||||
|
-> Global.Model
|
||||||
|
-> Html msg
|
||||||
|
view { route, toMsg, viewPage } model =
|
||||||
|
div [ class "layout" ]
|
||||||
|
[ Html.map toMsg
|
||||||
|
(Components.Navbar.view
|
||||||
|
{ currentRoute = route
|
||||||
|
, user = model.user
|
||||||
|
, signOut = Global.SignOut
|
||||||
|
}
|
||||||
|
)
|
||||||
|
, div [ class "layout__page" ] [ viewPage ]
|
||||||
|
, Html.map toMsg
|
||||||
|
(Components.Footer.view { user = model.user })
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
subscriptions : Route -> Global.Model -> Sub Global.Msg
|
||||||
|
subscriptions route model =
|
||||||
|
Sub.none
|
69
examples/basic/src/Components/Navbar.elm
Normal file
69
examples/basic/src/Components/Navbar.elm
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
module Components.Navbar exposing (view)
|
||||||
|
|
||||||
|
import Data.User exposing (User)
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes as Attr exposing (class)
|
||||||
|
import Html.Events as Events
|
||||||
|
import Route exposing (Route)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Options msg =
|
||||||
|
{ currentRoute : Route
|
||||||
|
, user : Maybe User
|
||||||
|
, signOut : msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
view : Options msg -> Html msg
|
||||||
|
view { currentRoute, user, signOut } =
|
||||||
|
header [ class "navbar" ]
|
||||||
|
[ div [ class "navbar__links" ]
|
||||||
|
(List.map
|
||||||
|
(viewLink currentRoute)
|
||||||
|
[ Route.Homepage
|
||||||
|
, Route.Counter
|
||||||
|
, Route.Random
|
||||||
|
]
|
||||||
|
)
|
||||||
|
, case user of
|
||||||
|
Just _ ->
|
||||||
|
button [ Events.onClick signOut ] [ text <| "Sign out" ]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
a [ Attr.href "/sign-in" ] [ text "Sign in" ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewLink : Route -> Route -> Html msg
|
||||||
|
viewLink currentRoute route =
|
||||||
|
a
|
||||||
|
[ class "navbar__link-item"
|
||||||
|
, Attr.href (Route.toPath route)
|
||||||
|
, Attr.style "font-weight"
|
||||||
|
(if route == currentRoute then
|
||||||
|
"bold"
|
||||||
|
|
||||||
|
else
|
||||||
|
"normal"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
[ text (linkLabel route) ]
|
||||||
|
|
||||||
|
|
||||||
|
linkLabel : Route -> String
|
||||||
|
linkLabel route =
|
||||||
|
case route of
|
||||||
|
Route.Homepage ->
|
||||||
|
"Home"
|
||||||
|
|
||||||
|
Route.Counter ->
|
||||||
|
"Counter"
|
||||||
|
|
||||||
|
Route.SignIn ->
|
||||||
|
"Sign In"
|
||||||
|
|
||||||
|
Route.Random ->
|
||||||
|
"Random"
|
||||||
|
|
||||||
|
Route.NotFound ->
|
||||||
|
"Not found"
|
@ -1,138 +0,0 @@
|
|||||||
module Context exposing
|
|
||||||
( Model
|
|
||||||
, Msg(..)
|
|
||||||
, init
|
|
||||||
, subscriptions
|
|
||||||
, update
|
|
||||||
, view
|
|
||||||
)
|
|
||||||
|
|
||||||
import Application
|
|
||||||
import Data.User exposing (User)
|
|
||||||
import Flags exposing (Flags)
|
|
||||||
import Html exposing (..)
|
|
||||||
import Html.Attributes as Attr exposing (class)
|
|
||||||
import Html.Events as Events
|
|
||||||
import Route exposing (Route)
|
|
||||||
import Utils.Cmd
|
|
||||||
|
|
||||||
|
|
||||||
type alias Model =
|
|
||||||
{ user : Maybe User
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type Msg
|
|
||||||
= SignIn (Result String User)
|
|
||||||
| SignOut
|
|
||||||
|
|
||||||
|
|
||||||
init : Route -> Flags -> ( Model, Cmd Msg )
|
|
||||||
init route flags =
|
|
||||||
( { user = Nothing }
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
update :
|
|
||||||
Application.Messages Route msg
|
|
||||||
-> Route
|
|
||||||
-> Msg
|
|
||||||
-> Model
|
|
||||||
-> ( Model, Cmd Msg, Cmd msg )
|
|
||||||
update { navigateTo } route msg model =
|
|
||||||
case msg of
|
|
||||||
SignIn (Ok user) ->
|
|
||||||
( { model | user = Just user }
|
|
||||||
, Cmd.none
|
|
||||||
, navigateTo Route.Homepage
|
|
||||||
)
|
|
||||||
|
|
||||||
SignIn (Err _) ->
|
|
||||||
Utils.Cmd.pure model
|
|
||||||
|
|
||||||
SignOut ->
|
|
||||||
Utils.Cmd.pure { model | user = Nothing }
|
|
||||||
|
|
||||||
|
|
||||||
view :
|
|
||||||
{ route : Route
|
|
||||||
, context : Model
|
|
||||||
, toMsg : Msg -> msg
|
|
||||||
, viewPage : Html msg
|
|
||||||
}
|
|
||||||
-> Html msg
|
|
||||||
view { context, route, toMsg, viewPage } =
|
|
||||||
div [ class "layout" ]
|
|
||||||
[ Html.map toMsg (viewNavbar route context)
|
|
||||||
, div [ class "container" ] [ viewPage ]
|
|
||||||
, Html.map toMsg (viewFooter context)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
viewNavbar : Route -> Model -> Html Msg
|
|
||||||
viewNavbar currentRoute model =
|
|
||||||
header [ class "navbar" ]
|
|
||||||
[ div [ class "navbar__links" ]
|
|
||||||
(List.map
|
|
||||||
(viewLink currentRoute)
|
|
||||||
[ Route.Homepage, Route.Counter, Route.Random ]
|
|
||||||
)
|
|
||||||
, case model.user of
|
|
||||||
Just _ ->
|
|
||||||
button [ Events.onClick SignOut ] [ text <| "Sign out" ]
|
|
||||||
|
|
||||||
Nothing ->
|
|
||||||
a [ Attr.href "/sign-in" ] [ text "Sign in" ]
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
viewLink : Route -> Route -> Html msg
|
|
||||||
viewLink currentRoute route =
|
|
||||||
a
|
|
||||||
[ class "navbar__link-item"
|
|
||||||
, Attr.href (Route.toPath route)
|
|
||||||
, Attr.style "font-weight"
|
|
||||||
(if route == currentRoute then
|
|
||||||
"bold"
|
|
||||||
|
|
||||||
else
|
|
||||||
"normal"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
[ text (linkLabel route) ]
|
|
||||||
|
|
||||||
|
|
||||||
linkLabel : Route -> String
|
|
||||||
linkLabel route =
|
|
||||||
case route of
|
|
||||||
Route.Homepage ->
|
|
||||||
"Home"
|
|
||||||
|
|
||||||
Route.Counter ->
|
|
||||||
"Counter"
|
|
||||||
|
|
||||||
Route.SignIn ->
|
|
||||||
"Sign In"
|
|
||||||
|
|
||||||
Route.Random ->
|
|
||||||
"Random"
|
|
||||||
|
|
||||||
Route.NotFound ->
|
|
||||||
"Not found"
|
|
||||||
|
|
||||||
|
|
||||||
viewFooter : Model -> Html Msg
|
|
||||||
viewFooter model =
|
|
||||||
footer [ Attr.class "footer" ]
|
|
||||||
[ model.user
|
|
||||||
|> Maybe.map Data.User.username
|
|
||||||
|> Maybe.withDefault "not signed in"
|
|
||||||
|> (++) "Current user: "
|
|
||||||
|> text
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
subscriptions : Route -> Model -> Sub Msg
|
|
||||||
subscriptions route model =
|
|
||||||
Sub.none
|
|
16
examples/basic/src/Global.elm
Normal file
16
examples/basic/src/Global.elm
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module Global exposing
|
||||||
|
( Model
|
||||||
|
, Msg(..)
|
||||||
|
)
|
||||||
|
|
||||||
|
import Data.User exposing (User)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ user : Maybe User
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= SignIn (Result String User)
|
||||||
|
| SignOut
|
@ -1,29 +1,30 @@
|
|||||||
module Main exposing (main)
|
module Main exposing (main)
|
||||||
|
|
||||||
import App
|
|
||||||
import Application exposing (Application)
|
import Application exposing (Application)
|
||||||
import Context
|
import Components.Layout as Layout
|
||||||
import Flags exposing (Flags)
|
import Flags exposing (Flags)
|
||||||
|
import Global
|
||||||
|
import Pages
|
||||||
import Route exposing (Route)
|
import Route exposing (Route)
|
||||||
|
|
||||||
|
|
||||||
main : Application Flags Context.Model Context.Msg App.Model App.Msg
|
main : Application Flags Global.Model Global.Msg Pages.Model Pages.Msg
|
||||||
main =
|
main =
|
||||||
Application.create
|
Application.create
|
||||||
{ transition = 200
|
{ routing =
|
||||||
, context =
|
{ transition = 200
|
||||||
{ init = Context.init
|
, fromUrl = Route.fromUrl
|
||||||
, update = Context.update
|
|
||||||
, view = Context.view
|
|
||||||
, subscriptions = Context.subscriptions
|
|
||||||
}
|
|
||||||
, page =
|
|
||||||
{ init = App.init
|
|
||||||
, update = App.update
|
|
||||||
, bundle = App.bundle
|
|
||||||
}
|
|
||||||
, route =
|
|
||||||
{ fromUrl = Route.fromUrl
|
|
||||||
, toPath = Route.toPath
|
, toPath = Route.toPath
|
||||||
}
|
}
|
||||||
|
, layout =
|
||||||
|
{ init = Layout.init
|
||||||
|
, update = Layout.update
|
||||||
|
, view = Layout.view
|
||||||
|
, subscriptions = Layout.subscriptions
|
||||||
|
}
|
||||||
|
, pages =
|
||||||
|
{ init = Pages.init
|
||||||
|
, update = Pages.update
|
||||||
|
, bundle = Pages.bundle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
module App exposing
|
module Pages exposing
|
||||||
( Model
|
( Model
|
||||||
, Msg
|
, Msg
|
||||||
, bundle
|
, bundle
|
||||||
@ -6,10 +6,11 @@ module App exposing
|
|||||||
, update
|
, update
|
||||||
)
|
)
|
||||||
|
|
||||||
import Application.Page as Page exposing (Context)
|
import Application exposing (Bundle, Context)
|
||||||
|
import Application.Page as Page
|
||||||
import Browser
|
import Browser
|
||||||
import Context
|
|
||||||
import Flags exposing (Flags)
|
import Flags exposing (Flags)
|
||||||
|
import Global
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
import Pages.Counter
|
import Pages.Counter
|
||||||
import Pages.Homepage
|
import Pages.Homepage
|
||||||
@ -81,50 +82,50 @@ pages =
|
|||||||
|
|
||||||
|
|
||||||
init :
|
init :
|
||||||
Context Flags Route Context.Model
|
Context Flags Route Global.Model
|
||||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||||
init context =
|
init context =
|
||||||
case context.route of
|
case context.route of
|
||||||
Route.Homepage ->
|
Route.Homepage ->
|
||||||
Page.init
|
Application.init
|
||||||
{ page = pages.homepage
|
{ page = pages.homepage
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
Route.Counter ->
|
Route.Counter ->
|
||||||
Page.init
|
Application.init
|
||||||
{ page = pages.counter
|
{ page = pages.counter
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
Route.Random ->
|
Route.Random ->
|
||||||
Page.init
|
Application.init
|
||||||
{ page = pages.random
|
{ page = pages.random
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
Route.SignIn ->
|
Route.SignIn ->
|
||||||
Page.init
|
Application.init
|
||||||
{ page = pages.signIn
|
{ page = pages.signIn
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
Route.NotFound ->
|
Route.NotFound ->
|
||||||
Page.init
|
Application.init
|
||||||
{ page = pages.notFound
|
{ page = pages.notFound
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
update :
|
update :
|
||||||
Context Flags Route Context.Model
|
Context Flags Route Global.Model
|
||||||
-> Msg
|
-> Msg
|
||||||
-> Model
|
-> Model
|
||||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||||
update context appMsg appModel =
|
update context appMsg appModel =
|
||||||
case ( appModel, appMsg ) of
|
case ( appModel, appMsg ) of
|
||||||
( HomepageModel model, HomepageMsg msg ) ->
|
( HomepageModel model, HomepageMsg msg ) ->
|
||||||
Page.update
|
Application.update
|
||||||
{ page = pages.homepage
|
{ page = pages.homepage
|
||||||
, msg = msg
|
, msg = msg
|
||||||
, model = model
|
, model = model
|
||||||
@ -138,7 +139,7 @@ update context appMsg appModel =
|
|||||||
)
|
)
|
||||||
|
|
||||||
( CounterModel model, CounterMsg msg ) ->
|
( CounterModel model, CounterMsg msg ) ->
|
||||||
Page.update
|
Application.update
|
||||||
{ page = pages.counter
|
{ page = pages.counter
|
||||||
, msg = msg
|
, msg = msg
|
||||||
, model = model
|
, model = model
|
||||||
@ -152,7 +153,7 @@ update context appMsg appModel =
|
|||||||
)
|
)
|
||||||
|
|
||||||
( RandomModel model, RandomMsg msg ) ->
|
( RandomModel model, RandomMsg msg ) ->
|
||||||
Page.update
|
Application.update
|
||||||
{ page = pages.random
|
{ page = pages.random
|
||||||
, msg = msg
|
, msg = msg
|
||||||
, model = model
|
, model = model
|
||||||
@ -166,7 +167,7 @@ update context appMsg appModel =
|
|||||||
)
|
)
|
||||||
|
|
||||||
( SignInModel model, SignInMsg msg ) ->
|
( SignInModel model, SignInMsg msg ) ->
|
||||||
Page.update
|
Application.update
|
||||||
{ page = pages.signIn
|
{ page = pages.signIn
|
||||||
, msg = msg
|
, msg = msg
|
||||||
, model = model
|
, model = model
|
||||||
@ -180,7 +181,7 @@ update context appMsg appModel =
|
|||||||
)
|
)
|
||||||
|
|
||||||
( NotFoundModel model, NotFoundMsg msg ) ->
|
( NotFoundModel model, NotFoundMsg msg ) ->
|
||||||
Page.update
|
Application.update
|
||||||
{ page = pages.notFound
|
{ page = pages.notFound
|
||||||
, msg = msg
|
, msg = msg
|
||||||
, model = model
|
, model = model
|
||||||
@ -195,41 +196,41 @@ update context appMsg appModel =
|
|||||||
|
|
||||||
|
|
||||||
bundle :
|
bundle :
|
||||||
Context Flags Route Context.Model
|
Context Flags Route Global.Model
|
||||||
-> Model
|
-> Model
|
||||||
-> Page.Bundle Msg
|
-> Bundle Msg
|
||||||
bundle context appModel =
|
bundle context appModel =
|
||||||
case appModel of
|
case appModel of
|
||||||
HomepageModel model ->
|
HomepageModel model ->
|
||||||
Page.bundle
|
Application.bundle
|
||||||
{ page = pages.homepage
|
{ page = pages.homepage
|
||||||
, model = model
|
, model = model
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
CounterModel model ->
|
CounterModel model ->
|
||||||
Page.bundle
|
Application.bundle
|
||||||
{ page = pages.counter
|
{ page = pages.counter
|
||||||
, model = model
|
, model = model
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
RandomModel model ->
|
RandomModel model ->
|
||||||
Page.bundle
|
Application.bundle
|
||||||
{ page = pages.random
|
{ page = pages.random
|
||||||
, model = model
|
, model = model
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
SignInModel model ->
|
SignInModel model ->
|
||||||
Page.bundle
|
Application.bundle
|
||||||
{ page = pages.signIn
|
{ page = pages.signIn
|
||||||
, model = model
|
, model = model
|
||||||
, context = context
|
, context = context
|
||||||
}
|
}
|
||||||
|
|
||||||
NotFoundModel model ->
|
NotFoundModel model ->
|
||||||
Page.bundle
|
Application.bundle
|
||||||
{ page = pages.notFound
|
{ page = pages.notFound
|
||||||
, model = model
|
, model = model
|
||||||
, context = context
|
, context = context
|
@ -8,10 +8,10 @@ module Pages.SignIn exposing
|
|||||||
, view
|
, view
|
||||||
)
|
)
|
||||||
|
|
||||||
import Application.Page exposing (Context)
|
import Application exposing (Context)
|
||||||
import Context
|
|
||||||
import Data.User as User exposing (User)
|
import Data.User as User exposing (User)
|
||||||
import Flags exposing (Flags)
|
import Flags exposing (Flags)
|
||||||
|
import Global
|
||||||
import Html exposing (..)
|
import Html exposing (..)
|
||||||
import Html.Attributes as Attr
|
import Html.Attributes as Attr
|
||||||
import Html.Events as Events
|
import Html.Events as Events
|
||||||
@ -35,7 +35,7 @@ type Field
|
|||||||
| Password
|
| Password
|
||||||
|
|
||||||
|
|
||||||
title : Context Flags Route Context.Model -> Model -> String
|
title : Context Flags Route Global.Model -> Model -> String
|
||||||
title { context } model =
|
title { context } model =
|
||||||
case context.user of
|
case context.user of
|
||||||
Just user ->
|
Just user ->
|
||||||
@ -46,17 +46,17 @@ title { context } model =
|
|||||||
|
|
||||||
|
|
||||||
init :
|
init :
|
||||||
Context Flags Route Context.Model
|
Context Flags Route Global.Model
|
||||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||||
init _ =
|
init _ =
|
||||||
Utils.Cmd.pure { username = "", password = "" }
|
Utils.Cmd.pure { username = "", password = "" }
|
||||||
|
|
||||||
|
|
||||||
update :
|
update :
|
||||||
Context Flags Route Context.Model
|
Context Flags Route Global.Model
|
||||||
-> Msg
|
-> Msg
|
||||||
-> Model
|
-> Model
|
||||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||||
update _ msg model =
|
update _ msg model =
|
||||||
case msg of
|
case msg of
|
||||||
Update Username value ->
|
Update Username value ->
|
||||||
@ -71,13 +71,13 @@ update _ msg model =
|
|||||||
, User.signIn
|
, User.signIn
|
||||||
{ username = model.username
|
{ username = model.username
|
||||||
, password = model.password
|
, password = model.password
|
||||||
, msg = Context.SignIn
|
, msg = Global.SignIn
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
view :
|
view :
|
||||||
Context Flags Route Context.Model
|
Context Flags Route Global.Model
|
||||||
-> Model
|
-> Model
|
||||||
-> Html Msg
|
-> Html Msg
|
||||||
view _ model =
|
view _ model =
|
||||||
@ -129,7 +129,7 @@ viewInput options =
|
|||||||
|
|
||||||
|
|
||||||
subscriptions :
|
subscriptions :
|
||||||
Context Flags Route Context.Model
|
Context Flags Route Global.Model
|
||||||
-> Model
|
-> Model
|
||||||
-> Sub Msg
|
-> Sub Msg
|
||||||
subscriptions _ model =
|
subscriptions _ model =
|
||||||
|
@ -1,60 +1,80 @@
|
|||||||
module Application exposing
|
module Application exposing
|
||||||
( Application
|
( create
|
||||||
, create
|
, Application
|
||||||
|
, Config
|
||||||
|
, Context
|
||||||
|
, init, update
|
||||||
|
, Bundle, bundle
|
||||||
, Messages
|
, Messages
|
||||||
)
|
)
|
||||||
|
|
||||||
{-| A package for building single page apps with Elm!
|
{-|
|
||||||
|
|
||||||
|
|
||||||
# Application
|
|
||||||
|
|
||||||
@docs Application
|
|
||||||
|
|
||||||
|
|
||||||
# Creating applications
|
|
||||||
|
|
||||||
@docs create
|
@docs create
|
||||||
|
|
||||||
|
@docs Application
|
||||||
|
|
||||||
# Navigating all smooth-like
|
@docs Config
|
||||||
|
|
||||||
|
@docs Context
|
||||||
|
|
||||||
|
@docs init, update
|
||||||
|
|
||||||
|
@docs Bundle, bundle
|
||||||
|
|
||||||
@docs Messages
|
@docs Messages
|
||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
import Application.Page exposing (Context)
|
|
||||||
import Browser
|
import Browser
|
||||||
import Browser.Navigation as Nav
|
import Browser.Navigation as Nav
|
||||||
import Html exposing (Html, div)
|
import Html exposing (Html, div)
|
||||||
import Html.Attributes as Attr
|
import Html.Attributes as Attr
|
||||||
|
import Internals.Context as Context exposing (Context)
|
||||||
|
import Internals.Page as Page exposing (Page)
|
||||||
import Process
|
import Process
|
||||||
import Task
|
import Task
|
||||||
import Url exposing (Url)
|
import Url exposing (Url)
|
||||||
|
|
||||||
|
|
||||||
{-| A type that's provided for type annotations!
|
{-| A type that's provided for type annotations!
|
||||||
|
|
||||||
Instead of `Program Flags Model Msg`, you can use this type to annotate your main method:
|
|
||||||
|
|
||||||
main : Application Flags Context.Model Context.Msg App.Model App.Msg
|
|
||||||
main =
|
|
||||||
Application.create { ... }
|
|
||||||
|
|
||||||
-}
|
-}
|
||||||
type alias Application flags contextModel contextMsg model msg =
|
type alias Application flags contextModel contextMsg model msg =
|
||||||
Program flags (Model flags contextModel model) (Msg contextMsg msg)
|
Program flags (Model flags contextModel model) (Msg contextMsg msg)
|
||||||
|
|
||||||
|
|
||||||
|
{-| The way to create an `Html` single page application!
|
||||||
|
-}
|
||||||
|
create :
|
||||||
|
Config flags route contextModel contextMsg model msg
|
||||||
|
-> Application flags contextModel contextMsg model msg
|
||||||
|
create config =
|
||||||
|
Browser.application
|
||||||
|
{ init = initWithConfig config
|
||||||
|
, update = updateWithConfig config
|
||||||
|
, view = viewWithConfig config
|
||||||
|
, subscriptions = subscriptionsWithConfig config
|
||||||
|
, onUrlChange = UrlChanged
|
||||||
|
, onUrlRequest = UrlRequested
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias LayoutContext route flags msg =
|
||||||
|
{ messages : Messages route msg
|
||||||
|
, route : route
|
||||||
|
, flags : flags
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Provide some high-level information for your application.
|
||||||
|
-}
|
||||||
type alias Config flags route contextModel contextMsg model msg =
|
type alias Config flags route contextModel contextMsg model msg =
|
||||||
{ context :
|
{ layout :
|
||||||
{ init :
|
{ init :
|
||||||
route
|
LayoutContext route flags (Msg contextMsg msg)
|
||||||
-> flags
|
-> ( contextModel, Cmd contextMsg, Cmd (Msg contextMsg msg) )
|
||||||
-> ( contextModel, Cmd contextMsg )
|
|
||||||
, update :
|
, update :
|
||||||
Messages route (Msg contextMsg msg)
|
LayoutContext route flags (Msg contextMsg msg)
|
||||||
-> route
|
|
||||||
-> contextMsg
|
-> contextMsg
|
||||||
-> contextModel
|
-> contextModel
|
||||||
-> ( contextModel, Cmd contextMsg, Cmd (Msg contextMsg msg) )
|
-> ( contextModel, Cmd contextMsg, Cmd (Msg contextMsg msg) )
|
||||||
@ -64,13 +84,13 @@ type alias Config flags route contextModel contextMsg model msg =
|
|||||||
-> Sub contextMsg
|
-> Sub contextMsg
|
||||||
, view :
|
, view :
|
||||||
{ route : route
|
{ route : route
|
||||||
, context : contextModel
|
|
||||||
, toMsg : contextMsg -> Msg contextMsg msg
|
, toMsg : contextMsg -> Msg contextMsg msg
|
||||||
, viewPage : Html (Msg contextMsg msg)
|
, viewPage : Html (Msg contextMsg msg)
|
||||||
}
|
}
|
||||||
|
-> contextModel
|
||||||
-> Html (Msg contextMsg msg)
|
-> Html (Msg contextMsg msg)
|
||||||
}
|
}
|
||||||
, page :
|
, pages :
|
||||||
{ init :
|
{ init :
|
||||||
Context flags route contextModel
|
Context flags route contextModel
|
||||||
-> ( model, Cmd msg, Cmd contextMsg )
|
-> ( model, Cmd msg, Cmd contextMsg )
|
||||||
@ -82,56 +102,24 @@ type alias Config flags route contextModel contextMsg model msg =
|
|||||||
, bundle :
|
, bundle :
|
||||||
Context flags route contextModel
|
Context flags route contextModel
|
||||||
-> model
|
-> model
|
||||||
-> Application.Page.Bundle msg
|
-> Bundle msg
|
||||||
}
|
}
|
||||||
, route :
|
, routing :
|
||||||
{ fromUrl : Url -> route
|
{ transition : Float
|
||||||
|
, fromUrl : Url -> route
|
||||||
, toPath : route -> String
|
, toPath : route -> String
|
||||||
}
|
}
|
||||||
, transition : Float
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
{-| The way to create an `Application`!
|
{-| The nformation about the route, flags, or global app state.
|
||||||
|
|
||||||
Provide this function with a configuration, and it will bundle things up for you.
|
|
||||||
|
|
||||||
Here's an example (from the `examples/basic` folder of this repo):
|
|
||||||
|
|
||||||
main : Application Flags Context.Model Context.Msg App.Model App.Msg
|
|
||||||
main =
|
|
||||||
Application.create
|
|
||||||
{ transition = 200
|
|
||||||
, context =
|
|
||||||
{ init = Context.init
|
|
||||||
, update = Context.update
|
|
||||||
, view = Context.view
|
|
||||||
, subscriptions = Context.subscriptions
|
|
||||||
}
|
|
||||||
, page =
|
|
||||||
{ init = App.init
|
|
||||||
, update = App.update
|
|
||||||
, bundle = App.view
|
|
||||||
}
|
|
||||||
, route =
|
|
||||||
{ fromUrl = Route.fromUrl
|
|
||||||
, toPath = Route.toPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
-}
|
-}
|
||||||
create :
|
type alias Context flags route contextModel =
|
||||||
Config flags route contextModel contextMsg model msg
|
Context.Context flags route contextModel
|
||||||
-> Application flags contextModel contextMsg model msg
|
|
||||||
create config =
|
|
||||||
Browser.application
|
|
||||||
{ init = init config
|
-- ACTUAl STUFF
|
||||||
, update = update config
|
|
||||||
, view = view config
|
|
||||||
, subscriptions = subscriptions config
|
|
||||||
, onUrlChange = UrlChanged
|
|
||||||
, onUrlRequest = UrlRequested
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type alias Model flags contextModel model =
|
type alias Model flags contextModel model =
|
||||||
@ -193,27 +181,43 @@ type Msg contextMsg msg
|
|||||||
| PageMsg msg
|
| PageMsg msg
|
||||||
|
|
||||||
|
|
||||||
|
{-| These are the messages that your top-level can send to the application.
|
||||||
|
|
||||||
|
For now, the only message is `navigateTo`, which navigates to the provided route!
|
||||||
|
|
||||||
|
case msg of
|
||||||
|
SignIn (Ok user) ->
|
||||||
|
( { model | user = Just user }
|
||||||
|
, Cmd.none
|
||||||
|
, navigateTo Route.Homepage
|
||||||
|
)
|
||||||
|
|
||||||
|
-}
|
||||||
type alias Messages route msg =
|
type alias Messages route msg =
|
||||||
{ navigateTo : route -> Cmd msg
|
{ navigateTo : route -> Cmd msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
init :
|
initWithConfig :
|
||||||
Config flags route contextModel contextMsg model msg
|
Config flags route contextModel contextMsg model msg
|
||||||
-> flags
|
-> flags
|
||||||
-> Url
|
-> Url
|
||||||
-> Nav.Key
|
-> Nav.Key
|
||||||
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
|
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
|
||||||
init config flags url key =
|
initWithConfig config flags url key =
|
||||||
let
|
let
|
||||||
route =
|
route =
|
||||||
config.route.fromUrl url
|
config.routing.fromUrl url
|
||||||
|
|
||||||
( contextModel, contextCmd ) =
|
( contextModel, contextCmd, globalCmd ) =
|
||||||
config.context.init route flags
|
config.layout.init
|
||||||
|
{ messages = { navigateTo = navigateTo config url }
|
||||||
|
, route = route
|
||||||
|
, flags = flags
|
||||||
|
}
|
||||||
|
|
||||||
( pageModel, pageCmd, pageContextCmd ) =
|
( pageModel, pageCmd, pageContextCmd ) =
|
||||||
config.page.init
|
config.pages.init
|
||||||
{ route = route
|
{ route = route
|
||||||
, flags = flags
|
, flags = flags
|
||||||
, context = contextModel
|
, context = contextModel
|
||||||
@ -226,7 +230,8 @@ init config flags url key =
|
|||||||
, page = FirstLoad pageModel
|
, page = FirstLoad pageModel
|
||||||
}
|
}
|
||||||
, Cmd.batch
|
, Cmd.batch
|
||||||
[ delay config.transition (PageLoaded url)
|
[ globalCmd
|
||||||
|
, delay config.routing.transition (PageLoaded url)
|
||||||
, Cmd.map ContextMsg contextCmd
|
, Cmd.map ContextMsg contextCmd
|
||||||
, Cmd.map ContextMsg pageContextCmd
|
, Cmd.map ContextMsg pageContextCmd
|
||||||
, Cmd.map PageMsg pageCmd
|
, Cmd.map PageMsg pageCmd
|
||||||
@ -239,12 +244,12 @@ delay ms msg =
|
|||||||
Task.perform (\_ -> msg) (Process.sleep ms)
|
Task.perform (\_ -> msg) (Process.sleep ms)
|
||||||
|
|
||||||
|
|
||||||
update :
|
updateWithConfig :
|
||||||
Config flags route contextModel contextMsg model msg
|
Config flags route contextModel contextMsg model msg
|
||||||
-> Msg contextMsg msg
|
-> Msg contextMsg msg
|
||||||
-> Model flags contextModel model
|
-> Model flags contextModel model
|
||||||
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
|
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
|
||||||
update config msg model =
|
updateWithConfig config msg model =
|
||||||
case msg of
|
case msg of
|
||||||
UrlRequested urlRequest ->
|
UrlRequested urlRequest ->
|
||||||
case urlRequest of
|
case urlRequest of
|
||||||
@ -260,14 +265,14 @@ update config msg model =
|
|||||||
|
|
||||||
UrlChanged url ->
|
UrlChanged url ->
|
||||||
( { model | page = Loading (unwrap model.page) }
|
( { model | page = Loading (unwrap model.page) }
|
||||||
, delay config.transition (PageLoaded url)
|
, delay config.routing.transition (PageLoaded url)
|
||||||
)
|
)
|
||||||
|
|
||||||
PageLoaded url ->
|
PageLoaded url ->
|
||||||
let
|
let
|
||||||
( pageModel, pageCmd, contextCmd ) =
|
( pageModel, pageCmd, contextCmd ) =
|
||||||
config.page.init
|
config.pages.init
|
||||||
{ route = config.route.fromUrl url
|
{ route = config.routing.fromUrl url
|
||||||
, flags = model.flags
|
, flags = model.flags
|
||||||
, context = model.context
|
, context = model.context
|
||||||
}
|
}
|
||||||
@ -282,10 +287,11 @@ update config msg model =
|
|||||||
ContextMsg msg_ ->
|
ContextMsg msg_ ->
|
||||||
let
|
let
|
||||||
( contextModel, contextCmd, globalCmd ) =
|
( contextModel, contextCmd, globalCmd ) =
|
||||||
config.context.update
|
config.layout.update
|
||||||
{ navigateTo = navigateTo config model.url
|
{ messages = { navigateTo = navigateTo config model.url }
|
||||||
|
, route = config.routing.fromUrl model.url
|
||||||
|
, flags = model.flags
|
||||||
}
|
}
|
||||||
(config.route.fromUrl model.url)
|
|
||||||
msg_
|
msg_
|
||||||
model.context
|
model.context
|
||||||
in
|
in
|
||||||
@ -299,8 +305,8 @@ update config msg model =
|
|||||||
PageMsg msg_ ->
|
PageMsg msg_ ->
|
||||||
let
|
let
|
||||||
( pageModel, pageCmd, contextCmd ) =
|
( pageModel, pageCmd, contextCmd ) =
|
||||||
config.page.update
|
config.pages.update
|
||||||
{ route = config.route.fromUrl model.url
|
{ route = config.routing.fromUrl model.url
|
||||||
, flags = model.flags
|
, flags = model.flags
|
||||||
, context = model.context
|
, context = model.context
|
||||||
}
|
}
|
||||||
@ -321,11 +327,11 @@ type alias Document msg =
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
view :
|
viewWithConfig :
|
||||||
Config flags route contextModel contextMsg model msg
|
Config flags route contextModel contextMsg model msg
|
||||||
-> Model flags contextModel model
|
-> Model flags contextModel model
|
||||||
-> Document (Msg contextMsg msg)
|
-> Document (Msg contextMsg msg)
|
||||||
view config model =
|
viewWithConfig config model =
|
||||||
let
|
let
|
||||||
transitionProp : Float -> String
|
transitionProp : Float -> String
|
||||||
transitionProp ms =
|
transitionProp ms =
|
||||||
@ -358,43 +364,43 @@ view config model =
|
|||||||
( context, pageModel ) =
|
( context, pageModel ) =
|
||||||
contextAndPage ( config, model )
|
contextAndPage ( config, model )
|
||||||
in
|
in
|
||||||
{ title = config.page.bundle context pageModel |> .title
|
{ title = config.pages.bundle context pageModel |> .title
|
||||||
, body =
|
, body =
|
||||||
[ div
|
[ div
|
||||||
[ Attr.class "app"
|
[ Attr.class "app"
|
||||||
, Attr.style "transition" (transitionProp config.transition)
|
, Attr.style "transition" (transitionProp config.routing.transition)
|
||||||
, Attr.style "opacity" (layoutOpacity model.page)
|
, Attr.style "opacity" (layoutOpacity model.page)
|
||||||
]
|
]
|
||||||
[ config.context.view
|
[ config.layout.view
|
||||||
{ route = config.route.fromUrl model.url
|
{ route = config.routing.fromUrl model.url
|
||||||
, toMsg = ContextMsg
|
, toMsg = ContextMsg
|
||||||
, context = model.context
|
|
||||||
, viewPage =
|
, viewPage =
|
||||||
div
|
div
|
||||||
[ Attr.style "transition" (transitionProp config.transition)
|
[ Attr.style "transition" (transitionProp config.routing.transition)
|
||||||
, Attr.style "opacity" (pageOpacity model.page)
|
, Attr.style "opacity" (pageOpacity model.page)
|
||||||
]
|
]
|
||||||
[ Html.map PageMsg
|
[ Html.map PageMsg
|
||||||
(config.page.bundle context pageModel |> .view)
|
(config.pages.bundle context pageModel |> .view)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
model.context
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
subscriptions :
|
subscriptionsWithConfig :
|
||||||
Config flags route contextModel contextMsg model msg
|
Config flags route contextModel contextMsg model msg
|
||||||
-> Model flags contextModel model
|
-> Model flags contextModel model
|
||||||
-> Sub (Msg contextMsg msg)
|
-> Sub (Msg contextMsg msg)
|
||||||
subscriptions config model =
|
subscriptionsWithConfig config model =
|
||||||
let
|
let
|
||||||
( context, pageModel ) =
|
( context, pageModel ) =
|
||||||
contextAndPage ( config, model )
|
contextAndPage ( config, model )
|
||||||
in
|
in
|
||||||
Sub.batch
|
Sub.batch
|
||||||
[ Sub.map ContextMsg (config.context.subscriptions (config.route.fromUrl model.url) model.context)
|
[ Sub.map ContextMsg (config.layout.subscriptions (config.routing.fromUrl model.url) model.context)
|
||||||
, Sub.map PageMsg (config.page.bundle context pageModel |> .subscriptions)
|
, Sub.map PageMsg (config.pages.bundle context pageModel |> .subscriptions)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -404,9 +410,9 @@ subscriptions config model =
|
|||||||
|
|
||||||
contextAndPage :
|
contextAndPage :
|
||||||
( Config flags route contextModel contextMsg model msg, Model flags contextModel model )
|
( Config flags route contextModel contextMsg model msg, Model flags contextModel model )
|
||||||
-> ( Application.Page.Context flags route contextModel, model )
|
-> ( Context flags route contextModel, model )
|
||||||
contextAndPage ( config, model ) =
|
contextAndPage ( config, model ) =
|
||||||
( { route = config.route.fromUrl model.url
|
( { route = config.routing.fromUrl model.url
|
||||||
, flags = model.flags
|
, flags = model.flags
|
||||||
, context = model.context
|
, context = model.context
|
||||||
}
|
}
|
||||||
@ -420,7 +426,131 @@ navigateTo :
|
|||||||
-> route
|
-> route
|
||||||
-> Cmd (Msg contextMsg msg)
|
-> Cmd (Msg contextMsg msg)
|
||||||
navigateTo config url route =
|
navigateTo config url route =
|
||||||
Task.succeed (config.route.toPath route)
|
Task.succeed (config.routing.toPath route)
|
||||||
|> Task.map (\path -> { url | path = path })
|
|> Task.map (\path -> { url | path = path })
|
||||||
|> Task.map Browser.Internal
|
|> Task.map Browser.Internal
|
||||||
|> Task.perform UrlRequested
|
|> Task.perform UrlRequested
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- HELPERS
|
||||||
|
|
||||||
|
|
||||||
|
{-| Used to help wire up the top-level `init` function.
|
||||||
|
|
||||||
|
-- ...
|
||||||
|
case context.route of
|
||||||
|
Route.Homepage ->
|
||||||
|
Application.init
|
||||||
|
{ page = pages.homepage
|
||||||
|
, context = context
|
||||||
|
}
|
||||||
|
-- ...
|
||||||
|
|
||||||
|
-}
|
||||||
|
init :
|
||||||
|
{ page : Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
, context : Context flags route contextModel
|
||||||
|
}
|
||||||
|
-> ( appModel, Cmd appMsg, Cmd contextMsg )
|
||||||
|
init config =
|
||||||
|
Page.init config.page config.context
|
||||||
|
|> mapTruple
|
||||||
|
{ fromMsg = Page.toMsg config.page
|
||||||
|
, fromModel = Page.toModel config.page
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Used to help wire up the top-level `update` function.
|
||||||
|
|
||||||
|
-- ...
|
||||||
|
case ( appModel, appMsg ) of
|
||||||
|
( HomepageModel model, HomepageMsg msg ) ->
|
||||||
|
Application.update
|
||||||
|
{ page = pages.homepage
|
||||||
|
, msg = msg
|
||||||
|
, model = model
|
||||||
|
, context = context
|
||||||
|
}
|
||||||
|
-- ...
|
||||||
|
|
||||||
|
-}
|
||||||
|
update :
|
||||||
|
{ page : Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
, msg : msg
|
||||||
|
, model : model
|
||||||
|
, context : Context flags route contextModel
|
||||||
|
}
|
||||||
|
-> ( appModel, Cmd appMsg, Cmd contextMsg )
|
||||||
|
update config =
|
||||||
|
Page.update config.page config.context config.msg config.model
|
||||||
|
|> mapTruple
|
||||||
|
{ fromMsg = Page.toMsg config.page
|
||||||
|
, fromModel = Page.toModel config.page
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| A bundle of `view`, `subscriptions`, and `title`, to eliminate the need for three separate functions for each at the top-level.
|
||||||
|
-}
|
||||||
|
type alias Bundle appMsg =
|
||||||
|
{ title : String
|
||||||
|
, view : Html appMsg
|
||||||
|
, subscriptions : Sub appMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Used to help wire up the top-level `bundle` function.
|
||||||
|
|
||||||
|
-- ...
|
||||||
|
case appModel of
|
||||||
|
HomepageModel model ->
|
||||||
|
Application.bundle
|
||||||
|
{ page = pages.homepage
|
||||||
|
, model = model
|
||||||
|
, context = context
|
||||||
|
}
|
||||||
|
-- ...
|
||||||
|
|
||||||
|
-}
|
||||||
|
bundle :
|
||||||
|
{ page : Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
, model : model
|
||||||
|
, context : Context flags route contextModel
|
||||||
|
}
|
||||||
|
-> Bundle appMsg
|
||||||
|
bundle config =
|
||||||
|
{ title =
|
||||||
|
Page.title
|
||||||
|
config.page
|
||||||
|
config.context
|
||||||
|
config.model
|
||||||
|
, view =
|
||||||
|
Html.map (Page.toMsg config.page) <|
|
||||||
|
Page.view
|
||||||
|
config.page
|
||||||
|
config.context
|
||||||
|
config.model
|
||||||
|
, subscriptions =
|
||||||
|
Sub.map (Page.toMsg config.page) <|
|
||||||
|
Page.subscriptions
|
||||||
|
config.page
|
||||||
|
config.context
|
||||||
|
config.model
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- UTILS
|
||||||
|
|
||||||
|
|
||||||
|
mapTruple :
|
||||||
|
{ fromMsg : msg -> appMsg
|
||||||
|
, fromModel : model -> appModel
|
||||||
|
}
|
||||||
|
-> ( model, Cmd msg, Cmd contextMsg )
|
||||||
|
-> ( appModel, Cmd appMsg, Cmd contextMsg )
|
||||||
|
mapTruple { fromModel, fromMsg } ( a, b, c ) =
|
||||||
|
( fromModel a
|
||||||
|
, Cmd.map fromMsg b
|
||||||
|
, c
|
||||||
|
)
|
||||||
|
@ -1,143 +1,114 @@
|
|||||||
module Application.Page exposing
|
module Application.Page exposing
|
||||||
( static, sandbox, element, page
|
( static
|
||||||
, init, update, bundle
|
, sandbox
|
||||||
, Context
|
, element
|
||||||
, Bundle
|
, page
|
||||||
)
|
)
|
||||||
|
|
||||||
{-| A package for building single page apps with Elm!
|
{-| The `Page` type builds simple or complex pages,
|
||||||
|
|
||||||
|
based on your use case. The naming conventions are inspired by the
|
||||||
|
[`elm/browser`](#) package.
|
||||||
|
|
||||||
|
|
||||||
|
## Warning: The types here look spooky! 👻
|
||||||
|
|
||||||
|
But they they're much less spooky in practice. You got this!
|
||||||
|
|
||||||
|
For the following examples, lets imagine this is our top level `Model` and
|
||||||
|
`Msg`
|
||||||
|
|
||||||
|
type Model
|
||||||
|
= HomepageModel ()
|
||||||
|
| CounterModel Pages.Counter.Model
|
||||||
|
| RandomModel Pages.Random.Model
|
||||||
|
| SignInModel Pages.SignIn.Model
|
||||||
|
| NotFoundModel ()
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= HomepageMsg Never
|
||||||
|
| CounterMsg Pages.Counter.Msg
|
||||||
|
| RandomMsg Pages.Random.Msg
|
||||||
|
| SignInMsg Pages.SignIn.Msg
|
||||||
|
| NotFoundMsg Never
|
||||||
|
|
||||||
|
**Note:** static pages use `()` and `Never` for their `Model` and `Msg` because
|
||||||
|
they have no model ( the `()` part ) and can't send messages ( the `Never` part ).
|
||||||
|
|
||||||
|
Having them accept arguments helps make the rest of the code more consistent 😎
|
||||||
|
|
||||||
|
|
||||||
|
# Static
|
||||||
|
|
||||||
|
A static page that doesn't need to send messages or make updates to the app.
|
||||||
|
|
||||||
|
@docs static
|
||||||
|
|
||||||
|
|
||||||
|
# Sandbox
|
||||||
|
|
||||||
|
A sandbox page that can make messages, but doesn't need to produce any side effects.
|
||||||
|
|
||||||
|
@docs sandbox
|
||||||
|
|
||||||
|
|
||||||
|
# Element
|
||||||
|
|
||||||
|
An element page that makes messages that might produce side effects.
|
||||||
|
|
||||||
|
@docs element
|
||||||
|
|
||||||
|
|
||||||
# Page
|
# Page
|
||||||
|
|
||||||
These functions convert your pages into one consistent `Page` type.
|
An complete page that needs access to the shared application state (context) and might produce side effects for the page _or_ the application.
|
||||||
|
|
||||||
This makes writing top-level functions like `init`, `update`, `view`, and `subscriptions` easy, without making pages themselves unnecessarily complex.
|
@docs page
|
||||||
|
|
||||||
You can check out [a full example here](https://github.com/ryannhg/elm-app/tree/master/examples/basic) to understand how these functions are used.
|
|
||||||
|
|
||||||
@docs static, sandbox, element, page
|
|
||||||
|
|
||||||
|
|
||||||
# Helpers
|
|
||||||
|
|
||||||
@docs init, update, bundle
|
|
||||||
|
|
||||||
|
|
||||||
# Related types
|
|
||||||
|
|
||||||
@docs Context
|
|
||||||
|
|
||||||
@docs Bundle
|
|
||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
import Browser
|
|
||||||
import Html exposing (Html)
|
import Html exposing (Html)
|
||||||
|
import Internals.Context exposing (Context)
|
||||||
|
import Internals.Page as Internals
|
||||||
type alias Context flags route contextModel =
|
|
||||||
{ flags : flags
|
|
||||||
, route : route
|
|
||||||
, context : contextModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type alias Page route flags contextModel contextMsg model msg appModel appMsg =
|
type alias Page route flags contextModel contextMsg model msg appModel appMsg =
|
||||||
{ title : Context flags route contextModel -> model -> String
|
Internals.Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
, init : Context flags route contextModel -> ( model, Cmd msg, Cmd contextMsg )
|
|
||||||
, update : Context flags route contextModel -> msg -> model -> ( model, Cmd msg, Cmd contextMsg )
|
|
||||||
, subscriptions : Context flags route contextModel -> model -> Sub msg
|
|
||||||
, view : Context flags route contextModel -> model -> Html msg
|
|
||||||
, toMsg : msg -> appMsg
|
|
||||||
, toModel : model -> appModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
-- PAGE HELPERS
|
homepage =
|
||||||
|
Page.static
|
||||||
|
{ title = Pages.Homepage.title
|
||||||
init :
|
, view = Pages.Homepage.view
|
||||||
{ page : Page route flags contextModel contextMsg model msg appModel appMsg
|
, toModel = HomepageModel
|
||||||
, context : Context flags route contextModel
|
|
||||||
}
|
|
||||||
-> ( appModel, Cmd appMsg, Cmd contextMsg )
|
|
||||||
init config =
|
|
||||||
config.page.init config.context
|
|
||||||
|> mapTruple
|
|
||||||
{ fromMsg = config.page.toMsg
|
|
||||||
, fromModel = config.page.toModel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-}
|
||||||
update :
|
|
||||||
{ page : Page route flags contextModel contextMsg model msg appModel appMsg
|
|
||||||
, msg : msg
|
|
||||||
, model : model
|
|
||||||
, context : Context flags route contextModel
|
|
||||||
}
|
|
||||||
-> ( appModel, Cmd appMsg, Cmd contextMsg )
|
|
||||||
update config =
|
|
||||||
config.page.update config.context config.msg config.model
|
|
||||||
|> mapTruple
|
|
||||||
{ fromMsg = config.page.toMsg
|
|
||||||
, fromModel = config.page.toModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
type alias Bundle appMsg =
|
|
||||||
{ title : String
|
|
||||||
, view : Html appMsg
|
|
||||||
, subscriptions : Sub appMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
bundle :
|
|
||||||
{ page : Page route flags contextModel contextMsg model msg appModel appMsg
|
|
||||||
, model : model
|
|
||||||
, context : Context flags route contextModel
|
|
||||||
}
|
|
||||||
-> Bundle appMsg
|
|
||||||
bundle config =
|
|
||||||
{ title =
|
|
||||||
config.page.title
|
|
||||||
config.context
|
|
||||||
config.model
|
|
||||||
, view =
|
|
||||||
Html.map config.page.toMsg <|
|
|
||||||
config.page.view
|
|
||||||
config.context
|
|
||||||
config.model
|
|
||||||
, subscriptions =
|
|
||||||
Sub.map config.page.toMsg <|
|
|
||||||
config.page.subscriptions
|
|
||||||
config.context
|
|
||||||
config.model
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- PAGE ADAPTERS
|
|
||||||
|
|
||||||
|
|
||||||
static :
|
static :
|
||||||
{ title : String
|
{ title : String
|
||||||
, view : Html Never
|
, view : Html Never
|
||||||
, toModel : () -> appModel
|
, toModel : () -> appModel
|
||||||
}
|
}
|
||||||
-> Page route flags contextModel contextMsg () Never appModel appMsg
|
-> Page route flags contextModel contextMsg () Never appModel appMsg
|
||||||
static config =
|
static =
|
||||||
{ title = \c m -> config.title
|
Internals.static
|
||||||
, init = \c -> ( (), Cmd.none, Cmd.none )
|
|
||||||
, update = \c m model -> ( model, Cmd.none, Cmd.none )
|
|
||||||
, subscriptions = \c m -> Sub.none
|
|
||||||
, view = \c m -> Html.map never config.view
|
|
||||||
, toMsg = never
|
|
||||||
, toModel = config.toModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
counter =
|
||||||
|
Page.sandbox
|
||||||
|
{ title = Pages.Counter.title
|
||||||
|
, init = Pages.Counter.init
|
||||||
|
, update = Pages.Counter.update
|
||||||
|
, view = Pages.Counter.view
|
||||||
|
, toModel = CounterModel
|
||||||
|
, toMsg = CounterMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
-}
|
||||||
sandbox :
|
sandbox :
|
||||||
{ title : model -> String
|
{ title : model -> String
|
||||||
, init : model
|
, init : model
|
||||||
@ -147,17 +118,24 @@ sandbox :
|
|||||||
, toModel : model -> appModel
|
, toModel : model -> appModel
|
||||||
}
|
}
|
||||||
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
sandbox config =
|
sandbox =
|
||||||
{ title = \c model -> config.title model
|
Internals.sandbox
|
||||||
, init = \c -> ( config.init, Cmd.none, Cmd.none )
|
|
||||||
, update = \c msg model -> ( config.update msg model, Cmd.none, Cmd.none )
|
|
||||||
, subscriptions = \c m -> Sub.none
|
|
||||||
, view = \c model -> config.view model
|
|
||||||
, toMsg = config.toMsg
|
|
||||||
, toModel = config.toModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
random =
|
||||||
|
Page.element
|
||||||
|
{ title = Pages.Random.title
|
||||||
|
, init = Pages.Random.init
|
||||||
|
, update = Pages.Random.update
|
||||||
|
, subscriptions = Pages.Random.subscriptions
|
||||||
|
, view = Pages.Random.view
|
||||||
|
, toModel = RandomModel
|
||||||
|
, toMsg = RandomMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
-}
|
||||||
element :
|
element :
|
||||||
{ title : model -> String
|
{ title : model -> String
|
||||||
, init : flags -> ( model, Cmd msg )
|
, init : flags -> ( model, Cmd msg )
|
||||||
@ -168,21 +146,24 @@ element :
|
|||||||
, toModel : model -> appModel
|
, toModel : model -> appModel
|
||||||
}
|
}
|
||||||
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
element config =
|
element =
|
||||||
let
|
Internals.element
|
||||||
appendCmd ( model, cmd ) =
|
|
||||||
( model, cmd, Cmd.none )
|
|
||||||
in
|
|
||||||
{ title = \c model -> config.title model
|
|
||||||
, init = \c -> config.init c.flags |> appendCmd
|
|
||||||
, update = \c msg model -> config.update msg model |> appendCmd
|
|
||||||
, subscriptions = \c model -> config.subscriptions model
|
|
||||||
, view = \c model -> config.view model
|
|
||||||
, toMsg = config.toMsg
|
|
||||||
, toModel = config.toModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
signIn =
|
||||||
|
Page.page
|
||||||
|
{ title = Pages.SignIn.title
|
||||||
|
, init = Pages.SignIn.init
|
||||||
|
, update = Pages.SignIn.update
|
||||||
|
, subscriptions = Pages.SignIn.subscriptions
|
||||||
|
, view = Pages.SignIn.view
|
||||||
|
, toModel = SignInModel
|
||||||
|
, toMsg = SignInMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
-}
|
||||||
page :
|
page :
|
||||||
{ title : Context flags route contextModel -> model -> String
|
{ title : Context flags route contextModel -> model -> String
|
||||||
, init : Context flags route contextModel -> ( model, Cmd msg, Cmd contextMsg )
|
, init : Context flags route contextModel -> ( model, Cmd msg, Cmd contextMsg )
|
||||||
@ -193,33 +174,5 @@ page :
|
|||||||
, toModel : model -> appModel
|
, toModel : model -> appModel
|
||||||
}
|
}
|
||||||
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
page config =
|
page =
|
||||||
let
|
Internals.page
|
||||||
appendCmd ( model, cmd ) =
|
|
||||||
( model, cmd, Cmd.none )
|
|
||||||
in
|
|
||||||
{ title = config.title
|
|
||||||
, init = config.init
|
|
||||||
, update = config.update
|
|
||||||
, subscriptions = \c model -> config.subscriptions c model
|
|
||||||
, view = \c model -> config.view c model
|
|
||||||
, toMsg = config.toMsg
|
|
||||||
, toModel = config.toModel
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
-- UTILS
|
|
||||||
|
|
||||||
|
|
||||||
mapTruple :
|
|
||||||
{ fromMsg : msg -> appMsg
|
|
||||||
, fromModel : model -> appModel
|
|
||||||
}
|
|
||||||
-> ( model, Cmd msg, Cmd contextMsg )
|
|
||||||
-> ( appModel, Cmd appMsg, Cmd contextMsg )
|
|
||||||
mapTruple { fromModel, fromMsg } ( a, b, c ) =
|
|
||||||
( fromModel a
|
|
||||||
, Cmd.map fromMsg b
|
|
||||||
, c
|
|
||||||
)
|
|
||||||
|
8
src/Internals/Context.elm
Normal file
8
src/Internals/Context.elm
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module Internals.Context exposing (Context)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Context flags route contextModel =
|
||||||
|
{ flags : flags
|
||||||
|
, route : route
|
||||||
|
, context : contextModel
|
||||||
|
}
|
192
src/Internals/Page.elm
Normal file
192
src/Internals/Page.elm
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
module Internals.Page exposing
|
||||||
|
( Page
|
||||||
|
, element
|
||||||
|
, init
|
||||||
|
, page
|
||||||
|
, sandbox
|
||||||
|
, static
|
||||||
|
, subscriptions
|
||||||
|
, title
|
||||||
|
, toModel
|
||||||
|
, toMsg
|
||||||
|
, update
|
||||||
|
, view
|
||||||
|
)
|
||||||
|
|
||||||
|
import Html exposing (Html)
|
||||||
|
import Internals.Context exposing (Context)
|
||||||
|
|
||||||
|
|
||||||
|
type Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
= Page (Page_ route flags contextModel contextMsg model msg appModel appMsg)
|
||||||
|
|
||||||
|
|
||||||
|
type alias Page_ route flags contextModel contextMsg model msg appModel appMsg =
|
||||||
|
{ title : Context flags route contextModel -> model -> String
|
||||||
|
, init : Context flags route contextModel -> ( model, Cmd msg, Cmd contextMsg )
|
||||||
|
, update : Context flags route contextModel -> msg -> model -> ( model, Cmd msg, Cmd contextMsg )
|
||||||
|
, subscriptions : Context flags route contextModel -> model -> Sub msg
|
||||||
|
, view : Context flags route contextModel -> model -> Html msg
|
||||||
|
, toMsg : msg -> appMsg
|
||||||
|
, toModel : model -> appModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- CONSTRUCTORS
|
||||||
|
|
||||||
|
|
||||||
|
static :
|
||||||
|
{ title : String
|
||||||
|
, view : Html Never
|
||||||
|
, toModel : () -> appModel
|
||||||
|
}
|
||||||
|
-> Page route flags contextModel contextMsg () Never appModel appMsg
|
||||||
|
static config =
|
||||||
|
Page
|
||||||
|
{ title = \c m -> config.title
|
||||||
|
, init = \c -> ( (), Cmd.none, Cmd.none )
|
||||||
|
, update = \c m model -> ( model, Cmd.none, Cmd.none )
|
||||||
|
, subscriptions = \c m -> Sub.none
|
||||||
|
, view = \c m -> Html.map never config.view
|
||||||
|
, toMsg = never
|
||||||
|
, toModel = config.toModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sandbox :
|
||||||
|
{ title : model -> String
|
||||||
|
, init : model
|
||||||
|
, update : msg -> model -> model
|
||||||
|
, view : model -> Html msg
|
||||||
|
, toMsg : msg -> appMsg
|
||||||
|
, toModel : model -> appModel
|
||||||
|
}
|
||||||
|
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
sandbox config =
|
||||||
|
Page
|
||||||
|
{ title = \c model -> config.title model
|
||||||
|
, init = \c -> ( config.init, Cmd.none, Cmd.none )
|
||||||
|
, update = \c msg model -> ( config.update msg model, Cmd.none, Cmd.none )
|
||||||
|
, subscriptions = \c m -> Sub.none
|
||||||
|
, view = \c model -> config.view model
|
||||||
|
, toMsg = config.toMsg
|
||||||
|
, toModel = config.toModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
element :
|
||||||
|
{ title : model -> String
|
||||||
|
, init : flags -> ( model, Cmd msg )
|
||||||
|
, update : msg -> model -> ( model, Cmd msg )
|
||||||
|
, subscriptions : model -> Sub msg
|
||||||
|
, view : model -> Html msg
|
||||||
|
, toMsg : msg -> appMsg
|
||||||
|
, toModel : model -> appModel
|
||||||
|
}
|
||||||
|
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
element config =
|
||||||
|
let
|
||||||
|
appendCmd ( model, cmd ) =
|
||||||
|
( model, cmd, Cmd.none )
|
||||||
|
in
|
||||||
|
Page
|
||||||
|
{ title = \c model -> config.title model
|
||||||
|
, init = \c -> config.init c.flags |> appendCmd
|
||||||
|
, update = \c msg model -> config.update msg model |> appendCmd
|
||||||
|
, subscriptions = \c model -> config.subscriptions model
|
||||||
|
, view = \c model -> config.view model
|
||||||
|
, toMsg = config.toMsg
|
||||||
|
, toModel = config.toModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
page :
|
||||||
|
{ title : Context flags route contextModel -> model -> String
|
||||||
|
, init : Context flags route contextModel -> ( model, Cmd msg, Cmd contextMsg )
|
||||||
|
, update : Context flags route contextModel -> msg -> model -> ( model, Cmd msg, Cmd contextMsg )
|
||||||
|
, subscriptions : Context flags route contextModel -> model -> Sub msg
|
||||||
|
, view : Context flags route contextModel -> model -> Html msg
|
||||||
|
, toMsg : msg -> appMsg
|
||||||
|
, toModel : model -> appModel
|
||||||
|
}
|
||||||
|
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
page config =
|
||||||
|
let
|
||||||
|
appendCmd ( model, cmd ) =
|
||||||
|
( model, cmd, Cmd.none )
|
||||||
|
in
|
||||||
|
Page
|
||||||
|
{ title = config.title
|
||||||
|
, init = config.init
|
||||||
|
, update = config.update
|
||||||
|
, subscriptions = \c model -> config.subscriptions c model
|
||||||
|
, view = \c model -> config.view c model
|
||||||
|
, toMsg = config.toMsg
|
||||||
|
, toModel = config.toModel
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- ACCESSORS
|
||||||
|
|
||||||
|
|
||||||
|
init :
|
||||||
|
Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
-> Context flags route contextModel
|
||||||
|
-> ( model, Cmd msg, Cmd contextMsg )
|
||||||
|
init (Page page_) =
|
||||||
|
page_.init
|
||||||
|
|
||||||
|
|
||||||
|
update :
|
||||||
|
Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
-> Context flags route contextModel
|
||||||
|
-> msg
|
||||||
|
-> model
|
||||||
|
-> ( model, Cmd msg, Cmd contextMsg )
|
||||||
|
update (Page page_) =
|
||||||
|
page_.update
|
||||||
|
|
||||||
|
|
||||||
|
title :
|
||||||
|
Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
-> Context flags route contextModel
|
||||||
|
-> model
|
||||||
|
-> String
|
||||||
|
title (Page page_) =
|
||||||
|
page_.title
|
||||||
|
|
||||||
|
|
||||||
|
view :
|
||||||
|
Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
-> Context flags route contextModel
|
||||||
|
-> model
|
||||||
|
-> Html msg
|
||||||
|
view (Page page_) =
|
||||||
|
page_.view
|
||||||
|
|
||||||
|
|
||||||
|
subscriptions :
|
||||||
|
Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
-> Context flags route contextModel
|
||||||
|
-> model
|
||||||
|
-> Sub msg
|
||||||
|
subscriptions (Page page_) =
|
||||||
|
page_.subscriptions
|
||||||
|
|
||||||
|
|
||||||
|
toMsg :
|
||||||
|
Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
-> msg
|
||||||
|
-> appMsg
|
||||||
|
toMsg (Page page_) =
|
||||||
|
page_.toMsg
|
||||||
|
|
||||||
|
|
||||||
|
toModel :
|
||||||
|
Page route flags contextModel contextMsg model msg appModel appMsg
|
||||||
|
-> model
|
||||||
|
-> appModel
|
||||||
|
toModel (Page page_) =
|
||||||
|
page_.toModel
|
Loading…
Reference in New Issue
Block a user