a fresh start :)

This commit is contained in:
Ryan Haskell-Glatz 2019-10-15 23:52:54 -05:00
parent d94b630c8e
commit fe048e5d83
65 changed files with 57 additions and 17831 deletions

866
README.md
View File

@ -1,857 +1,53 @@
# ryannhg/elm-app
> a way to build single page apps with Elm.
## installing
```
elm install ryannhg/elm-app
```
## 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__.
> an experiment for creating single page apps with Elm!
## is it real?
### try it out
- A working demo is available online here: [https://elm-app-demo.netlify.com/](https://elm-app-demo.netlify.com/)
1. `npm install`
- 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!
1. `npm run dev`
### src/Main.elm
```
our-project/
elm.json
src/
Main.elm ✨
```
This is the __entrypoint__ to the application, and connects all the parts of our `Application` together:
### overview
```elm
module Main exposing (main)
import Application
import Application exposing (Application)
import Generated.Pages as Pages
import Generated.Route as Route
import Layout as Layout
main : Application () Pages.Model Pages.Msg
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 ✨
Application.create
{ routing =
{ fromUrl = Route.fromUrl
, toPath = Route.toPath
}
, layout =
{ view = Layout.view
}
, pages =
{ init = Pages.init
, update = Pages.update
, bundle = Pages.bundle
}
}
, layout = -- TODO
, pages = -- TODO
}
```
The record for `routing` only has three properties:
#### supporting code
1. __fromUrl__ - a function that turns a `Url` into a `Route`
- [`Generated.Route`](./src/Generated/Route.elm)
2. __toPath__ - a function that turns a `Route` into a `String` used for links.
- [`Generated.Pages`](./src/Generated/Pages.elm)
3. __transitionSpeed__ - number of __milliseconds__ it takes to fade in/out pages.
- [`Layout`](./src/Layout.elm)
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!
- [`Pages.Homepage`](./src/Pages/Homepage.elm) (a static page)
We'll link to that in a bit!
- [`Pages.Counter`](./src/Pages/Counter.elm) (a sandbox page)
#### 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 Application
import Route
import Components.Layout as Layout
import Pages ✨
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 ✨
}
}
```
Much like the last property, `pages` is just a few functions.
The `init` and `update` parts are fairly the same, but there's a new property that might look strange: `bundle`.
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.)
#### 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
| SignIn
```
For now, there is only two routes: `Homepage` and `SignIn`.
We also need to make `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
| SignIn
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
| SignIn
| NotFound ✨ -- see note #2
fromUrl : Url -> Route
fromUrl url =
let
router =
Parser.oneOf ✨ -- see note #1
[ Parser.map Homepage Parser.top
, Parser.map SignIn (Parser.s "sign-in")
]
in
Parser.parse router url
|> Maybe.withDefault NotFound ✨ -- see note #2
```
__Notes__
1. With `Parser.oneOf`, we match `/` to `Homepage` and `/sign-in` to `SignIn`.
2. `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 -> "/"
SignIn -> "/sign-in"
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
| SignIn
| NotFound
fromUrl : Url -> Route
fromUrl url =
let
router =
Parser.oneOf
[ Parser.map Homepage Parser.top
, Parser.map SignIn (Parser.s "sign-in")
]
in
Parser.parse router url
|> Maybe.withDefault NotFound
toPath : Route -> String
toPath route =
case route of
Homepage -> "/"
SignIn -> "/sign-in"
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 = ()
```
So let's move onto something more interesting!
---
### src/Global.elm
```
our-project/
elm.json
src/
Main.elm
Route.elm
Flags.elm
Global.elm
Ports.elm ✨
```
The purpose of `Global.elm` is 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
| Log String
```
Here we create a simple record to keep track of the user's sign in status.
Let's see an example of `Global.Model` and `Global.Msg` being used in our layout:
#### src/Ports.elm
```
our-project/
elm.json
src/
Main.elm
Route.elm
Flags.elm
Global.elm
Ports.elm ✨
```
If you need to use ports, create a file called `src/Ports.elm`
```elm
port module Ports exposing (log)
port log : String -> Cmd msg
```
We'll use them in the layouts component up next!
### src/Components/Layout.elm
```
our-project/
elm.json
src/
Main.elm
Route.elm
Flags.elm
Global.elm
Ports.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 :
{ flags : Flags
, route : Route
, navigateTo : Route -> Cmd msg
}
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
init _ =
( { isSignedIn = False }
, Cmd.none
, Cmd.none
)
```
Initially, our layout has access to a record with three fields:
- __flags__ - the initial JSON passed in with the app.
- __route__ - the current route
- __navigateTo__ - allows programmatic navigation to other pages.
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
import Ports ✨
update :
{ flags : Flags
, route : Route
, navigateTo : Route -> Cmd msg
}
-> 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
)
Log message ->
( model
, Cmd.none
, Ports.log message
)
```
In addition to the record we saw earlier with `init`, our layout's `update` function take a `Global.Msg` and `Global.Model`:
That allows us to return an updated state of the app, and programmatically navigate to different pages!
#### view
```elm
view :
{ flags : Flags
, route : Route
, viewPage : Html msg
, toMsg : Global.Msg -> msg
}
-> Global.Model
-> Html msg
view { viewPage, toMsg } model =
div [ class "layout" ]
[ Html.map toMsg (viewNavbar model)
, viewPage
, viewFooter
]
```
Instead of `navigateTo`, our `view` function takes in a record with two other properties:
2. __viewPage__ - where we want the rendered page to show up in our layout
3. __toMsg__ - a way to convert from `Global.Msg` to `msg`, so that components can send global messages, but still return `Html msg`.
The `viewNavbar` function is an example of where we would use `Html.map toMsg` to turn `Html Global.Msg` into `Html msg`:
```elm
viewNavbar : Global.Model -> Html Global.Msg
viewNavbar model =
header
[ class "navbar" ]
[ a [ href (Route.toPath Route.Homepage) ]
[ text "Home" ]
, if model.isSignedIn then
button
[ Events.onClick SignOut ]
[ text "Sign out" ]
else
button
[ Events.onClick SignIn ]
[ text "Sign in" ]
]
```
The `viewFooter` function doesn't send messages, so `Html.map toMsg` isn't necessary!
```elm
viewFooter : Html msg
viewFooter =
footer
[ class "footer" ]
[ text "Build with Elm in 2019" ]
```
If you'd like, you can update the view to use components in folders like this:
```
our-project/
elm.json
src/
Main.elm
Route.elm
Flags.elm
Global.elm
Ports.elm
Components/
Layout.elm
Navbar.elm ✨
Footer.elm ✨
```
```elm
import Components.Navbar as Navbar ✨
import Components.Footer as Footer ✨
-- ...
view :
{ flags : Flags
, route : Route
, viewPage : Html msg
, toMsg : Global.Msg -> msg
}
-> Global.Model
-> Html msg
view { viewPage, toMsg } model =
div [ class "layout" ]
[ Html.map toMsg (Navbar.view model) ✨
, viewPage
, Footer.view ✨
]
```
Moving `Components.Layout.viewNavbar` into `Components.Navbar.view`
#### subscriptions
```elm
subscriptions :
{ navigateTo : Route -> Cmd msg
, route : Route
, flags : Flags
}
-> Global.Model
-> Html Global.Msg
subscriptions _ model =
Sub.none
```
That's the entire file! Here it is
---
### src/Pages.elm
```
our-project/
elm.json
src/
Main.elm
Route.elm
Flags.elm
Global.elm
Ports.elm
Pages.elm ✨
Components/
Layout.elm
Navbar.elm
Footer.elm
```
```elm
module Pages exposing (init, update, bundle)
import Pages.Homepage
import Pages.SignIn
import Pages.NotFound
type Model
= HomepageModel ()
| SignInModel Pages.SignIn.Model
| NotFoundModel ()
type Msg
= HomepageMsg Never
| SignInMsg Pages.SignIn.Msg
| NotFoundMsg Never
pages = -- TODO
init = -- TODO
update = -- TODO
bundle = -- TODO
```
Here we define a top level `Model` and `Msg`, so we can easily implement `init`, `update`, and `bundle`.
### pages
```elm
import Application.Page as Page ✨
pages =
{ homepage =
Page.static
{ title = Pages.Homepage.title
, view = Pages.Homepage.view
, toModel = HomepageModel
}
, 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
}
, notFound =
Page.static
{ title = Pages.NotFound.title
, view = Pages.NotFound.view
, toModel = NotFoundModel
}
}
```
The `Page` type is the important abstraction that allows us to make our `init` function take in the same shape.
#### init
```elm
import Application ✨
import Flags exposing (Flags) ✨
import Global ✨
import Route exposing (Route) ✨
init :
Route
-> Application.Update Flags Route Global.Model Global.Msg Model Msg
init route =
case route of
Route.Homepage ->
Application.init
{ page = pages.homepage
}
Route.SignIn ->
Application.init
{ page = pages.signIn
}
Route.NotFound ->
Application.init
{ page = pages.notFound
}
```
#### update
```elm
update :
Msg
-> Model
-> Application.Update Flags Route Global.Model Global.Msg Model Msg
update appMsg appModel =
case ( appModel, appMsg ) of
( HomepageModel model, HomepageMsg msg ) ->
Application.update
{ page = pages.homepage
, msg = msg
, model = model
}
( HomepageModel _, _ ) ->
Application.keep appModel
( SignInModel model, SignInMsg msg ) ->
Application.update
{ page = pages.signIn
, msg = msg
, model = model
}
( SignInModel _, _ ) ->
Application.keep appModel
( NotFoundModel model, NotFoundMsg msg ) ->
Application.update
{ page = pages.notFound
, msg = msg
, model = model
}
( NotFoundModel _, _ ) ->
Application.keep appModel
```
#### bundle
```elm
bundle :
Model
-> Application.Bundle Flags Route Global.Model Msg
bundle appModel =
case appModel of
HomepageModel model ->
Application.bundle
{ page = pages.homepage
, model = model
}
SignInModel model ->
Application.bundle
{ page = pages.signIn
, model = model
}
NotFoundModel model ->
Application.bundle
{ page = pages.notFound
, model = model
}
```
Like with the last two examples, `Application.bundle` makes our case expression consistent. Behind the scenes, `bundle` is used to provide `view`, `subscriptions`, and `title`.
The alternative would look super repetitive:
```elm
-- AN IMPROVEMENT ON
view appModel =
case appModel of
HomepageModel model -> Application.view { ... }
SignInModel model -> Application.view { ... }
NotFoundModel model -> Application.view { ... }
title appModel =
case appModel of
HomepageModel model -> Application.title { ... }
SignInModel model -> Application.title { ... }
NotFoundModel model -> Application.title { ... }
subscriptions appModel =
case appModel of
HomepageModel model -> Application.subscriptions { ... }
SignInModel model -> Application.subscriptions { ... }
NotFoundModel model -> Application.subscriptions { ... }
```
The `bundle` abstraction gives us the ability to only write __one case expression__ at the top level for all three of these things 😎
You can find `src/Pages/*.elm` examples in the [basic example]. All those pages are really just normal Elm `init/update/view` things!
## that's it!
Thanks for reading this huge README, I hope this package helps you build great single page apps with Elm! 😄
- [`Pages.Random`](./src/Pages/Random.elm) (a static page)

View File

@ -1,19 +1,28 @@
{
"type": "package",
"name": "ryannhg/elm-app",
"summary": "an experiment for making single page apps with Elm",
"license": "BSD-3-Clause",
"version": "1.0.0",
"exposed-modules": [
"Application",
"Application.Page"
"type": "application",
"source-directories": [
"src",
"package"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"elm-version": "0.19.0",
"dependencies": {
"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"
"direct": {
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {}
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -5,6 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ryannhg/elm-app</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; }
</style>
</head>
<body>
<div id="app"></div>

View File

@ -1,6 +0,0 @@
# examples
> some examples of how to use this package.
- [basic](./basic)
- [elm-ui](./elm-ui)

View File

@ -1,53 +0,0 @@
# ryannhg/elm-app
> an experiment for creating single page apps with Elm!
### try it out
1. `npm install`
1. `npm run dev`
### overview
```elm
module Main exposing (main)
import Application exposing (Application)
import Generated.Pages as Pages
import Generated.Route as Route
import Layout as Layout
main : Application () Pages.Model Pages.Msg
main =
Application.create
{ routing =
{ fromUrl = Route.fromUrl
, toPath = Route.toPath
}
, layout =
{ view = Layout.view
}
, pages =
{ init = Pages.init
, update = Pages.update
, bundle = Pages.bundle
}
}
```
#### supporting code
- [`Generated.Route`](./src/Generated/Route.elm)
- [`Generated.Pages`](./src/Generated/Pages.elm)
- [`Layout`](./src/Layout.elm)
- [`Pages.Homepage`](./src/Pages/Homepage.elm) (a static page)
- [`Pages.Counter`](./src/Pages/Counter.elm) (a sandbox page)
- [`Pages.Random`](./src/Pages/Random.elm) (a static page)

View File

@ -1,28 +0,0 @@
{
"type": "application",
"source-directories": [
"src",
"package"
],
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -1,8 +0,0 @@
# examples/basic
> an intro into an app using `ryannhg/elm-app`
## how to run
1. `npm install`
1. `npm run dev`

View File

@ -1,26 +0,0 @@
{
"type": "application",
"source-directories": [
"src",
"../../src"
],
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/random": "1.0.0",
"elm/url": "1.0.0"
},
"indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="./main.css">
</head>
<body>
<div id="app"></div>
<script src="./main.js"></script>
</body>
</html>

View File

@ -1,79 +0,0 @@
html, body {
height: 100%;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
.app {
height: 100%;
padding: 2rem 1rem;
box-sizing: border-box;
max-width: 720px;
margin: 0 auto;
}
.navbar {
display: flex;
justify-content: space-between;
}
.navbar__links {
display: flex;
align-items: baseline;
}
.navbar__links > *:first-child {
font-size: 20px;
}
.navbar__links > :not(:first-child) {
margin-left: 1rem;
}
input {
padding: 0.25rem 0.5rem;
border: solid 1px #ccc;
font-size: inherit;
font-family: inherit;
margin-top: 0.5rem;
}
.button {
border: solid 1px #ccc;
padding: 0.5rem 1.5rem;
background: #06f;
color: white;
font-family: inherit;
font-size: inherit;
border-radius: 4px;
}
label {
display: block;
margin-bottom: 1rem;
}
label div {
font-weight: bold;
}
.layout {
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
}
.layout > * {
width: 100%;
}
.layout__page {
flex: 1 0 auto;
padding: 2rem 0;
box-sizing: border-box;
}
footer {
padding-bottom: 1rem;
}

View File

@ -1,5 +0,0 @@
import { Elm } from './src/Main.elm'
Elm.Main.init({
node: document.getElementById('app')
})

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +0,0 @@
{
"name": "application",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "parcel build index.html",
"dev": "parcel index.html"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"elm": "^0.19.0-no-deps",
"elm-format": "^0.8.2",
"elm-hot": "^1.1.2",
"node-elm-compiler": "^5.0.4",
"parcel-bundler": "^1.12.3"
}
}

View File

@ -1,21 +0,0 @@
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

@ -1,88 +0,0 @@
module Components.Layout exposing
( init
, subscriptions
, update
, view
)
import Components.Footer
import Components.Navbar
import Flags exposing (Flags)
import Global
import Html exposing (..)
import Html.Attributes exposing (class)
import Route exposing (Route)
import Utils.Cmd
init :
{ navigateTo : Route -> Cmd msg
, route : Route
, flags : Flags
}
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
init _ =
( { user = Nothing }
, Cmd.none
, Cmd.none
)
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 (Ok user) ->
( { model | user = Just user }
, Cmd.none
, navigateTo Route.Homepage
)
Global.SignIn (Err _) ->
Utils.Cmd.pure model
Global.SignOut ->
( { model | user = Nothing }
, Cmd.none
, navigateTo Route.SignIn
)
view :
{ flags : Flags
, 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 :
{ navigateTo : Route -> Cmd msg
, route : Route
, flags : Flags
}
-> Global.Model
-> Sub Global.Msg
subscriptions _ model =
Sub.none

View File

@ -1,69 +0,0 @@
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,28 +0,0 @@
module Data.User exposing (User, signIn, username)
import Utils.Cmd
type User
= User String
username : User -> String
username (User username_) =
username_
signIn :
{ username : String
, password : String
, msg : Result String User -> msg
}
-> Cmd msg
signIn options =
(Utils.Cmd.toCmd << options.msg) <|
case ( options.username, options.password ) of
( _, "password" ) ->
Ok (User options.username)
_ ->
Err "Sign in failed."

View File

@ -1,5 +0,0 @@
module Flags exposing (Flags)
type alias Flags =
()

View File

@ -1,16 +0,0 @@
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,31 +0,0 @@
module Main exposing (main)
import Application
import Components.Layout as Layout
import Flags exposing (Flags)
import Global
import Pages
import Route
main : Application.Program Flags Global.Model Global.Msg Pages.Model Pages.Msg
main =
Application.start <|
Application.create
{ 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
}
}

View File

@ -1,211 +0,0 @@
module Pages exposing
( Model
, Msg
, bundle
, init
, update
)
import Application
import Application.Page as Page
import Flags exposing (Flags)
import Global
import Html exposing (Html)
import Pages.Counter
import Pages.Homepage
import Pages.NotFound
import Pages.Random
import Pages.SignIn
import Route exposing (Route)
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
app =
{ bundle = Application.bundle Html.map
}
pages =
{ homepage =
Page.static
{ title = Pages.Homepage.title
, view = Pages.Homepage.view
, toModel = HomepageModel
, fromNever = Html.map never
}
, counter =
Page.sandbox
{ title = Pages.Counter.title
, init = Pages.Counter.init
, update = Pages.Counter.update
, view = Pages.Counter.view
, toModel = CounterModel
, toMsg = CounterMsg
}
, 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
}
, 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
}
, notFound =
Page.static
{ title = Pages.NotFound.title
, view = Pages.NotFound.view
, toModel = NotFoundModel
, fromNever = Html.map never
}
}
init :
Route
-> Application.Update Flags Route Global.Model Global.Msg Model Msg
init route =
case route of
Route.Homepage ->
Application.init
{ page = pages.homepage
}
Route.Counter ->
Application.init
{ page = pages.counter
}
Route.Random ->
Application.init
{ page = pages.random
}
Route.SignIn ->
Application.init
{ page = pages.signIn
}
Route.NotFound ->
Application.init
{ page = pages.notFound
}
update :
Msg
-> Model
-> Application.Update Flags Route Global.Model Global.Msg Model Msg
update appMsg appModel =
case ( appModel, appMsg ) of
( HomepageModel model, HomepageMsg msg ) ->
Application.update
{ page = pages.homepage
, msg = msg
, model = model
}
( HomepageModel _, _ ) ->
Application.keep appModel
( CounterModel model, CounterMsg msg ) ->
Application.update
{ page = pages.counter
, msg = msg
, model = model
}
( CounterModel _, _ ) ->
Application.keep appModel
( RandomModel model, RandomMsg msg ) ->
Application.update
{ page = pages.random
, msg = msg
, model = model
}
( RandomModel _, _ ) ->
Application.keep appModel
( SignInModel model, SignInMsg msg ) ->
Application.update
{ page = pages.signIn
, msg = msg
, model = model
}
( SignInModel _, _ ) ->
Application.keep appModel
( NotFoundModel model, NotFoundMsg msg ) ->
Application.update
{ page = pages.notFound
, msg = msg
, model = model
}
( NotFoundModel _, _ ) ->
Application.keep appModel
bundle :
Model
-> Application.Bundle Flags Route Global.Model Msg (Html Msg)
bundle appModel =
case appModel of
HomepageModel model ->
app.bundle
{ page = pages.homepage
, model = model
}
CounterModel model ->
app.bundle
{ page = pages.counter
, model = model
}
RandomModel model ->
app.bundle
{ page = pages.random
, model = model
}
SignInModel model ->
app.bundle
{ page = pages.signIn
, model = model
}
NotFoundModel model ->
app.bundle
{ page = pages.notFound
, model = model
}

View File

@ -1,55 +0,0 @@
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 "+" ]
]
]

View File

@ -1,19 +0,0 @@
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!" ]
]

View File

@ -1,22 +0,0 @@
module Pages.NotFound exposing
( title
, view
)
import Html exposing (..)
title : String
title =
"Not found."
view : Html Never
view =
div []
[ h1 [] [ text "Page not found!" ]
, p []
[ text "Is this space? Am I in "
, em [] [ text "space?" ]
]
]

View File

@ -1,77 +0,0 @@
module Pages.Random exposing
( Model
, Msg
, init
, subscriptions
, title
, update
, view
)
import Flags exposing (Flags)
import Html exposing (..)
import Html.Events as Events
import Random
type alias Model =
{ roll : Maybe Int
}
type Msg
= Roll
| GotOutcome Int
title : Model -> String
title model =
"Random | elm-app"
init : Flags -> ( Model, Cmd Msg )
init _ =
( { roll = Nothing }
, Cmd.none
)
rollDice : Model -> ( Model, Cmd Msg )
rollDice model =
( model
, Random.generate GotOutcome (Random.int 1 6)
)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Roll ->
rollDice model
GotOutcome value ->
( { model | roll = Just value }
, Cmd.none
)
view : Model -> Html Msg
view model =
div []
[ h1 [] [ text "Random!" ]
, p [] [ text "Did somebody say 'random numbers pls'?" ]
, div []
[ button [ Events.onClick Roll ] [ text "Roll" ]
, p []
[ model.roll
|> Maybe.map String.fromInt
|> Maybe.withDefault "Click the button!"
|> text
]
]
]
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none

View File

@ -1,136 +0,0 @@
module Pages.SignIn exposing
( Model
, Msg
, init
, subscriptions
, title
, update
, view
)
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
import Route exposing (Route)
import Utils.Cmd
type alias Model =
{ username : String
, password : String
}
type Msg
= Update Field String
| AttemptSignIn
type Field
= Username
| Password
title : Context Flags Route Global.Model -> Model -> String
title { context } model =
case context.user of
Just user ->
"Sign out " ++ User.username user ++ " | elm-app"
Nothing ->
"Sign in | elm-app"
init :
Context Flags Route Global.Model
-> ( Model, Cmd Msg, Cmd Global.Msg )
init _ =
Utils.Cmd.pure { username = "", password = "" }
update :
Context Flags Route Global.Model
-> Msg
-> Model
-> ( Model, Cmd Msg, Cmd Global.Msg )
update _ msg model =
case msg of
Update Username value ->
Utils.Cmd.pure { model | username = value }
Update Password value ->
Utils.Cmd.pure { model | password = value }
AttemptSignIn ->
( model
, Cmd.none
, User.signIn
{ username = model.username
, password = model.password
, msg = Global.SignIn
}
)
view :
Context Flags Route Global.Model
-> Model
-> Html Msg
view _ model =
div []
[ h1 [] [ text "Sign in" ]
, p [] [ text "and update some user state!" ]
, Html.form [ Events.onSubmit AttemptSignIn ]
[ viewInput
{ label = "Username"
, fieldType = "text"
, value = model.username
, onInput = Update Username
}
, viewInput
{ label = "Password"
, fieldType = "password"
, value = model.password
, onInput = Update Password
}
, p []
[ button
[ Attr.class "button"
, Attr.type_ "submit"
]
[ text "Sign in"
]
]
]
]
viewInput :
{ label : String
, fieldType : String
, value : String
, onInput : String -> msg
}
-> Html msg
viewInput options =
label []
[ div [] [ text options.label ]
, input
[ Attr.value options.value
, Attr.type_ options.fieldType
, Events.onInput options.onInput
]
[]
]
subscriptions :
Context Flags Route Global.Model
-> Model
-> Sub Msg
subscriptions _ model =
Sub.none

View File

@ -1,4 +0,0 @@
port module Ports exposing (log)
port log : String -> Cmd msg

View File

@ -1,44 +0,0 @@
module Route exposing (Route(..), fromUrl, toPath)
import Url exposing (Url)
import Url.Parser as Parser
type Route
= Homepage
| SignIn
| Counter
| Random
| NotFound
fromUrl : Url -> Route
fromUrl =
Parser.parse
(Parser.oneOf
[ Parser.map Homepage Parser.top
, Parser.map SignIn (Parser.s "sign-in")
, Parser.map Counter (Parser.s "counter")
, Parser.map Random (Parser.s "random")
]
)
>> Maybe.withDefault NotFound
toPath : Route -> String
toPath route =
case route of
Homepage ->
"/"
SignIn ->
"/sign-in"
Counter ->
"/counter"
Random ->
"/random"
NotFound ->
"/not-found"

View File

@ -1,19 +0,0 @@
module Utils.Cmd exposing
( pure
, toCmd
)
import Task
pure : model -> ( model, Cmd a, Cmd b )
pure model =
( model
, Cmd.none
, Cmd.none
)
toCmd : msg -> Cmd msg
toCmd msg =
Task.perform identity (Task.succeed msg)

View File

@ -1,8 +0,0 @@
# examples/elm-ui
> an example of how to use ryannhg/elm-app with Elm UI.
### try it out
1. `npm install`
1. `npm run dev`

View File

@ -1,26 +0,0 @@
{
"type": "application",
"source-directories": [
"src",
"../../src"
],
"elm-version": "0.19.0",
"dependencies": {
"direct": {
"elm/browser": "1.0.1",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/url": "1.0.0",
"mdgriffith/elm-ui": "1.1.5"
},
"indirect": {
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Elm UI Example | ryannhg/elm-app</title>
</head>
<body>
<div id="app"></div>
<script src="./main.js"></script>
</body>
</html>

View File

@ -1,3 +0,0 @@
import { Elm } from './src/Main.elm'
Elm.Main.init({ node: document.getElementById('app') })

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
{
"name": "@ryannhg/elm-app-examples-elm-ui",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "parcel index.html"
},
"author": "Ryan Haskell-Glatz",
"license": "ISC",
"devDependencies": {
"elm": "0.19.0-no-deps",
"elm-hot": "^1.1.2",
"node-elm-compiler": "^5.0.4",
"parcel-bundler": "1.12.4"
}
}

View File

@ -1,56 +0,0 @@
module Components.Layout exposing (init, subscriptions, update, view)
import Element exposing (Element)
import Flags exposing (Flags)
import Global
import Route exposing (Route)
init :
{ navigateTo : Route -> Cmd msg
, route : Route
, flags : Flags
}
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
init _ =
( {}
, Cmd.none
, Cmd.none
)
update :
{ navigateTo : Route -> Cmd msg
, route : Route
, flags : Flags
}
-> Global.Msg
-> Global.Model
-> ( Global.Model, Cmd Global.Msg, Cmd msg )
update _ msg model =
case msg of
Global.NoOp ->
( model, Cmd.none, Cmd.none )
view :
{ flags : Flags
, route : Route
, toMsg : Global.Msg -> msg
, viewPage : Element msg
}
-> Global.Model
-> Element msg
view { viewPage } _ =
viewPage
subscriptions :
{ navigateTo : Route -> Cmd msg
, route : Route
, flags : Flags
}
-> Global.Model
-> Sub Global.Msg
subscriptions _ _ =
Sub.none

View File

@ -1,27 +0,0 @@
module Components.LinkPage exposing (view)
import Element exposing (..)
import Element.Font as Font
import Route exposing (Route)
view :
{ title : String
, link : { label : String, route : Route }
}
-> Element msg
view options =
column [ centerX, centerY, Font.center, spacing 24 ]
[ el [ Font.bold, Font.size 48, centerX ] (text options.title)
, link
[ Font.underline
, Font.color (rgb 0 0.5 0.85)
, centerX
, mouseOver
[ alpha 0.75
]
]
{ url = Route.toPath options.link.route
, label = text options.link.label
}
]

View File

@ -1,5 +0,0 @@
module Flags exposing (Flags)
type alias Flags =
()

View File

@ -1,9 +0,0 @@
module Global exposing (Model, Msg(..))
type alias Model =
{}
type Msg
= NoOp

View File

@ -1,41 +0,0 @@
module Main exposing (main)
import Application
import Components.Layout as Layout
import Element
import Flags exposing (Flags)
import Global
import Pages
import Route
main : Application.Program Flags Global.Model Global.Msg Pages.Model Pages.Msg
main =
Application.createWith
{ toLayout =
Element.layout
[ Element.height Element.fill
, Element.width Element.fill
]
, fromAttribute = Element.htmlAttribute
, map = Element.map
, node = Element.column
}
{ 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
}
}
|> Application.start

View File

@ -1,110 +0,0 @@
module Pages exposing
( Model
, Msg
, bundle
, init
, update
)
import Application
import Application.Page
import Element exposing (Element)
import Flags exposing (Flags)
import Global
import Pages.Homepage
import Pages.NotFound
import Route exposing (Route)
type Model
= HomepageModel ()
| NotFoundModel ()
type Msg
= HomepageMsg Never
| NotFoundMsg Never
pages =
{ homepage =
Application.Page.static
{ title = "examples/elm-ui"
, view = Pages.Homepage.view
, toModel = HomepageModel
, fromNever = Element.map never
}
, notFound =
Application.Page.static
{ title = "Page not found"
, view = Pages.NotFound.view
, toModel = NotFoundModel
, fromNever = Element.map never
}
}
app =
{ bundle = Application.bundle Element.map
}
init :
Route
-> Application.Update Flags Route Global.Model Global.Msg Model Msg
init route =
case route of
Route.Homepage ->
Application.init
{ page = pages.homepage
}
Route.NotFound ->
Application.init
{ page = pages.notFound
}
update :
Msg
-> Model
-> Application.Update Flags Route Global.Model Global.Msg Model Msg
update appMsg appModel =
case ( appModel, appMsg ) of
( HomepageModel model, HomepageMsg msg ) ->
Application.update
{ page = pages.homepage
, msg = msg
, model = model
}
( HomepageModel _, _ ) ->
Application.keep appModel
( NotFoundModel model, NotFoundMsg msg ) ->
Application.update
{ page = pages.notFound
, msg = msg
, model = model
}
( NotFoundModel _, _ ) ->
Application.keep appModel
bundle :
Model
-> Application.Bundle Flags Route Global.Model Msg (Element Msg)
bundle appModel =
case appModel of
HomepageModel model ->
app.bundle
{ page = pages.homepage
, model = model
}
NotFoundModel model ->
app.bundle
{ page = pages.notFound
, model = model
}

View File

@ -1,16 +0,0 @@
module Pages.Homepage exposing (view)
import Components.LinkPage
import Element exposing (..)
import Route
view : Element Never
view =
Components.LinkPage.view
{ title = "a homepage!"
, link =
{ label = "Go somewhere cool"
, route = Route.NotFound
}
}

View File

@ -1,16 +0,0 @@
module Pages.NotFound exposing (view)
import Components.LinkPage
import Element exposing (..)
import Route
view : Element Never
view =
Components.LinkPage.view
{ title = "yea, sorry that's it."
, link =
{ label = "back to homepage"
, route = Route.Homepage
}
}

View File

@ -1,30 +0,0 @@
module Route exposing (Route(..), fromUrl, toPath)
import Url exposing (Url)
import Url.Parser as Parser
type Route
= Homepage
| NotFound
fromUrl : Url -> Route
fromUrl =
let
routes =
Parser.oneOf
[ Parser.map Homepage Parser.top
]
in
Parser.parse routes >> Maybe.withDefault NotFound
toPath : Route -> String
toPath route =
case route of
Homepage ->
"/"
NotFound ->
"/not-found"

View File

@ -1,8 +1,3 @@
[build]
base = "examples/basic/"
publish = "dist/"
command = "npm run build"
[[redirects]]
from = "/*"
to = "/index.html"

View File

@ -1,492 +0,0 @@
module Application exposing
( Application
, Bundle
, Config
, Context
, Program
, Update
, bundle
, create
, createWith
, init
, keep
, start
, update
)
import Browser
import Browser.Navigation as Nav
import Html exposing (Html)
import Html.Attributes as Attr
import Internals.Context as Context exposing (Context)
import Internals.Page as Page exposing (Page)
import Internals.Transitionable as Transitionable exposing (Transitionable)
import Process
import Task
import Url exposing (Url)
type Application flags route contextModel contextMsg model msg appElement element appAttribute
= Application
{ adapters : Adapters appElement appAttribute element contextMsg msg
, config : Config flags route contextModel contextMsg model msg appElement element
}
type alias Program flags contextModel contextMsg model msg =
Platform.Program flags (Model flags contextModel model) (Msg contextMsg msg)
type alias Adapters appElement appAttribute element contextMsg msg =
{ toLayout : appElement -> Html (Msg contextMsg msg)
, fromAttribute : Html.Attribute (Msg contextMsg msg) -> appAttribute
, map : (msg -> Msg contextMsg msg) -> element -> appElement
, node : List appAttribute -> List appElement -> appElement
}
type alias LayoutContext route flags msg =
{ navigateTo : route -> Cmd msg
, route : route
, flags : flags
}
type alias Config flags route contextModel contextMsg model msg appElement element =
{ layout :
{ init :
LayoutContext route flags (Msg contextMsg msg)
-> ( contextModel, Cmd contextMsg, Cmd (Msg contextMsg msg) )
, update :
LayoutContext route flags (Msg contextMsg msg)
-> contextMsg
-> contextModel
-> ( contextModel, Cmd contextMsg, Cmd (Msg contextMsg msg) )
, subscriptions :
LayoutContext route flags (Msg contextMsg msg)
-> contextModel
-> Sub contextMsg
, view :
{ flags : flags
, route : route
, toMsg : contextMsg -> Msg contextMsg msg
, viewPage : appElement
}
-> contextModel
-> appElement
}
, pages :
{ init :
route
-> Context flags route contextModel
-> ( model, Cmd msg, Cmd contextMsg )
, update :
msg
-> model
-> Context flags route contextModel
-> ( model, Cmd msg, Cmd contextMsg )
, bundle :
model
-> Context flags route contextModel
-> TitleViewSubs msg element
}
, routing :
{ transition : Float
, fromUrl : Url -> route
, toPath : route -> String
}
}
type alias Context flags route contextModel =
Context.Context flags route contextModel
create :
Config flags route contextModel contextMsg model msg (Html (Msg contextMsg msg)) (Html msg)
-> Application flags route contextModel contextMsg model msg (Html (Msg contextMsg msg)) (Html msg) (Html.Attribute (Msg contextMsg msg))
create =
createWith
{ toLayout = identity
, fromAttribute = identity
, map = Html.map
, node = Html.div
}
createWith :
Adapters appElement appAttribute element contextMsg msg
-> Config flags route contextModel contextMsg model msg appElement element
-> Application flags route contextModel contextMsg model msg appElement element appAttribute
createWith adapters config =
Application
{ adapters = adapters
, config = config
}
start :
Application flags route contextModel contextMsg model msg appElement element appAttribute
-> Program flags contextModel contextMsg model msg
start (Application { adapters, config }) =
Browser.application
{ init = initWithConfig config
, update = updateWithConfig config
, view = viewWithConfig adapters config
, subscriptions = subscriptionsWithConfig config
, onUrlChange = UrlChanged
, onUrlRequest = UrlRequested
}
-- ACTUAL STUFF
type alias Model flags contextModel model =
{ key : Nav.Key
, url : Url
, flags : flags
, context : contextModel
, page : Transitionable model
}
type Msg contextMsg msg
= UrlChanged Url
| UrlRequested Browser.UrlRequest
| PageLoaded Url
| ContextMsg contextMsg
| PageMsg msg
initWithConfig :
Config flags route contextModel contextMsg model msg appElement element
-> flags
-> Url
-> Nav.Key
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
initWithConfig config flags url key =
let
route =
config.routing.fromUrl url
( contextModel, contextCmd, globalCmd ) =
config.layout.init
{ navigateTo = navigateTo config url
, route = route
, flags = flags
}
( pageModel, pageCmd, pageContextCmd ) =
config.pages.init
route
{ route = route
, flags = flags
, context = contextModel
}
in
( { url = url
, key = key
, flags = flags
, context = contextModel
, page = Transitionable.FirstLoad pageModel
}
, Cmd.batch
[ globalCmd
, delay config.routing.transition (PageLoaded url)
, Cmd.map ContextMsg contextCmd
, Cmd.map ContextMsg pageContextCmd
, Cmd.map PageMsg pageCmd
]
)
delay : Float -> msg -> Cmd msg
delay ms msg =
Task.perform (\_ -> msg) (Process.sleep ms)
updateWithConfig :
Config flags route contextModel contextMsg model msg appElement element
-> Msg contextMsg msg
-> Model flags contextModel model
-> ( Model flags contextModel model, Cmd (Msg contextMsg msg) )
updateWithConfig config msg model =
case msg of
UrlRequested urlRequest ->
case urlRequest of
Browser.Internal url ->
( model
, Nav.pushUrl model.key (Url.toString url)
)
Browser.External url ->
( model
, Nav.load url
)
UrlChanged url ->
( { model | page = Transitionable.Loading (Transitionable.unwrap model.page) }
, delay config.routing.transition (PageLoaded url)
)
PageLoaded url ->
let
route =
config.routing.fromUrl url
( pageModel, pageCmd, contextCmd ) =
config.pages.init
route
{ route = route
, flags = model.flags
, context = model.context
}
in
( { model | url = url, page = Transitionable.Loaded pageModel }
, Cmd.batch
[ Cmd.map PageMsg pageCmd
, Cmd.map ContextMsg contextCmd
]
)
ContextMsg msg_ ->
let
( contextModel, contextCmd, globalCmd ) =
config.layout.update
{ navigateTo = navigateTo config model.url
, route = config.routing.fromUrl model.url
, flags = model.flags
}
msg_
model.context
in
( { model | context = contextModel }
, Cmd.batch
[ Cmd.map ContextMsg contextCmd
, globalCmd
]
)
PageMsg msg_ ->
let
( pageModel, pageCmd, contextCmd ) =
config.pages.update
msg_
(Transitionable.unwrap model.page)
{ route = config.routing.fromUrl model.url
, flags = model.flags
, context = model.context
}
in
( { model | page = Transitionable.map (always pageModel) model.page }
, Cmd.batch
[ Cmd.map ContextMsg contextCmd
, Cmd.map PageMsg pageCmd
]
)
type alias Document msg =
{ title : String
, body : List (Html msg)
}
viewWithConfig :
Adapters appElement appAttribute element contextMsg msg
-> Config flags route contextModel contextMsg model msg appElement element
-> Model flags contextModel model
-> Document (Msg contextMsg msg)
viewWithConfig adapters config model =
let
transitionProp : Float -> String
transitionProp ms =
"opacity " ++ String.fromFloat ms ++ "ms ease-in-out"
( context, pageModel ) =
contextAndPage ( config, model )
bundle_ : TitleViewSubs msg element
bundle_ =
config.pages.bundle pageModel context
in
{ title = bundle_.title
, body =
[ Html.div
[ Attr.class "app"
, Attr.style "transition" (transitionProp config.routing.transition)
, Attr.style "opacity" (Transitionable.layoutOpacity model.page)
, Attr.style "height" "100%"
, Attr.style "width" "100%"
]
[ adapters.toLayout <|
config.layout.view
{ flags = model.flags
, route = config.routing.fromUrl model.url
, toMsg = ContextMsg
, viewPage =
adapters.node
[ adapters.fromAttribute (Attr.style "height" "100%")
, adapters.fromAttribute (Attr.style "width" "100%")
, adapters.fromAttribute (Attr.style "transition" (transitionProp config.routing.transition))
, adapters.fromAttribute (Attr.style "opacity" (Transitionable.pageOpacity model.page))
]
[ adapters.map PageMsg bundle_.view
]
}
model.context
]
]
}
subscriptionsWithConfig :
Config flags route contextModel contextMsg model msg appElement element
-> Model flags contextModel model
-> Sub (Msg contextMsg msg)
subscriptionsWithConfig config model =
let
( context, pageModel ) =
contextAndPage ( config, model )
bundle_ =
config.pages.bundle pageModel context
in
Sub.batch
[ Sub.map ContextMsg
(config.layout.subscriptions
{ navigateTo = navigateTo config model.url
, route = config.routing.fromUrl model.url
, flags = model.flags
}
model.context
)
, Sub.map PageMsg bundle_.subscriptions
]
-- UTILS
contextAndPage :
( Config flags route contextModel contextMsg model msg appElement element, Model flags contextModel model )
-> ( Context flags route contextModel, model )
contextAndPage ( config, model ) =
( { route = config.routing.fromUrl model.url
, flags = model.flags
, context = model.context
}
, Transitionable.unwrap model.page
)
navigateTo :
Config flags route contextModel contextMsg model msg appElement element
-> Url
-> route
-> Cmd (Msg contextMsg msg)
navigateTo config url route =
Task.succeed (config.routing.toPath route)
|> Task.map (\path -> { url | path = path })
|> Task.map Browser.Internal
|> Task.perform UrlRequested
-- HELPERS
type alias Update flags route contextModel contextMsg appModel appMsg =
Context flags route contextModel -> ( appModel, Cmd appMsg, Cmd contextMsg )
type alias Bundle flags route contextModel appMsg appElement =
Context flags route contextModel -> TitleViewSubs appMsg appElement
init :
{ page : Page route flags contextModel contextMsg model msg appModel appMsg element
}
-> Update flags route contextModel contextMsg appModel appMsg
init config context =
Page.init config.page context
|> mapTruple
{ fromMsg = Page.toMsg config.page
, fromModel = Page.toModel config.page
}
update :
{ page : Page route flags contextModel contextMsg model msg appModel appMsg element
, msg : msg
, model : model
}
-> Update flags route contextModel contextMsg appModel appMsg
update config context =
Page.update config.page context config.msg config.model
|> mapTruple
{ fromMsg = Page.toMsg config.page
, fromModel = Page.toModel config.page
}
keep :
appModel
-> Update flags route contextModel contextMsg appModel appMsg
keep model _ =
( model, Cmd.none, Cmd.none )
type alias TitleViewSubs appMsg appElement =
{ title : String
, view : appElement
, subscriptions : Sub appMsg
}
bundle :
((msg -> appMsg) -> pageElement -> appElement)
->
{ page : Page route flags contextModel contextMsg model msg appModel appMsg pageElement
, model : model
}
-> Bundle flags route contextModel appMsg appElement
bundle toHtml config context =
{ title =
Page.title
config.page
context
config.model
, view =
toHtml (Page.toMsg config.page) <|
Page.view
config.page
context
config.model
, subscriptions =
Sub.map (Page.toMsg config.page) <|
Page.subscriptions
config.page
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,65 +0,0 @@
module Application.Page exposing
( element
, page
, sandbox
, static
)
import Internals.Context exposing (Context)
import Internals.Page as Internals
type alias Page route flags contextModel contextMsg model msg appModel appMsg element =
Internals.Page route flags contextModel contextMsg model msg appModel appMsg element
static :
{ title : String
, view : neverElement
, toModel : () -> appModel
, fromNever : neverElement -> element
}
-> Page route flags contextModel contextMsg () Never appModel appMsg element
static =
Internals.static
sandbox :
{ title : model -> String
, init : model
, update : msg -> model -> model
, view : model -> element
, toMsg : msg -> appMsg
, toModel : model -> appModel
}
-> Page route flags contextModel contextMsg model msg appModel appMsg element
sandbox =
Internals.sandbox
element :
{ title : model -> String
, init : flags -> ( model, Cmd msg )
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, view : model -> element
, toMsg : msg -> appMsg
, toModel : model -> appModel
}
-> Page route flags contextModel contextMsg model msg appModel appMsg element
element =
Internals.element
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 -> element
, toMsg : msg -> appMsg
, toModel : model -> appModel
}
-> Page route flags contextModel contextMsg model msg appModel appMsg element
page =
Internals.page

View File

@ -1,8 +0,0 @@
module Internals.Context exposing (Context)
type alias Context flags route contextModel =
{ flags : flags
, route : route
, context : contextModel
}

View File

@ -1,192 +0,0 @@
module Internals.Page exposing
( Page
, element
, init
, page
, sandbox
, static
, subscriptions
, title
, toModel
, toMsg
, update
, view
)
import Internals.Context exposing (Context)
type Page route flags contextModel contextMsg model msg appModel appMsg element
= Page (Page_ route flags contextModel contextMsg model msg appModel appMsg element)
type alias Page_ route flags contextModel contextMsg model msg appModel appMsg element =
{ 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 -> element
, toMsg : msg -> appMsg
, toModel : model -> appModel
}
-- CONSTRUCTORS
static :
{ title : String
, view : neverElement
, toModel : () -> appModel
, fromNever : neverElement -> element
}
-> Page route flags contextModel contextMsg () Never appModel appMsg element
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 -> config.fromNever config.view
, toMsg = never
, toModel = config.toModel
}
sandbox :
{ title : model -> String
, init : model
, update : msg -> model -> model
, view : model -> element
, toMsg : msg -> appMsg
, toModel : model -> appModel
}
-> Page route flags contextModel contextMsg model msg appModel appMsg element
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 -> element
, toMsg : msg -> appMsg
, toModel : model -> appModel
}
-> Page route flags contextModel contextMsg model msg appModel appMsg element
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 -> element
, toMsg : msg -> appMsg
, toModel : model -> appModel
}
-> Page route flags contextModel contextMsg model msg appModel appMsg element
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 element
-> Context flags route contextModel
-> ( model, Cmd msg, Cmd contextMsg )
init (Page page_) =
page_.init
update :
Page route flags contextModel contextMsg model msg appModel appMsg element
-> 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 element
-> Context flags route contextModel
-> model
-> String
title (Page page_) =
page_.title
view :
Page route flags contextModel contextMsg model msg appModel appMsg element
-> Context flags route contextModel
-> model
-> element
view (Page page_) =
page_.view
subscriptions :
Page route flags contextModel contextMsg model msg appModel appMsg element
-> Context flags route contextModel
-> model
-> Sub msg
subscriptions (Page page_) =
page_.subscriptions
toMsg :
Page route flags contextModel contextMsg model msg appModel appMsg element
-> msg
-> appMsg
toMsg (Page page_) =
page_.toMsg
toModel :
Page route flags contextModel contextMsg model msg appModel appMsg element
-> model
-> appModel
toModel (Page page_) =
page_.toModel

View File

@ -1,76 +0,0 @@
module Internals.Transitionable exposing
( Transitionable(..)
, isFirstLoad
, layoutOpacity
, map
, pageOpacity
, unwrap
)
type Transitionable a
= FirstLoad a
| Loading a
| Loaded a
isFirstLoad : Transitionable a -> Bool
isFirstLoad transitionable =
case transitionable of
FirstLoad _ ->
True
_ ->
False
unwrap : Transitionable a -> a
unwrap transitionable =
case transitionable of
FirstLoad a ->
a
Loading a ->
a
Loaded a ->
a
map : (a -> b) -> Transitionable a -> Transitionable b
map fn transitionable =
case transitionable of
FirstLoad a ->
FirstLoad (fn a)
Loading a ->
Loading (fn a)
Loaded a ->
Loaded (fn a)
layoutOpacity : Transitionable a -> String
layoutOpacity transitionable =
case transitionable of
FirstLoad _ ->
"0"
Loading _ ->
"1"
Loaded _ ->
"1"
pageOpacity : Transitionable a -> String
pageOpacity transitionable =
case transitionable of
FirstLoad _ ->
"0"
Loading _ ->
"0"
Loaded _ ->
"1"