mirror of
https://github.com/ryannhg/elm-spa.git
synced 2024-11-22 09:44:55 +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
|
||||
> 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!)
|
||||
|
||||
|
||||
## try it out
|
||||
## installing
|
||||
|
||||
```
|
||||
git clone https://github.com/ryannhg/elm-app
|
||||
cd elm-app/examples/basic
|
||||
npm install
|
||||
npm run dev
|
||||
elm install ryannhg/elm-app
|
||||
```
|
||||
|
||||
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
|
||||
> 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)
|
||||
- `App` - the top level `Model`, `Msg`, `init`, `update`, `subscriptions`, and `view`
|
||||
- `Context` - the shared state between pages.
|
||||
- `Route` - the routes for your application
|
||||
- `Flags` - the initial JSON sent into the app
|
||||
This is the __entrypoint__ to the application, and connects all the parts of our `Application` together:
|
||||
|
||||
```elm
|
||||
module Main exposing (main)
|
||||
|
||||
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
|
||||
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 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
|
||||
import Route
|
||||
import Components.Layout as Layout
|
||||
import Pages ✨
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ user : Maybe User
|
||||
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 ✨
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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",
|
||||
"summary": "an experiment for making single page apps with Elm",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"exposed-modules": [
|
||||
"Application",
|
||||
"Application.Page"
|
||||
],
|
||||
"elm-version": "0.19.0 <= v < 0.20.0",
|
||||
"dependencies": {
|
||||
"elm/browser": "1.0.1",
|
||||
"elm/core": "1.0.2",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/url": "1.0.0"
|
||||
"elm/browser": "1.0.0 <= v < 2.0.0",
|
||||
"elm/core": "1.0.0 <= v < 2.0.0",
|
||||
"elm/html": "1.0.0 <= v < 2.0.0",
|
||||
"elm/url": "1.0.0 <= v < 2.0.0"
|
||||
},
|
||||
"test-dependencies": {}
|
||||
}
|
@ -68,7 +68,7 @@ label div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
.layout__page {
|
||||
flex: 1 0 auto;
|
||||
padding: 2rem 0;
|
||||
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)
|
||||
|
||||
import App
|
||||
import Application exposing (Application)
|
||||
import Context
|
||||
import Components.Layout as Layout
|
||||
import Flags exposing (Flags)
|
||||
import Global
|
||||
import Pages
|
||||
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 =
|
||||
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
|
||||
{ routing =
|
||||
{ transition = 200
|
||||
, fromUrl = Route.fromUrl
|
||||
, 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
|
||||
, Msg
|
||||
, bundle
|
||||
@ -6,10 +6,11 @@ module App exposing
|
||||
, update
|
||||
)
|
||||
|
||||
import Application.Page as Page exposing (Context)
|
||||
import Application exposing (Bundle, Context)
|
||||
import Application.Page as Page
|
||||
import Browser
|
||||
import Context
|
||||
import Flags exposing (Flags)
|
||||
import Global
|
||||
import Html exposing (Html)
|
||||
import Pages.Counter
|
||||
import Pages.Homepage
|
||||
@ -81,50 +82,50 @@ pages =
|
||||
|
||||
|
||||
init :
|
||||
Context Flags Route Context.Model
|
||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
||||
Context Flags Route Global.Model
|
||||
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
init context =
|
||||
case context.route of
|
||||
Route.Homepage ->
|
||||
Page.init
|
||||
Application.init
|
||||
{ page = pages.homepage
|
||||
, context = context
|
||||
}
|
||||
|
||||
Route.Counter ->
|
||||
Page.init
|
||||
Application.init
|
||||
{ page = pages.counter
|
||||
, context = context
|
||||
}
|
||||
|
||||
Route.Random ->
|
||||
Page.init
|
||||
Application.init
|
||||
{ page = pages.random
|
||||
, context = context
|
||||
}
|
||||
|
||||
Route.SignIn ->
|
||||
Page.init
|
||||
Application.init
|
||||
{ page = pages.signIn
|
||||
, context = context
|
||||
}
|
||||
|
||||
Route.NotFound ->
|
||||
Page.init
|
||||
Application.init
|
||||
{ page = pages.notFound
|
||||
, context = context
|
||||
}
|
||||
|
||||
|
||||
update :
|
||||
Context Flags Route Context.Model
|
||||
Context Flags Route Global.Model
|
||||
-> Msg
|
||||
-> Model
|
||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
||||
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
update context appMsg appModel =
|
||||
case ( appModel, appMsg ) of
|
||||
( HomepageModel model, HomepageMsg msg ) ->
|
||||
Page.update
|
||||
Application.update
|
||||
{ page = pages.homepage
|
||||
, msg = msg
|
||||
, model = model
|
||||
@ -138,7 +139,7 @@ update context appMsg appModel =
|
||||
)
|
||||
|
||||
( CounterModel model, CounterMsg msg ) ->
|
||||
Page.update
|
||||
Application.update
|
||||
{ page = pages.counter
|
||||
, msg = msg
|
||||
, model = model
|
||||
@ -152,7 +153,7 @@ update context appMsg appModel =
|
||||
)
|
||||
|
||||
( RandomModel model, RandomMsg msg ) ->
|
||||
Page.update
|
||||
Application.update
|
||||
{ page = pages.random
|
||||
, msg = msg
|
||||
, model = model
|
||||
@ -166,7 +167,7 @@ update context appMsg appModel =
|
||||
)
|
||||
|
||||
( SignInModel model, SignInMsg msg ) ->
|
||||
Page.update
|
||||
Application.update
|
||||
{ page = pages.signIn
|
||||
, msg = msg
|
||||
, model = model
|
||||
@ -180,7 +181,7 @@ update context appMsg appModel =
|
||||
)
|
||||
|
||||
( NotFoundModel model, NotFoundMsg msg ) ->
|
||||
Page.update
|
||||
Application.update
|
||||
{ page = pages.notFound
|
||||
, msg = msg
|
||||
, model = model
|
||||
@ -195,41 +196,41 @@ update context appMsg appModel =
|
||||
|
||||
|
||||
bundle :
|
||||
Context Flags Route Context.Model
|
||||
Context Flags Route Global.Model
|
||||
-> Model
|
||||
-> Page.Bundle Msg
|
||||
-> Bundle Msg
|
||||
bundle context appModel =
|
||||
case appModel of
|
||||
HomepageModel model ->
|
||||
Page.bundle
|
||||
Application.bundle
|
||||
{ page = pages.homepage
|
||||
, model = model
|
||||
, context = context
|
||||
}
|
||||
|
||||
CounterModel model ->
|
||||
Page.bundle
|
||||
Application.bundle
|
||||
{ page = pages.counter
|
||||
, model = model
|
||||
, context = context
|
||||
}
|
||||
|
||||
RandomModel model ->
|
||||
Page.bundle
|
||||
Application.bundle
|
||||
{ page = pages.random
|
||||
, model = model
|
||||
, context = context
|
||||
}
|
||||
|
||||
SignInModel model ->
|
||||
Page.bundle
|
||||
Application.bundle
|
||||
{ page = pages.signIn
|
||||
, model = model
|
||||
, context = context
|
||||
}
|
||||
|
||||
NotFoundModel model ->
|
||||
Page.bundle
|
||||
Application.bundle
|
||||
{ page = pages.notFound
|
||||
, model = model
|
||||
, context = context
|
@ -8,10 +8,10 @@ module Pages.SignIn exposing
|
||||
, view
|
||||
)
|
||||
|
||||
import Application.Page exposing (Context)
|
||||
import Context
|
||||
import Application exposing (Context)
|
||||
import Data.User as User exposing (User)
|
||||
import Flags exposing (Flags)
|
||||
import Global
|
||||
import Html exposing (..)
|
||||
import Html.Attributes as Attr
|
||||
import Html.Events as Events
|
||||
@ -35,7 +35,7 @@ type Field
|
||||
| Password
|
||||
|
||||
|
||||
title : Context Flags Route Context.Model -> Model -> String
|
||||
title : Context Flags Route Global.Model -> Model -> String
|
||||
title { context } model =
|
||||
case context.user of
|
||||
Just user ->
|
||||
@ -46,17 +46,17 @@ title { context } model =
|
||||
|
||||
|
||||
init :
|
||||
Context Flags Route Context.Model
|
||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
||||
Context Flags Route Global.Model
|
||||
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
init _ =
|
||||
Utils.Cmd.pure { username = "", password = "" }
|
||||
|
||||
|
||||
update :
|
||||
Context Flags Route Context.Model
|
||||
Context Flags Route Global.Model
|
||||
-> Msg
|
||||
-> Model
|
||||
-> ( Model, Cmd Msg, Cmd Context.Msg )
|
||||
-> ( Model, Cmd Msg, Cmd Global.Msg )
|
||||
update _ msg model =
|
||||
case msg of
|
||||
Update Username value ->
|
||||
@ -71,13 +71,13 @@ update _ msg model =
|
||||
, User.signIn
|
||||
{ username = model.username
|
||||
, password = model.password
|
||||
, msg = Context.SignIn
|
||||
, msg = Global.SignIn
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
view :
|
||||
Context Flags Route Context.Model
|
||||
Context Flags Route Global.Model
|
||||
-> Model
|
||||
-> Html Msg
|
||||
view _ model =
|
||||
@ -129,7 +129,7 @@ viewInput options =
|
||||
|
||||
|
||||
subscriptions :
|
||||
Context Flags Route Context.Model
|
||||
Context Flags Route Global.Model
|
||||
-> Model
|
||||
-> Sub Msg
|
||||
subscriptions _ model =
|
||||
|
@ -1,60 +1,80 @@
|
||||
module Application exposing
|
||||
( Application
|
||||
, create
|
||||
( create
|
||||
, Application
|
||||
, Config
|
||||
, Context
|
||||
, init, update
|
||||
, Bundle, bundle
|
||||
, Messages
|
||||
)
|
||||
|
||||
{-| A package for building single page apps with Elm!
|
||||
|
||||
|
||||
# Application
|
||||
|
||||
@docs Application
|
||||
|
||||
|
||||
# Creating applications
|
||||
{-|
|
||||
|
||||
@docs create
|
||||
|
||||
@docs Application
|
||||
|
||||
# Navigating all smooth-like
|
||||
@docs Config
|
||||
|
||||
@docs Context
|
||||
|
||||
@docs init, update
|
||||
|
||||
@docs Bundle, bundle
|
||||
|
||||
@docs Messages
|
||||
|
||||
-}
|
||||
|
||||
import Application.Page exposing (Context)
|
||||
import Browser
|
||||
import Browser.Navigation as Nav
|
||||
import Html exposing (Html, div)
|
||||
import Html.Attributes as Attr
|
||||
import Internals.Context as Context exposing (Context)
|
||||
import Internals.Page as Page exposing (Page)
|
||||
import Process
|
||||
import Task
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
{-| 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 =
|
||||
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 =
|
||||
{ context :
|
||||
{ layout :
|
||||
{ init :
|
||||
route
|
||||
-> flags
|
||||
-> ( contextModel, Cmd contextMsg )
|
||||
LayoutContext route flags (Msg contextMsg msg)
|
||||
-> ( contextModel, Cmd contextMsg, Cmd (Msg contextMsg msg) )
|
||||
, update :
|
||||
Messages route (Msg contextMsg msg)
|
||||
-> route
|
||||
LayoutContext route flags (Msg contextMsg msg)
|
||||
-> contextMsg
|
||||
-> contextModel
|
||||
-> ( contextModel, Cmd contextMsg, Cmd (Msg contextMsg msg) )
|
||||
@ -64,13 +84,13 @@ type alias Config flags route contextModel contextMsg model msg =
|
||||
-> Sub contextMsg
|
||||
, view :
|
||||
{ route : route
|
||||
, context : contextModel
|
||||
, toMsg : contextMsg -> Msg contextMsg msg
|
||||
, viewPage : Html (Msg contextMsg msg)
|
||||
}
|
||||
-> contextModel
|
||||
-> Html (Msg contextMsg msg)
|
||||
}
|
||||
, page :
|
||||
, pages :
|
||||
{ init :
|
||||
Context flags route contextModel
|
||||
-> ( model, Cmd msg, Cmd contextMsg )
|
||||
@ -82,56 +102,24 @@ type alias Config flags route contextModel contextMsg model msg =
|
||||
, bundle :
|
||||
Context flags route contextModel
|
||||
-> model
|
||||
-> Application.Page.Bundle msg
|
||||
-> Bundle msg
|
||||
}
|
||||
, route :
|
||||
{ fromUrl : Url -> route
|
||||
, routing :
|
||||
{ transition : Float
|
||||
, fromUrl : Url -> route
|
||||
, toPath : route -> String
|
||||
}
|
||||
, transition : Float
|
||||
}
|
||||
|
||||
|
||||
{-| The way to create an `Application`!
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
{-| The nformation about the route, flags, or global app state.
|
||||
-}
|
||||
create :
|
||||
Config flags route contextModel contextMsg model msg
|
||||
-> Application flags contextModel contextMsg model msg
|
||||
create config =
|
||||
Browser.application
|
||||
{ init = init config
|
||||
, update = update config
|
||||
, view = view config
|
||||
, subscriptions = subscriptions config
|
||||
, onUrlChange = UrlChanged
|
||||
, onUrlRequest = UrlRequested
|
||||
}
|
||||
type alias Context flags route contextModel =
|
||||
Context.Context flags route contextModel
|
||||
|
||||
|
||||
|
||||
-- ACTUAl STUFF
|
||||
|
||||
|
||||
type alias Model flags contextModel model =
|
||||
@ -193,27 +181,43 @@ type Msg contextMsg 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 =
|
||||
{ navigateTo : route -> Cmd msg
|
||||
}
|
||||
|
||||
|
||||
init :
|
||||
initWithConfig :
|
||||
Config flags route contextModel contextMsg model msg
|
||||
-> flags
|
||||
-> Url
|
||||
-> Nav.Key
|
||||
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
|
||||
init config flags url key =
|
||||
initWithConfig config flags url key =
|
||||
let
|
||||
route =
|
||||
config.route.fromUrl url
|
||||
config.routing.fromUrl url
|
||||
|
||||
( contextModel, contextCmd ) =
|
||||
config.context.init route flags
|
||||
( contextModel, contextCmd, globalCmd ) =
|
||||
config.layout.init
|
||||
{ messages = { navigateTo = navigateTo config url }
|
||||
, route = route
|
||||
, flags = flags
|
||||
}
|
||||
|
||||
( pageModel, pageCmd, pageContextCmd ) =
|
||||
config.page.init
|
||||
config.pages.init
|
||||
{ route = route
|
||||
, flags = flags
|
||||
, context = contextModel
|
||||
@ -226,7 +230,8 @@ init config flags url key =
|
||||
, page = FirstLoad pageModel
|
||||
}
|
||||
, Cmd.batch
|
||||
[ delay config.transition (PageLoaded url)
|
||||
[ globalCmd
|
||||
, delay config.routing.transition (PageLoaded url)
|
||||
, Cmd.map ContextMsg contextCmd
|
||||
, Cmd.map ContextMsg pageContextCmd
|
||||
, Cmd.map PageMsg pageCmd
|
||||
@ -239,12 +244,12 @@ delay ms msg =
|
||||
Task.perform (\_ -> msg) (Process.sleep ms)
|
||||
|
||||
|
||||
update :
|
||||
updateWithConfig :
|
||||
Config flags route contextModel contextMsg model msg
|
||||
-> Msg contextMsg msg
|
||||
-> Model flags contextModel model
|
||||
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
|
||||
update config msg model =
|
||||
updateWithConfig config msg model =
|
||||
case msg of
|
||||
UrlRequested urlRequest ->
|
||||
case urlRequest of
|
||||
@ -260,14 +265,14 @@ update config msg model =
|
||||
|
||||
UrlChanged url ->
|
||||
( { model | page = Loading (unwrap model.page) }
|
||||
, delay config.transition (PageLoaded url)
|
||||
, delay config.routing.transition (PageLoaded url)
|
||||
)
|
||||
|
||||
PageLoaded url ->
|
||||
let
|
||||
( pageModel, pageCmd, contextCmd ) =
|
||||
config.page.init
|
||||
{ route = config.route.fromUrl url
|
||||
config.pages.init
|
||||
{ route = config.routing.fromUrl url
|
||||
, flags = model.flags
|
||||
, context = model.context
|
||||
}
|
||||
@ -282,10 +287,11 @@ update config msg model =
|
||||
ContextMsg msg_ ->
|
||||
let
|
||||
( contextModel, contextCmd, globalCmd ) =
|
||||
config.context.update
|
||||
{ navigateTo = navigateTo config model.url
|
||||
config.layout.update
|
||||
{ messages = { navigateTo = navigateTo config model.url }
|
||||
, route = config.routing.fromUrl model.url
|
||||
, flags = model.flags
|
||||
}
|
||||
(config.route.fromUrl model.url)
|
||||
msg_
|
||||
model.context
|
||||
in
|
||||
@ -299,8 +305,8 @@ update config msg model =
|
||||
PageMsg msg_ ->
|
||||
let
|
||||
( pageModel, pageCmd, contextCmd ) =
|
||||
config.page.update
|
||||
{ route = config.route.fromUrl model.url
|
||||
config.pages.update
|
||||
{ route = config.routing.fromUrl model.url
|
||||
, flags = model.flags
|
||||
, context = model.context
|
||||
}
|
||||
@ -321,11 +327,11 @@ type alias Document msg =
|
||||
}
|
||||
|
||||
|
||||
view :
|
||||
viewWithConfig :
|
||||
Config flags route contextModel contextMsg model msg
|
||||
-> Model flags contextModel model
|
||||
-> Document (Msg contextMsg msg)
|
||||
view config model =
|
||||
viewWithConfig config model =
|
||||
let
|
||||
transitionProp : Float -> String
|
||||
transitionProp ms =
|
||||
@ -358,43 +364,43 @@ view config model =
|
||||
( context, pageModel ) =
|
||||
contextAndPage ( config, model )
|
||||
in
|
||||
{ title = config.page.bundle context pageModel |> .title
|
||||
{ title = config.pages.bundle context pageModel |> .title
|
||||
, body =
|
||||
[ div
|
||||
[ Attr.class "app"
|
||||
, Attr.style "transition" (transitionProp config.transition)
|
||||
, Attr.style "transition" (transitionProp config.routing.transition)
|
||||
, Attr.style "opacity" (layoutOpacity model.page)
|
||||
]
|
||||
[ config.context.view
|
||||
{ route = config.route.fromUrl model.url
|
||||
[ config.layout.view
|
||||
{ route = config.routing.fromUrl model.url
|
||||
, toMsg = ContextMsg
|
||||
, context = model.context
|
||||
, viewPage =
|
||||
div
|
||||
[ Attr.style "transition" (transitionProp config.transition)
|
||||
[ Attr.style "transition" (transitionProp config.routing.transition)
|
||||
, Attr.style "opacity" (pageOpacity model.page)
|
||||
]
|
||||
[ 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
|
||||
-> Model flags contextModel model
|
||||
-> Sub (Msg contextMsg msg)
|
||||
subscriptions config model =
|
||||
subscriptionsWithConfig config model =
|
||||
let
|
||||
( context, pageModel ) =
|
||||
contextAndPage ( config, model )
|
||||
in
|
||||
Sub.batch
|
||||
[ Sub.map ContextMsg (config.context.subscriptions (config.route.fromUrl model.url) model.context)
|
||||
, Sub.map PageMsg (config.page.bundle context pageModel |> .subscriptions)
|
||||
[ Sub.map ContextMsg (config.layout.subscriptions (config.routing.fromUrl model.url) model.context)
|
||||
, Sub.map PageMsg (config.pages.bundle context pageModel |> .subscriptions)
|
||||
]
|
||||
|
||||
|
||||
@ -404,9 +410,9 @@ subscriptions config model =
|
||||
|
||||
contextAndPage :
|
||||
( 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 ) =
|
||||
( { route = config.route.fromUrl model.url
|
||||
( { route = config.routing.fromUrl model.url
|
||||
, flags = model.flags
|
||||
, context = model.context
|
||||
}
|
||||
@ -420,7 +426,131 @@ navigateTo :
|
||||
-> route
|
||||
-> Cmd (Msg contextMsg msg)
|
||||
navigateTo config url route =
|
||||
Task.succeed (config.route.toPath route)
|
||||
Task.succeed (config.routing.toPath route)
|
||||
|> Task.map (\path -> { url | path = path })
|
||||
|> Task.map Browser.Internal
|
||||
|> 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
|
||||
( static, sandbox, element, page
|
||||
, init, update, bundle
|
||||
, Context
|
||||
, Bundle
|
||||
( static
|
||||
, sandbox
|
||||
, element
|
||||
, 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
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
@docs page
|
||||
|
||||
-}
|
||||
|
||||
import Browser
|
||||
import Html exposing (Html)
|
||||
|
||||
|
||||
type alias Context flags route contextModel =
|
||||
{ flags : flags
|
||||
, route : route
|
||||
, context : contextModel
|
||||
}
|
||||
import Internals.Context exposing (Context)
|
||||
import Internals.Page as Internals
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
Internals.Page route flags contextModel contextMsg model msg appModel appMsg
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
-- PAGE HELPERS
|
||||
|
||||
|
||||
init :
|
||||
{ page : Page route flags contextModel contextMsg model msg appModel appMsg
|
||||
, 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
|
||||
homepage =
|
||||
Page.static
|
||||
{ title = Pages.Homepage.title
|
||||
, view = Pages.Homepage.view
|
||||
, toModel = HomepageModel
|
||||
}
|
||||
|
||||
|
||||
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 :
|
||||
{ title : String
|
||||
, view : Html Never
|
||||
, toModel : () -> appModel
|
||||
}
|
||||
-> Page route flags contextModel contextMsg () Never appModel appMsg
|
||||
static config =
|
||||
{ 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
|
||||
}
|
||||
static =
|
||||
Internals.static
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
counter =
|
||||
Page.sandbox
|
||||
{ title = Pages.Counter.title
|
||||
, init = Pages.Counter.init
|
||||
, update = Pages.Counter.update
|
||||
, view = Pages.Counter.view
|
||||
, toModel = CounterModel
|
||||
, toMsg = CounterMsg
|
||||
}
|
||||
|
||||
-}
|
||||
sandbox :
|
||||
{ title : model -> String
|
||||
, init : model
|
||||
@ -147,17 +118,24 @@ sandbox :
|
||||
, toModel : model -> appModel
|
||||
}
|
||||
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||
sandbox config =
|
||||
{ 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
|
||||
}
|
||||
sandbox =
|
||||
Internals.sandbox
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
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 :
|
||||
{ title : model -> String
|
||||
, init : flags -> ( model, Cmd msg )
|
||||
@ -168,21 +146,24 @@ element :
|
||||
, toModel : model -> appModel
|
||||
}
|
||||
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||
element config =
|
||||
let
|
||||
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
|
||||
}
|
||||
element =
|
||||
Internals.element
|
||||
|
||||
|
||||
{-|
|
||||
|
||||
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 :
|
||||
{ title : Context flags route contextModel -> model -> String
|
||||
, init : Context flags route contextModel -> ( model, Cmd msg, Cmd contextMsg )
|
||||
@ -193,33 +174,5 @@ page :
|
||||
, toModel : model -> appModel
|
||||
}
|
||||
-> Page route flags contextModel contextMsg model msg appModel appMsg
|
||||
page config =
|
||||
let
|
||||
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
|
||||
)
|
||||
page =
|
||||
Internals.page
|
||||
|
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