refactoring and documentation

This commit is contained in:
Ryan Haskell-Glatz 2019-10-08 00:22:17 -05:00
parent 21a03d5db7
commit 18676881f3
15 changed files with 1273 additions and 918 deletions

931
README.md
View File

@ -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!!!

View File

@ -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": {}
} }

View File

@ -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;

View 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
]

View 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

View 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"

View File

@ -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

View 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

View File

@ -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
}
} }

View File

@ -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

View File

@ -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 =

View File

@ -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
)

View File

@ -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
)

View 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
View 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