set up a project for api exploration

This commit is contained in:
Ryan Haskell-Glatz 2019-11-05 17:37:10 -06:00
commit 3fccf3dbe1
55 changed files with 4555 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
dist
elm-stuff/0.19.1
node_modules

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# elm-spa
> for building single page apps
## local development
```
npm install && npm run dev
```

5
cli/README.md Normal file
View File

@ -0,0 +1,5 @@
# cli
> the thing that types the stuff
__Note:__ I will not implement this until I understand
the manual workflow first.

View File

@ -0,0 +1,12 @@
module Generated.Docs.Flags exposing
( Dynamic
, Static
)
type alias Static =
()
type alias Dynamic =
String

View File

@ -0,0 +1,111 @@
module Generated.Docs.Pages exposing
( Model
, Msg
, page
)
import App.Page
import Generated.Docs.Flags as Flags
import Generated.Docs.Routes as Routes exposing (Route(..))
import Layouts.Docs as Layout
import Pages.Docs.Dynamic
import Pages.Docs.Static
import Utils.Page as Page exposing (Page)
type Model
= DynamicModel Pages.Docs.Dynamic.Model
| StaticModel Pages.Docs.Static.Model
type Msg
= DynamicMsg Pages.Docs.Dynamic.Msg
| StaticMsg Pages.Docs.Static.Msg
page : Page Route Model Msg layoutModel layoutMsg appMsg
page =
Page.layout
{ view = Layout.view
, recipe =
{ init = init
, update = update
, bundle = bundle
}
}
-- RECIPES
type alias Recipe flags model msg appMsg =
Page.Recipe flags model msg Model Msg appMsg
type alias Recipes msg =
{ dynamic : Recipe Flags.Dynamic Pages.Docs.Dynamic.Model Pages.Docs.Dynamic.Msg msg
, static : Recipe Flags.Static Pages.Docs.Static.Model Pages.Docs.Static.Msg msg
}
recipes : Recipes msg
recipes =
{ dynamic =
Page.recipe
{ page = Pages.Docs.Dynamic.page
, toModel = DynamicModel
, toMsg = DynamicMsg
}
, static =
Page.recipe
{ page = Pages.Docs.Static.page
, toModel = StaticModel
, toMsg = StaticMsg
}
}
-- INIT
init : Route -> Page.Init Model Msg
init route =
case route of
Routes.Dynamic flags ->
recipes.dynamic.init flags
Routes.Static flags ->
recipes.static.init flags
-- UPDATE
update : Msg -> Model -> Page.Update Model Msg
update bigMsg bigModel =
case ( bigMsg, bigModel ) of
( DynamicMsg msg, DynamicModel model ) ->
recipes.dynamic.update msg model
( StaticMsg msg, StaticModel model ) ->
recipes.static.update msg model
_ ->
App.Page.keep bigModel
-- BUNDLE
bundle : Model -> Page.Bundle Msg msg
bundle bigModel =
case bigModel of
DynamicModel model ->
recipes.dynamic.bundle model
StaticModel model ->
recipes.static.bundle model

View File

@ -0,0 +1,30 @@
module Generated.Docs.Routes exposing
( Route(..)
, routes
, toPath
)
import App.Route as Route
import Generated.Docs.Flags as Flags
type Route
= Static Flags.Static
| Dynamic Flags.Dynamic
routes : List (Route.Route Route a)
routes =
[ Route.path "static" Static
, Route.dynamic Dynamic
]
toPath : Route -> String
toPath route =
case route of
Static _ ->
"/static"
Dynamic string ->
"/" ++ string

View File

@ -0,0 +1,27 @@
module Generated.Flags exposing
( Docs
, Guide
, NotFound
, SignIn
, Top
)
type alias Top =
()
type alias Docs =
()
type alias NotFound =
()
type alias SignIn =
()
type alias Guide =
()

View File

@ -0,0 +1,5 @@
module Generated.Guide.Dynamic.Flags exposing (Intro)
type alias Intro =
()

View File

@ -0,0 +1,91 @@
module Generated.Guide.Dynamic.Pages exposing
( Model
, Msg
, page
)
import App.Page
import Generated.Guide.Dynamic.Flags as Flags
import Generated.Guide.Dynamic.Routes as Routes exposing (Route(..))
import Layouts.Guide.Dynamic as Layout
import Pages.Guide.Dynamic.Intro
import Utils.Page as Page exposing (Page)
type Model
= IntroModel Pages.Guide.Dynamic.Intro.Model
type Msg
= IntroMsg Pages.Guide.Dynamic.Intro.Msg
page : Page Route Model Msg layoutModel layoutMsg appMsg
page =
Page.layout
{ view = Layout.view
, recipe =
{ init = init
, update = update
, bundle = bundle
}
}
-- RECIPES
type alias Recipe flags model msg appMsg =
Page.Recipe flags model msg Model Msg appMsg
type alias Recipes msg =
{ intro : Recipe Flags.Intro Pages.Guide.Dynamic.Intro.Model Pages.Guide.Dynamic.Intro.Msg msg
}
recipes : Recipes msg
recipes =
{ intro =
Page.recipe
{ page = Pages.Guide.Dynamic.Intro.page
, toModel = IntroModel
, toMsg = IntroMsg
}
}
-- INIT
init : Route -> Page.Init Model Msg
init route =
case route of
Routes.Intro flags ->
recipes.intro.init flags
-- UPDATE
update : Msg -> Model -> Page.Update Model Msg
update bigMsg bigModel =
case ( bigMsg, bigModel ) of
( IntroMsg msg, IntroModel model ) ->
recipes.intro.update msg model
-- _ ->
-- App.Page.keep bigModel
-- BUNDLE
bundle : Model -> Page.Bundle Msg msg
bundle bigModel =
case bigModel of
IntroModel model ->
recipes.intro.bundle model

View File

@ -0,0 +1,25 @@
module Generated.Guide.Dynamic.Routes exposing
( Route(..)
, routes
, toPath
)
import App.Route as Route
import Generated.Guide.Dynamic.Flags as Flags
type Route
= Intro Flags.Intro
routes : List (Route.Route Route a)
routes =
[ Route.path "intro" Intro
]
toPath : Route -> String
toPath route =
case route of
Intro _ ->
"/intro"

View File

@ -0,0 +1,17 @@
module Generated.Guide.Flags exposing
( Elm
, ElmSpa
, Programming
)
type alias Elm =
()
type alias ElmSpa =
()
type alias Programming =
()

View File

@ -0,0 +1,150 @@
module Generated.Guide.Pages exposing
( Model
, Msg
, page
)
import App.Page
import Generated.Guide.Dynamic.Pages
import Generated.Guide.Dynamic.Routes
import Generated.Guide.Flags as Flags
import Generated.Guide.Routes as Routes exposing (Route(..))
import Layouts.Guide as Layout
import Pages.Guide.Elm
import Pages.Guide.ElmSpa
import Pages.Guide.Programming
import Utils.Page as Page exposing (Page)
type Model
= ElmModel Pages.Guide.Elm.Model
| ElmSpaModel Pages.Guide.ElmSpa.Model
| ProgrammingModel Pages.Guide.Programming.Model
| Dynamic_Folder_Model Generated.Guide.Dynamic.Pages.Model
type Msg
= ElmMsg Pages.Guide.Elm.Msg
| ElmSpaMsg Pages.Guide.ElmSpa.Msg
| ProgrammingMsg Pages.Guide.Programming.Msg
| Dynamic_Folder_Msg Generated.Guide.Dynamic.Pages.Msg
page : Page Route Model Msg layoutModel layoutMsg appMsg
page =
Page.layout
{ view = Layout.view
, recipe =
{ init = init
, update = update
, bundle = bundle
}
}
-- RECIPES
type alias Recipe flags model msg appMsg =
Page.Recipe flags model msg Model Msg appMsg
type alias Recipes msg =
{ elm : Recipe Flags.Elm Pages.Guide.Elm.Model Pages.Guide.Elm.Msg msg
, elmApp : Recipe Flags.ElmSpa Pages.Guide.ElmSpa.Model Pages.Guide.ElmSpa.Msg msg
, programming : Recipe Flags.Programming Pages.Guide.Programming.Model Pages.Guide.Programming.Msg msg
, dynamic_folder : Recipe Generated.Guide.Dynamic.Routes.Route Generated.Guide.Dynamic.Pages.Model Generated.Guide.Dynamic.Pages.Msg msg
}
recipes : Recipes msg
recipes =
{ elm =
Page.recipe
{ page = Pages.Guide.Elm.page
, toModel = ElmModel
, toMsg = ElmMsg
}
, elmApp =
Page.recipe
{ page = Pages.Guide.ElmSpa.page
, toModel = ElmSpaModel
, toMsg = ElmSpaMsg
}
, programming =
Page.recipe
{ page = Pages.Guide.Programming.page
, toModel = ProgrammingModel
, toMsg = ProgrammingMsg
}
, dynamic_folder =
Page.recipe
{ page = Generated.Guide.Dynamic.Pages.page
, toModel = Dynamic_Folder_Model
, toMsg = Dynamic_Folder_Msg
}
}
-- INIT
init : Route -> Page.Init Model Msg
init route =
case route of
Routes.Elm flags ->
recipes.elm.init flags
Routes.ElmSpa flags ->
recipes.elmApp.init flags
Routes.Programming flags ->
recipes.programming.init flags
Routes.Dynamic_Folder flags route_ ->
recipes.dynamic_folder.init route_
-- UPDATE
update : Msg -> Model -> Page.Update Model Msg
update bigMsg bigModel =
case ( bigMsg, bigModel ) of
( ElmMsg msg, ElmModel model ) ->
recipes.elm.update msg model
( ElmSpaMsg msg, ElmSpaModel model ) ->
recipes.elmApp.update msg model
( ProgrammingMsg msg, ProgrammingModel model ) ->
recipes.programming.update msg model
( Dynamic_Folder_Msg msg, Dynamic_Folder_Model model ) ->
recipes.dynamic_folder.update msg model
_ ->
App.Page.keep bigModel
-- BUNDLE
bundle : Model -> Page.Bundle Msg msg
bundle bigModel =
case bigModel of
ElmModel model ->
recipes.elm.bundle model
ElmSpaModel model ->
recipes.elmApp.bundle model
ProgrammingModel model ->
recipes.programming.bundle model
Dynamic_Folder_Model model ->
recipes.dynamic_folder.bundle model

View File

@ -0,0 +1,41 @@
module Generated.Guide.Routes exposing
( Route(..)
, routes
, toPath
)
import App.Route as Route
import Generated.Guide.Dynamic.Routes
import Generated.Guide.Flags as Flags
type Route
= Elm Flags.Elm
| ElmSpa Flags.ElmSpa
| Programming Flags.Programming
| Dynamic_Folder String Generated.Guide.Dynamic.Routes.Route
routes : List (Route.Route Route a)
routes =
[ Route.path "elm" Elm
, Route.path "elm-spa" ElmSpa
, Route.path "programming" Programming
, Route.dynamicFolder Dynamic_Folder Generated.Guide.Dynamic.Routes.routes
]
toPath : Route -> String
toPath route =
case route of
Elm _ ->
"/"
ElmSpa _ ->
"/elm-spa"
Programming _ ->
"/programming"
Dynamic_Folder string route_ ->
"/" ++ string ++ Generated.Guide.Dynamic.Routes.toPath route_

View File

@ -0,0 +1,208 @@
module Generated.Pages exposing
( Model
, Msg
, page
)
import App.Page
import Generated.Docs.Pages
import Generated.Docs.Routes
import Generated.Flags as Flags
import Generated.Guide.Pages
import Generated.Guide.Routes
import Generated.Routes as Routes exposing (Route(..))
import Layout as Layout
import Pages.Docs
import Pages.Guide
import Pages.NotFound
import Pages.SignIn
import Pages.Top
import Utils.Page as Page exposing (Page)
type Model
= TopModel Pages.Top.Model
| DocsModel Pages.Docs.Model
| NotFoundModel Pages.NotFound.Model
| SignInModel Pages.SignIn.Model
| GuideModel Pages.Guide.Model
| Guide_Folder_Model Generated.Guide.Pages.Model
| Docs_Folder_Model Generated.Docs.Pages.Model
type Msg
= TopMsg Pages.Top.Msg
| DocsMsg Pages.Docs.Msg
| NotFoundMsg Pages.NotFound.Msg
| SignInMsg Pages.SignIn.Msg
| GuideMsg Pages.Guide.Msg
| Guide_Folder_Msg Generated.Guide.Pages.Msg
| Docs_Folder_Msg Generated.Docs.Pages.Msg
page : Page Route Model Msg layoutModel layoutMsg appMsg
page =
Page.layout
{ view = Layout.view
, recipe =
{ init = init
, update = update
, bundle = bundle
}
}
-- RECIPES
type alias Recipe flags model msg appMsg =
Page.Recipe flags model msg Model Msg appMsg
type alias Recipes msg =
{ top : Recipe Flags.Top Pages.Top.Model Pages.Top.Msg msg
, docs : Recipe Flags.Docs Pages.Docs.Model Pages.Docs.Msg msg
, notFound : Recipe Flags.NotFound Pages.NotFound.Model Pages.NotFound.Msg msg
, signIn : Recipe Flags.SignIn Pages.SignIn.Model Pages.SignIn.Msg msg
, guide : Recipe Flags.Guide Pages.Guide.Model Pages.Guide.Msg msg
, guide_folder : Recipe Generated.Guide.Routes.Route Generated.Guide.Pages.Model Generated.Guide.Pages.Msg msg
, docs_folder : Recipe Generated.Docs.Routes.Route Generated.Docs.Pages.Model Generated.Docs.Pages.Msg msg
}
recipes : Recipes msg
recipes =
{ top =
Page.recipe
{ page = Pages.Top.page
, toModel = TopModel
, toMsg = TopMsg
}
, docs =
Page.recipe
{ page = Pages.Docs.page
, toModel = DocsModel
, toMsg = DocsMsg
}
, notFound =
Page.recipe
{ page = Pages.NotFound.page
, toModel = NotFoundModel
, toMsg = NotFoundMsg
}
, signIn =
Page.recipe
{ page = Pages.SignIn.page
, toModel = SignInModel
, toMsg = SignInMsg
}
, guide =
Page.recipe
{ page = Pages.Guide.page
, toModel = GuideModel
, toMsg = GuideMsg
}
, guide_folder =
Page.recipe
{ page = Generated.Guide.Pages.page
, toModel = Guide_Folder_Model
, toMsg = Guide_Folder_Msg
}
, docs_folder =
Page.recipe
{ page = Generated.Docs.Pages.page
, toModel = Docs_Folder_Model
, toMsg = Docs_Folder_Msg
}
}
-- INIT
init : Route -> Page.Init Model Msg
init route_ =
case route_ of
Routes.Top flags ->
recipes.top.init flags
Routes.Docs flags ->
recipes.docs.init flags
Routes.NotFound flags ->
recipes.notFound.init flags
Routes.SignIn flags ->
recipes.signIn.init flags
Routes.Guide flags ->
recipes.guide.init flags
Routes.Guide_Folder route ->
recipes.guide_folder.init route
Routes.Docs_Folder route ->
recipes.docs_folder.init route
-- UPDATE
update : Msg -> Model -> Page.Update Model Msg
update bigMsg bigModel =
case ( bigMsg, bigModel ) of
( TopMsg msg, TopModel model ) ->
recipes.top.update msg model
( DocsMsg msg, DocsModel model ) ->
recipes.docs.update msg model
( NotFoundMsg msg, NotFoundModel model ) ->
recipes.notFound.update msg model
( SignInMsg msg, SignInModel model ) ->
recipes.signIn.update msg model
( GuideMsg msg, GuideModel model ) ->
recipes.guide.update msg model
( Guide_Folder_Msg msg, Guide_Folder_Model model ) ->
recipes.guide_folder.update msg model
( Docs_Folder_Msg msg, Docs_Folder_Model model ) ->
recipes.docs_folder.update msg model
_ ->
App.Page.keep bigModel
-- BUNDLE
bundle : Model -> Page.Bundle Msg msg
bundle bigModel =
case bigModel of
TopModel model ->
recipes.top.bundle model
DocsModel model ->
recipes.docs.bundle model
NotFoundModel model ->
recipes.notFound.bundle model
SignInModel model ->
recipes.signIn.bundle model
GuideModel model ->
recipes.guide.bundle model
Guide_Folder_Model model ->
recipes.guide_folder.bundle model
Docs_Folder_Model model ->
recipes.docs_folder.bundle model

View File

@ -0,0 +1,57 @@
module Generated.Routes exposing
( Route(..)
, routes
, toPath
)
import App.Route as Route
import Generated.Docs.Routes
import Generated.Flags as Flags
import Generated.Guide.Routes
type Route
= Top Flags.Top
| Docs Flags.Docs
| NotFound Flags.NotFound
| SignIn Flags.SignIn
| Guide Flags.Guide
| Guide_Folder Generated.Guide.Routes.Route
| Docs_Folder Generated.Docs.Routes.Route
routes : List (Route.Route Route a)
routes =
[ Route.top Top
, Route.path "docs" Docs
, Route.path "not-found" NotFound
, Route.path "sign-in" SignIn
, Route.path "guide" Guide
, Route.folder "guide" Guide_Folder Generated.Guide.Routes.routes
, Route.folder "docs" Docs_Folder Generated.Docs.Routes.routes
]
toPath : Route -> String
toPath route =
case route of
Top _ ->
"/"
Docs _ ->
"/docs"
NotFound _ ->
"/not-found"
SignIn _ ->
"/sign-in"
Guide _ ->
"/guide"
Guide_Folder route_ ->
"/guide" ++ Generated.Guide.Routes.toPath route_
Docs_Folder route_ ->
"/docs" ++ Generated.Docs.Routes.toPath route_

View File

@ -0,0 +1,6 @@
# elm-stuff/.elm-spa
> this is where all the generated code goes!
Normally, this whole directory should be `.gitignore`d,
but I'm keeping it around so I can manually practice
what `elm-spa build` should be building.

29
elm.json Normal file
View File

@ -0,0 +1,29 @@
{
"type": "application",
"source-directories": [
"src",
"example",
"elm-stuff/.elm-spa"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.2",
"elm/html": "1.0.0",
"elm/json": "1.1.3",
"elm/url": "1.0.0",
"elm-explorations/markdown": "1.0.0",
"mdgriffith/elm-ui": "1.1.5",
"ryannhg/elm-spa": "1.0.0"
},
"indirect": {
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

View File

@ -0,0 +1,27 @@
module Components.Button exposing (view)
import Components.Styles as Styles
import Element exposing (..)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import Html.Attributes as Attr
view :
{ onPress : Maybe msg
, label : Element msg
}
-> Element msg
view config =
Input.button
((if config.onPress == Nothing then
alpha 0.6
else
alpha 1
)
:: Styles.button
)
config

View File

@ -0,0 +1,58 @@
module Components.Hero exposing (Action(..), view)
import Components.Styles as Styles
import Element exposing (..)
import Element.Input as Input
type Action msg
= Link String
| Button msg
view :
{ title : String
, subtitle : Element msg
, buttons : List { action : Action msg, label : Element msg }
}
-> Element msg
view config =
column
[ paddingEach
{ top = 128
, bottom = 148
, left = 0
, right = 0
}
, spacing 20
, centerX
]
<|
List.concat
[ [ Styles.h1 [ centerX ] (text config.title)
, el [ centerX, alpha 0.8 ] config.subtitle
]
, config.buttons
|> List.map (viewAction (centerX :: Styles.button))
|> viewActions
]
viewAction : List (Attribute msg) -> { action : Action msg, label : Element msg } -> Element msg
viewAction attrs { action, label } =
case action of
Link url ->
link attrs { url = url, label = label }
Button msg ->
Input.button attrs { onPress = Just msg, label = label }
viewActions : List (Element msg) -> List (Element msg)
viewActions links =
if List.isEmpty links then
[]
else
[ wrappedRow [ spacing 24, centerX ] links
]

View File

@ -0,0 +1,18 @@
module Components.Section exposing (view)
import Components.Styles as Styles
import Element exposing (..)
import Html.Attributes as Attr
import Markdown
view :
{ title : String
, content : String
}
-> Element msg
view config =
paragraph []
[ Styles.h3 [] (text config.title)
, Element.html (Markdown.toHtml [ Attr.class "markdown" ] config.content)
]

View File

@ -0,0 +1,104 @@
module Components.Styles exposing
( button
, colors
, fonts
, h1
, h3
, link
, transition
)
import Element exposing (..)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Html.Attributes as Attr
colors =
{ white = rgb 1 1 1
, jet = rgb255 40 40 40
, coral = rgb255 204 75 75
}
fonts =
{ sans =
[ Font.external
{ name = "IBM Plex Sans"
, url = "https://fonts.googleapis.com/css?family=IBM+Plex+Sans:400,400i,600,600i&display=swap"
}
, Font.serif
]
}
link : List (Attribute msg)
link =
[ Font.underline
, Font.color colors.coral
, transition
{ property = "opacity"
, speed = 150
}
, mouseOver
[ alpha 0.6
]
]
button : List (Attribute msg)
button =
[ paddingXY 16 8
, Font.size 14
, Border.color colors.coral
, Font.color colors.coral
, Background.color colors.white
, Border.width 2
, Border.rounded 4
, pointer
, transition
{ property = "all"
, speed = 150
}
, mouseOver
[ Font.color colors.white
, Background.color colors.coral
]
]
h1 : List (Attribute msg) -> Element msg -> Element msg
h1 =
elWith
[ Font.family fonts.sans
, Font.semiBold
, Font.size 64
]
h3 : List (Attribute msg) -> Element msg -> Element msg
h3 =
elWith
[ Font.family fonts.sans
, Font.semiBold
, Font.size 36
]
transition :
{ property : String
, speed : Int
}
-> Attribute msg
transition { property, speed } =
Element.htmlAttribute
(Attr.style
"transition"
(property ++ " " ++ String.fromInt speed ++ "ms ease-in-out")
)
elWith : List (Attribute msg) -> List (Attribute msg) -> Element msg -> Element msg
elWith styles otherStyles =
el ([ Element.htmlAttribute (Attr.class "markdown") ] ++ styles ++ otherStyles)

58
example/Global.elm Normal file
View File

@ -0,0 +1,58 @@
module Global exposing
( Flags
, Model
, Msg(..)
, init
, subscriptions
, update
)
import Generated.Routes as Routes exposing (Route)
type alias Flags =
()
type alias Model =
{ user : Maybe String
}
type Msg
= SignIn String
| SignOut
type alias Commands msg =
{ navigate : Route -> Cmd msg
}
init : Commands msg -> Flags -> ( Model, Cmd Msg, Cmd msg )
init _ _ =
( { user = Nothing }
, Cmd.none
, Cmd.none
)
update : Commands msg -> Msg -> Model -> ( Model, Cmd Msg, Cmd msg )
update commands msg model =
case msg of
SignIn user ->
( { model | user = Just user }
, Cmd.none
, commands.navigate (Routes.Top ())
)
SignOut ->
( { model | user = Nothing }
, Cmd.none
, Cmd.none
)
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none

106
example/Layout.elm Normal file
View File

@ -0,0 +1,106 @@
module Layout exposing (view)
import App.Page
import Components.Button
import Components.Styles as Styles
import Element exposing (..)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import Global
import Html.Attributes as Attr
type alias Context msg =
{ page : Element msg
, global : Global.Model
, toMsg : Global.Msg -> msg
}
view : Context msg -> Element msg
view { page, global, toMsg } =
column
[ Font.size 16
, Font.color Styles.colors.jet
, Font.family Styles.fonts.sans
, paddingEach
{ top = 32
, left = 16
, right = 16
, bottom = 128
}
, spacing 32
, width (fill |> maximum 640)
, height fill
, centerX
]
[ Element.map toMsg (viewNavbar global.user)
, page
]
viewNavbar : Maybe String -> Element Global.Msg
viewNavbar user_ =
row
[ width fill
, spacing 24
]
[ row [ Font.size 18, spacing 24 ] <|
(link
[ Font.size 20
, Font.semiBold
, Font.color Styles.colors.coral
, Styles.transition
{ property = "opacity"
, speed = 150
}
, mouseOver [ alpha 0.6 ]
]
{ label = text "elm-spa"
, url = "/"
}
:: List.map viewLink
[ ( "docs", "/docs" )
, ( "guide", "/guide" )
]
)
, el [ alignRight ] <|
case user_ of
Just name ->
Components.Button.view
{ onPress = Just Global.SignOut
, label = text ("sign out " ++ name)
}
Nothing ->
viewButtonLink ( "sign in", "/sign-in" )
]
viewLink : ( String, String ) -> Element msg
viewLink ( label, url ) =
link Styles.link
{ url = url
, label = text label
}
viewButtonLink : ( String, String ) -> Element msg
viewButtonLink ( label, url ) =
link Styles.button
{ url = url
, label = text label
}
transition :
{ property : String, speed : Int }
-> Element.Attribute msg
transition { property, speed } =
Element.htmlAttribute
(Attr.style
"transition"
(property ++ " " ++ String.fromInt speed ++ "ms ease-in-out")
)

16
example/Layouts/Docs.elm Normal file
View File

@ -0,0 +1,16 @@
module Layouts.Docs exposing (view)
import Element exposing (..)
import Global
type alias Context msg =
{ page : Element msg
, global : Global.Model
, toMsg : Global.Msg -> msg
}
view : Context msg -> Element msg
view { page } =
page

16
example/Layouts/Guide.elm Normal file
View File

@ -0,0 +1,16 @@
module Layouts.Guide exposing (view)
import Element exposing (..)
import Global
type alias Context msg =
{ page : Element msg
, global : Global.Model
, toMsg : Global.Msg -> msg
}
view : Context msg -> Element msg
view { page } =
page

View File

@ -0,0 +1,16 @@
module Layouts.Guide.Dynamic exposing (view)
import Element exposing (..)
import Global
type alias Context msg =
{ page : Element msg
, global : Global.Model
, toMsg : Global.Msg -> msg
}
view : Context msg -> Element msg
view { page } =
page

29
example/Main.elm Normal file
View File

@ -0,0 +1,29 @@
module Main exposing (main)
import App
import Element
import Generated.Pages as Pages
import Generated.Routes as Routes
import Global
import Pages.NotFound
main : App.Program Global.Flags Global.Model Global.Msg Pages.Model Pages.Msg
main =
App.create
{ ui =
{ toHtml = Element.layout []
, map = Element.map
}
, routing =
{ routes = Routes.routes
, toPath = Routes.toPath
, notFound = Routes.NotFound ()
}
, global =
{ init = Global.init
, update = Global.update
, subscriptions = Global.subscriptions
}
, page = Pages.page
}

41
example/Pages/Docs.elm Normal file
View File

@ -0,0 +1,41 @@
module Pages.Docs exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.Docs Model Msg model msg appMsg
page =
App.Page.static
{ title = always "Docs"
, view = always view
}
-- VIEW
view : Element Msg
view =
column [ width fill ]
[ Components.Hero.view
{ title = "docs"
, subtitle = text "\"it's not done until the docs are great.\""
, buttons =
[ { label = text "elm-app", action = Components.Hero.Link "/docs/elm-app" }
, { label = text "elm-spa", action = Components.Hero.Link "/docs/elm-spa" }
]
}
]

View File

@ -0,0 +1,70 @@
module Pages.Docs.Dynamic exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Docs.Flags as Flags
import Global
import Utils.Page exposing (Page)
type alias Model =
{ slug : String
}
type alias Msg =
Never
page : Page Flags.Dynamic Model Msg model msg appMsg
page =
App.Page.sandbox
{ title = always "Dynamic"
, init = always init
, update = always update
, view = view
}
-- INIT
init : Flags.Dynamic -> Model
init slug =
{ slug = slug
}
-- UPDATE
update : Msg -> Model -> Model
update msg model =
model
-- VIEW
view : Global.Model -> Model -> Element Msg
view global model =
column
[ width fill
]
[ Components.Hero.view
{ title = "docs: " ++ model.slug
, subtitle = text "\"it's not done until the docs are great.\""
, buttons =
[ { label = text "back to docs", action = Components.Hero.Link "/docs" }
]
}
, global.user
|> Maybe.map (\name -> "Oh hey there, " ++ name ++ "!")
|> Maybe.withDefault "Sign in if you want me to say hello!"
|> text
|> el [ centerX ]
]

View File

@ -0,0 +1,40 @@
module Pages.Docs.Static exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Docs.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.Static Model Msg model msg appMsg
page =
App.Page.static
{ title = always "Static"
, view = always view
}
-- VIEW
view : Element Msg
view =
column
[ width fill
]
[ Components.Hero.view
{ title = "static tho"
, subtitle = text "\"it's not done until the docs are great.\""
, buttons = []
}
]

43
example/Pages/Guide.elm Normal file
View File

@ -0,0 +1,43 @@
module Pages.Guide exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.Guide Model Msg model msg appMsg
page =
App.Page.static
{ title = always "Guide"
, view = always view
}
-- VIEW
view : Element Msg
view =
column
[ width fill ]
[ Components.Hero.view
{ title = "guide"
, subtitle = text "alright, where should we begin?"
, buttons =
[ { label = text "new to web dev", action = Components.Hero.Link "/guide/programming" }
, { label = text "new to elm", action = Components.Hero.Link "/guide/elm" }
, { label = text "new to elm-spa", action = Components.Hero.Link "/guide/elm-spa" }
]
}
]

View File

@ -0,0 +1,40 @@
module Pages.Guide.Dynamic.Intro exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Guide.Dynamic.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.Intro Model Msg model msg appMsg
page =
App.Page.static
{ title = always "Guide.Dynamic.Intro"
, view = always view
}
-- VIEW
view : Element Msg
view =
column
[ width fill
]
[ Components.Hero.view
{ title = "intro"
, subtitle = text "\"you're gonna be great.\""
, buttons = []
}
]

View File

@ -0,0 +1,38 @@
module Pages.Guide.Elm exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Guide.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.Elm Model Msg model msg appMsg
page =
App.Page.static
{ title = always "Guide.Elm"
, view = always view
}
-- VIEW
view : Element Msg
view =
column [ width fill ]
[ Components.Hero.view
{ title = "intro to elm"
, subtitle = text "\"you're gonna be great.\""
, buttons = []
}
]

View File

@ -0,0 +1,38 @@
module Pages.Guide.ElmSpa exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Guide.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.ElmSpa Model Msg model msg appMsg
page =
App.Page.static
{ title = always "Guide.ElmSpa"
, view = always view
}
-- VIEW
view : Element Msg
view =
column [ width fill ]
[ Components.Hero.view
{ title = "intro to elm-spa"
, subtitle = text "\"you're gonna be great.\""
, buttons = []
}
]

View File

@ -0,0 +1,38 @@
module Pages.Guide.Programming exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Guide.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.Programming Model Msg model msg appMsg
page =
App.Page.static
{ title = always "Guide.Programming"
, view = always view
}
-- VIEW
view : Element Msg
view =
column [ width fill ]
[ Components.Hero.view
{ title = "programming"
, subtitle = text "become nerdy, in a lovable way"
, buttons = []
}
]

View File

@ -0,0 +1,40 @@
module Pages.NotFound exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Element exposing (..)
import Generated.Flags as Flags
import Utils.Page exposing (Page)
type alias Model =
()
type alias Msg =
Never
page : Page Flags.NotFound Model Msg model msg appMsg
page =
App.Page.static
{ title = always "NotFound"
, view = always view
}
-- VIEW
view : Element Msg
view =
column [ width fill ]
[ Components.Hero.view
{ title = "that's not a place."
, subtitle = text "but i'm not even mad about it."
, buttons =
[ { label = text "back home", action = Components.Hero.Link "/" }
]
}
]

204
example/Pages/SignIn.elm Normal file
View File

@ -0,0 +1,204 @@
module Pages.SignIn exposing (Model, Msg, page)
import App.Page
import Components.Button
import Components.Styles as Styles
import Element exposing (..)
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import Generated.Flags as Flags
import Global
import Html exposing (Html)
import Html.Attributes as Attr
import Html.Events as Events
import Utils.Page exposing (Page)
type alias Model =
{ username : String
, password : String
}
type Msg
= UpdatedField Field String
| ClickedSignIn
type Field
= Username
| Password
page : Page Flags.SignIn Model Msg model msg appMsg
page =
App.Page.component
{ title = always "sign in | elm-spa"
, init = always init
, update = always update
, subscriptions = always subscriptions
, view = always view
}
-- INIT
init : Flags.SignIn -> ( Model, Cmd Msg, Cmd Global.Msg )
init flags =
( { username = ""
, password = ""
}
, Cmd.none
, Cmd.none
)
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update msg model =
case msg of
UpdatedField Username value ->
( { model | username = value }
, Cmd.none
, Cmd.none
)
UpdatedField Password value ->
( { model | password = value }
, Cmd.none
, Cmd.none
)
ClickedSignIn ->
( model
, Cmd.none
, App.Page.send <|
Global.SignIn model.username
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> Element Msg
view model =
el [ centerX, centerY ] <|
form
{ onSubmit = ClickedSignIn
}
[ spacing 32 ]
[ el [ Font.size 24, Font.semiBold ]
(text "Sign in")
, column [ spacing 16 ]
[ viewField
{ label = "Username"
, onChange = UpdatedField Username
, inputType = TextInput
, value = model.username
}
, viewField
{ label = "Password"
, onChange = UpdatedField Password
, inputType = PasswordInput
, value = model.password
}
]
, el [ alignRight ] <|
if String.isEmpty model.username then
Input.button (Styles.button ++ [ alpha 0.6 ])
{ onPress = Nothing
, label = text "Sign In"
}
else
Input.button (Styles.button ++ [ htmlAttribute (Attr.type_ "submit") ])
{ onPress = Just ClickedSignIn
, label = text "Sign In"
}
]
form : { onSubmit : msg } -> List (Attribute msg) -> List (Element msg) -> Element msg
form config attrs children =
Element.html
(Html.form
[ Events.onSubmit config.onSubmit ]
[ toHtml (column attrs children)
]
)
toHtml : Element msg -> Html msg
toHtml =
Element.layoutWith { options = [ Element.noStaticStyleSheet ] } []
type InputType
= TextInput
| PasswordInput
viewField :
{ inputType : InputType
, label : String
, onChange : String -> msg
, value : String
}
-> Element msg
viewField config =
let
styles =
{ field =
[ paddingXY 4 4
, Border.rounded 0
, Border.widthEach
{ top = 0
, left = 0
, right = 0
, bottom = 1
}
]
, label =
[ Font.size 16
, Font.semiBold
]
}
label =
Input.labelAbove
styles.label
(text config.label)
in
case config.inputType of
TextInput ->
Input.text styles.field
{ onChange = config.onChange
, text = config.value
, placeholder = Nothing
, label = label
}
PasswordInput ->
Input.currentPassword styles.field
{ onChange = config.onChange
, text = config.value
, placeholder = Nothing
, label = label
, show = False
}

131
example/Pages/Top.elm Normal file
View File

@ -0,0 +1,131 @@
module Pages.Top exposing (Model, Msg, page)
import App.Page
import Components.Hero
import Components.Section
import Components.Styles as Styles
import Element exposing (..)
import Generated.Flags as Flags
import Html.Attributes as Attr
import Ports
import Utils.Page exposing (Page)
type alias Model =
()
page : Page Flags.Top Model Msg model msg appMsg
page =
App.Page.element
{ title = always "elm-spa"
, init = always init
, update = always update
, view = always view
, subscriptions = always subscriptions
}
-- INIT
init : Flags.Top -> ( Model, Cmd Msg )
init flags =
( ()
, Cmd.none
)
-- UPDATE
type Msg
= ScrollTo String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ScrollTo id ->
( model
, Ports.scrollTo id
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- VIEW
view : Model -> Element Msg
view model =
column [ width fill ]
[ Components.Hero.view
{ title = "elm-spa"
, subtitle = text "a framework for Elm."
, buttons =
[ { label = text "learn more"
, action = Components.Hero.Button (ScrollTo "page-content")
}
]
}
, column
[ width fill
, spacing 48
, htmlAttribute (Attr.id "page-content")
]
[ Components.Section.view
{ title = "does elm need a framework?"
, content = """
__nope, not really__ it's kinda got one built in! so building something like _React_, _VueJS_, or _Angular_ wouldn't really make sense.
#### ...but even _frameworks_ need frameworks!
that's why projects like _VueJS_ also have awesome tools like [NuxtJS](#nuxt) that bring together the best tools in the ecosystem (and a set of shared best practices!)
welcome to __elm-spa__, a framework for Elm!
"""
}
, Components.Section.view
{ title = "what does it do?"
, content = """
__elm-spa__ brings together the best of the Elm ecosystem in one place.
- [elm-ui](#elm-ui) a package for creating layout and styles (without CSS!)
- [elm-live](#elm-live) a dev server (without a webpack config!)
- [elm-spa](#elm-spa) a package for composing pages (without all the typing!)
"""
}
, Components.Section.view
{ title = "new to programming?"
, content = """
perfect! if you're able to read through this paragraph, you're already _overqualified_.
#### new to elm?
welcome aboard! we've got a series of short tutorials to help you get started.
#### new to elm-spa?
let's dive in and check out all the neat stuff that's ready for your next Elm app!
"""
}
, wrappedRow [ spacing 24 ]
[ link Styles.button { label = text "new to programming", url = "/guide/programming" }
, link Styles.button { label = text "new to elm", url = "/guide/elm" }
, link Styles.button { label = text "new to elm-spa", url = "/guide/elm-spa" }
]
]
]

14
example/Ports.elm Normal file
View File

@ -0,0 +1,14 @@
port module Ports exposing (scrollTo)
import Json.Encode as Json
port outgoing : { action : String, data : Json.Value } -> Cmd msg
scrollTo : String -> Cmd msg
scrollTo id =
outgoing
{ action = "SCROLL_TO"
, data = Json.string id
}

5
example/README.md Normal file
View File

@ -0,0 +1,5 @@
# src
```elm
```

51
example/Utils/Page.elm Normal file
View File

@ -0,0 +1,51 @@
module Utils.Page exposing
( Bundle
, Init
, Page
, Recipe
, Update
, layout
, recipe
)
import App.Page
import App.Types
import Element exposing (Element)
import Global
type alias Page flags model msg layoutModel layoutMsg appMsg =
App.Types.Page flags model msg (Element msg) layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
type alias Recipe flags model msg layoutModel layoutMsg appMsg =
App.Types.Recipe flags model msg layoutModel layoutMsg (Element layoutMsg) Global.Model Global.Msg appMsg (Element appMsg)
type alias Init model msg =
App.Types.Init model msg Global.Model Global.Msg
type alias Update model msg =
App.Types.Update model msg Global.Model Global.Msg
type alias Bundle msg appMsg =
App.Types.Bundle msg (Element msg) Global.Model Global.Msg appMsg (Element appMsg)
layout config =
App.Page.layout
{ map = Element.map
, view = config.view
, recipe = config.recipe
}
recipe config =
App.Page.recipe
{ map = Element.map
, page = config.page
, toModel = config.toModel
, toMsg = config.toMsg
}

23
index.html Normal file
View File

@ -0,0 +1,23 @@
<!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-spa</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/styles/atom-one-light.min.css" />
<link rel="stylesheet" href="/public/styles.css">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.10/languages/elm.min.js"></script>
<script src="/dist/elm.compiled.js"></script>
<script src="/public/ports.js"></script>
<script>
window.addEventListener('load', _ => {
window.ports.init(Elm.Main.init())
hljs.initHighlightingOnLoad()
})
</script>
</body>
</html>

6
netlify.toml Normal file
View File

@ -0,0 +1,6 @@
# sends all routes to /index.html
# (so you can handle 404s there!)
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

1256
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "elm-spa-elm-ui",
"version": "1.0.0",
"description": "elm-live running in the browser!",
"main": "src/index.js",
"scripts": {
"start": "npm run dev",
"dev": "npm run elm:spa:build && npm run elm:spa:watch & npm run elm:live",
"build": "npm run elm:spa:build && npm run elm:compile",
"elm:compile": "elm make example/Main.elm --output=dist/elm.compiled.js --optimize",
"elm:live": "elm-live example/Main.elm --start-page=index.html --pushstate --port=8080 -- --output=dist/elm.compiled.js --debug",
"elm:spa:build": "echo '🌳 elm-spa build .'",
"elm:spa:watch": "SHELL=/bin/bash chokidar './example' -c 'npm run elm:spa:build'"
},
"dependencies": {
"chokidar-cli": "2.1.0",
"elm": "0.19.1-3",
"elm-live": "4.0.1",
"elm-spa": "1.1.0"
},
"devDependencies": {},
"keywords": []
}

27
public/ports.js Normal file
View File

@ -0,0 +1,27 @@
const SCROLL_TO = (id) =>
document.getElementById(id) &&
window.scrollTo({
top: document.getElementById(id).offsetTop,
left: 0,
behavior: "smooth"
})
window.addEventListener("load", () => {
window.ports = {
init: (app) => {
app.ports.outgoing.subscribe(({ action, data }) => {
const actions = {
SCROLL_TO
}
if (actions[action]) {
actions[action](data)
} else {
console.warn(
`I didn't recognize action "${action}". Check out ./public/ports.js`
)
}
})
}
}
})

37
public/styles.css Normal file
View File

@ -0,0 +1,37 @@
body {
overflow-y: scroll;
}
.markdown > * {
margin: 0;
margin-top: 32px;
}
.markdown > *:not(:first-child) {
margin-top: 1rem;
}
.markdown pre {
font-size: 16px;
line-height: 1.3;
border: solid 1px #d0d0d0;
border-radius: 2px;
padding: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.markdown p {
font-size: 18px;
line-height: 1.5;
}
.markdown h3,
.markdown h3 * {
font-size: 36px;
line-height: 1.2;
}
.markdown h4,
.markdown h4 * {
font-size: 22px;
line-height: 1.2;
}
.markdown a {
text-decoration: underline;
font-weight: 600;
color: rgb(204, 75, 75);
}

8
sandbox.config.json Normal file
View File

@ -0,0 +1,8 @@
{
"infiniteLoopProtection": true,
"hardReloadOnChange": false,
"view": "browser",
"container": {
"port": 8080
}
}

407
src/App.elm Normal file
View File

@ -0,0 +1,407 @@
module App exposing
( Program, create
, usingHtml
)
{-|
## Let's build some single page applications!
`App.create` replaces [Browser.application](https://package.elm-lang.org/packages/elm/browser/latest/Browser#application)
as the entrypoint to your app.
import App
import Generated.Pages as Pages
import Generated.Route as Route
import Global
main =
App.create
{ ui = App.usingHtml
, routing =
{ routes = Route.routes
, toPath = Route.toPath
, notFound = Route.NotFound ()
}
, global =
{ init = Global.init
, update = Global.update
, subscriptions = Global.subscriptions
}
, page = Pages.page
}
@docs Program, create
# Supports more than elm/html
If you're a fan of [mdgriffith/elm-ui](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/),
it's important to support using `Element msg` instead of `Html msg` for your pages and components.
Let `App.create` know about this by passing in your own `Options` like these:
import Element
-- other imports
App.create
{ ui =
{ toHtml = Element.layout []
, map = Element.map
}
, -- ... the rest of your app
}
@docs usingHtml
-}
import App.Route as Route exposing (Route)
import Browser
import Browser.Navigation as Nav
import Html exposing (Html)
import Internals.Page as Page
import Internals.Utils as Utils
import Url exposing (Url)
import Url.Parser as Parser
-- APPLICATION
{-| An alias for `Platform.Program` to make annotations a little more clear.
-}
type alias Program flags globalModel globalMsg layoutModel layoutMsg =
Platform.Program flags (Model flags globalModel layoutModel) (Msg globalMsg layoutMsg)
{-| Pass this in when calling `App.create`
( It will work if your view returns the standard `Html msg` )
-}
usingHtml :
{ map :
(layoutMsg -> Msg globalMsg layoutMsg)
-> Html layoutMsg
-> Html (Msg globalMsg layoutMsg)
, toHtml : uiMsg -> uiMsg
}
usingHtml =
{ toHtml = identity
, map = Html.map
}
{-| Creates a new `Program` given some one-time configuration:
- `ui` - How do we convert the view to `Html msg`?
- `routing` - What are the app's routes?
- `global` - How do we manage shared state between pages?
- `page` - What pages do we have available?
-}
create :
{ ui :
{ toHtml : uiMsg -> Html (Msg globalMsg layoutMsg)
, map : (layoutMsg -> Msg globalMsg layoutMsg) -> uiLayoutMsg -> uiMsg
}
, routing :
{ routes : List (Route route route)
, toPath : route -> String
, notFound : route
}
, global :
{ init :
{ navigate : route -> Cmd (Msg globalMsg layoutMsg)
}
-> flags
-> ( globalModel, Cmd globalMsg, Cmd (Msg globalMsg layoutMsg) )
, update :
{ navigate : route -> Cmd (Msg globalMsg layoutMsg)
}
-> globalMsg
-> globalModel
-> ( globalModel, Cmd globalMsg, Cmd (Msg globalMsg layoutMsg) )
, subscriptions : globalModel -> Sub globalMsg
}
, page : Page.Page route layoutModel layoutMsg uiLayoutMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg (Msg globalMsg layoutMsg) uiMsg
}
-> Program flags globalModel globalMsg layoutModel layoutMsg
create config =
let
page =
Page.upgrade
{ toModel = identity
, toMsg = identity
, page = config.page
, map = always identity
}
in
Browser.application
{ init =
init
{ init =
{ global = config.global.init
, pages = page.init
}
, routing =
{ fromUrl = fromUrl config.routing
, toPath = config.routing.toPath
}
}
, update =
update
{ routing =
{ fromUrl = fromUrl config.routing
, toPath = config.routing.toPath
, routes = config.routing.routes
}
, init = page.init
, update =
{ global = config.global.update
, pages = page.update
}
}
, subscriptions =
subscriptions
{ bundle = page.bundle
, map = config.ui.map
}
, view =
view
{ toHtml = config.ui.toHtml
, bundle = page.bundle
, map = config.ui.map
}
, onUrlChange = ChangedUrl
, onUrlRequest = ClickedLink
}
-- ROUTING
type alias Routes route a =
List (Route.Route route a)
fromUrl : { a | routes : Routes route route, notFound : route } -> Url -> route
fromUrl config =
Parser.parse (Parser.oneOf config.routes)
>> Maybe.withDefault config.notFound
-- INIT
type alias Model flags globalModel model =
{ url : Url
, flags : flags
, key : Nav.Key
, global : globalModel
, page : model
}
init :
{ routing :
{ fromUrl : Url -> route
, toPath : route -> String
}
, init :
{ global :
{ navigate : route -> Cmd (Msg globalMsg layoutMsg) }
-> flags
-> ( globalModel, Cmd globalMsg, Cmd (Msg globalMsg layoutMsg) )
, pages : route -> Page.Init layoutModel layoutMsg globalModel globalMsg
}
}
-> flags
-> Url
-> Nav.Key
-> ( Model flags globalModel layoutModel, Cmd (Msg globalMsg layoutMsg) )
init config flags url key =
url
|> config.routing.fromUrl
|> (\route ->
let
( globalModel, globalCmd, cmd ) =
config.init.global
{ navigate = navigate config.routing.toPath url
}
flags
( pageModel, pageCmd, pageGlobalCmd ) =
config.init.pages route { global = globalModel }
in
( { flags = flags
, url = url
, key = key
, global = globalModel
, page = pageModel
}
, Cmd.batch
[ Cmd.map Page pageCmd
, Cmd.map Global pageGlobalCmd
, Cmd.map Global globalCmd
, cmd
]
)
)
-- UPDATE
type Msg globalMsg msg
= ChangedUrl Url
| ClickedLink Browser.UrlRequest
| Global globalMsg
| Page msg
update :
{ routing :
{ fromUrl : Url -> route
, toPath : route -> String
, routes : Routes route a
}
, init : route -> Page.Init layoutModel layoutMsg globalModel globalMsg
, update :
{ global :
{ navigate : route -> Cmd (Msg globalMsg layoutMsg) }
-> globalMsg
-> globalModel
-> ( globalModel, Cmd globalMsg, Cmd (Msg globalMsg layoutMsg) )
, pages :
layoutMsg
-> layoutModel
-> Page.Update layoutModel layoutMsg globalModel globalMsg
}
}
-> Msg globalMsg layoutMsg
-> Model flags globalModel layoutModel
-> ( Model flags globalModel layoutModel, Cmd (Msg globalMsg layoutMsg) )
update config msg model =
case msg of
ClickedLink (Browser.Internal url) ->
if url == model.url then
( model, Cmd.none )
else
( model
, Nav.pushUrl model.key (Url.toString url)
)
ClickedLink (Browser.External url) ->
( model
, Nav.load url
)
ChangedUrl url ->
url
|> config.routing.fromUrl
|> (\route -> config.init route { global = model.global })
|> (\( pageModel, pageCmd, globalCmd ) ->
( { model
| url = url
, page = pageModel
}
, Cmd.batch
[ Cmd.map Page pageCmd
, Cmd.map Global globalCmd
]
)
)
Global globalMsg ->
config.update.global
{ navigate = navigate config.routing.toPath model.url
}
globalMsg
model.global
|> (\( global, globalCmd, cmd ) ->
( { model | global = global }
, Cmd.batch
[ Cmd.map Global globalCmd
, cmd
]
)
)
Page pageMsg ->
config.update.pages pageMsg model.page { global = model.global }
|> (\( page, pageCmd, globalCmd ) ->
( { model | page = page }
, Cmd.batch
[ Cmd.map Page pageCmd
, Cmd.map Global globalCmd
]
)
)
navigate : (route -> String) -> Url -> route -> Cmd (Msg globalMsg layoutMsg)
navigate toPath url route =
Utils.send <|
ClickedLink (Browser.Internal { url | path = toPath route })
-- SUBSCRIPTIONS
subscriptions :
{ map : (layoutMsg -> Msg globalMsg layoutMsg) -> uiLayoutMsg -> uiMsg
, bundle :
layoutModel
-> Page.Bundle layoutMsg uiLayoutMsg globalModel globalMsg (Msg globalMsg layoutMsg) uiMsg
}
-> Model flags globalModel layoutModel
-> Sub (Msg globalMsg layoutMsg)
subscriptions config model =
(config.bundle
model.page
{ fromGlobalMsg = Global
, fromPageMsg = Page
, global = model.global
, map = config.map
}
).subscriptions
-- VIEW
view :
{ map : (layoutMsg -> Msg globalMsg layoutMsg) -> uiLayoutMsg -> uiMsg
, toHtml : uiMsg -> Html (Msg globalMsg layoutMsg)
, bundle :
layoutModel
-> Page.Bundle layoutMsg uiLayoutMsg globalModel globalMsg (Msg globalMsg layoutMsg) uiMsg
}
-> Model flags globalModel layoutModel
-> Browser.Document (Msg globalMsg layoutMsg)
view config model =
let
bundle =
config.bundle
model.page
{ fromGlobalMsg = Global
, fromPageMsg = Page
, global = model.global
, map = config.map
}
in
{ title = bundle.title
, body =
[ config.toHtml bundle.view
]
}

471
src/App/Page.elm Normal file
View File

@ -0,0 +1,471 @@
module App.Page exposing
( static
, sandbox
, element
, component, send
, layout
, recipe
, keep
)
{-| Each page can be as simple or complex as you need:
1. [Static](#static) - for rendering a simple view
2. [Sandbox](#sandbox) - for maintaining state, without any side-effects
3. [Element](#element) - for maintaining state, **with** side-effects
4. [Component](#component) - for a full-blown page, that can view and update global state
## Static
For rendering a simple view.
page =
Page.static
{ title = title
, view = view
}
@docs static
## Sandbox
For maintaining state, without any side-effects.
page =
Page.sandbox
{ title = title
, init = init
, update = update
, view = view
}
@docs sandbox
## Element
For maintaining state, **with** side-effects.
page =
Page.element
{ title = title
, init = init
, update = update
, view = view
, subscriptions = subscriptions
}
@docs element
## Component
For a full-blown page, that can view and update global state.
page =
Page.component
{ title = title
, init = init
, update = update
, view = view
, subscriptions = subscriptions
}
@docs component, send
# Composing pages together
The rest of this module contains types and functions that
can be generated with the [cli companion tool](https://github.com/ryannhg/elm-spa/tree/master/cli)
If you're typing this stuff manually, you might need to know what
these are for!
## Layout
A page that is comprimised of smaller pages, that is
able to share a common layout (maybe a something like a sidebar!)
page =
Page.layout
{ map = Html.map
, layout = Layout.view
, pages =
{ init = init
, update = update
, bundle = bundle
}
}
@docs layout
## Recipe
Implementing the `init`, `update` and `bundle` functions is much easier
when you turn a `Page` type into `Recipe`.
A `Recipe` contains a record waiting for page specific data.
- `init`: just needs a `route`
- `upgrade` : just needs a `msg` and `model`
- `bundle` (`view`/`subscriptions`) : just needs a `model`
### What's a "bundle"?
We can "bundle" the `view` and `subscriptions` functions together,
because they both only depend on the current `model`. That's why this
API exposes `bundle` instead of making you type this:
-- BEFORE
view model =
case model_ of
FooModel model ->
foo.view model
BarModel model ->
bar.view model
BazModel model ->
baz.view model
subscriptions model =
case model_ of
FooModel model ->
foo.subscriptions model
BarModel model ->
bar.subscriptions model
BazModel model ->
baz.subscriptions model
-- AFTER
bundle model =
case model_ of
FooModel model ->
foo.bundle model
BarModel model ->
bar.bundle model
BazModel model ->
baz.bundle model
(Woohoo, less case expressions to type out!)
@docs recipe
## Update helper
@docs keep
-}
import Internals.Page exposing (..)
import Internals.Utils as Utils
type alias Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg =
Internals.Page.Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
{-| Turns a page and some upgrade information into a recipe,
for use in a layout's `init`, `update`, and `bundle` functions!
Page.recipe Homepage.page
{ toModel = HomepageModel
, toMsg = HomepageMsg
, map = Element.map -- ( if using elm-ui )
}
-}
recipe :
{ page : Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
, toModel : pageModel -> layoutModel
, toMsg : pageMsg -> layoutMsg
, map : (pageMsg -> layoutMsg) -> uiPageMsg -> uiLayoutMsg
}
-> Recipe pageRoute pageModel pageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
recipe =
Internals.Page.upgrade
{-| In the event that our `case` expression in `update` receives a `msg` that doesn't
match it's `model`, we use this function to keep the model as-is.
update msg_ model_ =
case ( msg_, model_ ) of
( FooMsg msg, FooModel model ) ->
foo.update msg model
( BarMsg msg, BarModel model ) ->
bar.update msg model
( BazMsg msg, BazModel model ) ->
baz.update msg model
_ ->
Page.keep model_
-}
keep :
layoutModel
-> Update layoutModel layoutMsg globalModel globalMsg
keep model =
always ( model, Cmd.none, Cmd.none )
-- STATIC
{-| Create an `static` page from a record. [Here's an example](https://github.com/ryannhg/elm-spa/examples/html/src/Pages/Index.elm)
-}
static :
{ title : { global : globalModel } -> String
, view : globalModel -> uiPageMsg
}
-> Page pageRoute () Never uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
static page =
Page
(\{ toModel, toMsg, map } ->
{ init = \_ _ -> ( toModel (), Cmd.none, Cmd.none )
, update = \_ model _ -> ( toModel model, Cmd.none, Cmd.none )
, bundle =
\_ context ->
{ title = page.title { global = context.global }
, view = page.view context.global |> map toMsg |> context.map context.fromPageMsg
, subscriptions = Sub.none
}
}
)
-- SANDBOX
{-| Create an `sandbox` page from a record. [Here's an example](https://github.com/ryannhg/elm-spa/examples/html/src/Pages/Counter.elm)
-}
sandbox :
{ title : { global : globalModel, model : pageModel } -> String
, init : globalModel -> pageRoute -> pageModel
, update : globalModel -> pageMsg -> pageModel -> pageModel
, view : globalModel -> pageModel -> uiPageMsg
}
-> Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
sandbox page =
Page
(\{ toModel, toMsg, map } ->
{ init =
\pageRoute context ->
( toModel (page.init context.global pageRoute)
, Cmd.none
, Cmd.none
)
, update =
\msg model context ->
( page.update context.global msg model |> toModel
, Cmd.none
, Cmd.none
)
, bundle =
\model context ->
{ title = page.title { global = context.global, model = model }
, view = page.view context.global model |> map toMsg |> context.map context.fromPageMsg
, subscriptions = Sub.none
}
}
)
-- ELEMENT
{-| Create an `element` page from a record. [Here's an example](https://github.com/ryannhg/elm-spa/examples/html/src/Pages/Random.elm)
-}
element :
{ title : { global : globalModel, model : pageModel } -> String
, init : globalModel -> pageRoute -> ( pageModel, Cmd pageMsg )
, update : globalModel -> pageMsg -> pageModel -> ( pageModel, Cmd pageMsg )
, view : globalModel -> pageModel -> uiPageMsg
, subscriptions : globalModel -> pageModel -> Sub pageMsg
}
-> Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
element page =
Page
(\{ toModel, toMsg, map } ->
{ init =
\pageRoute context ->
page.init context.global pageRoute
|> tuple toModel toMsg
, update =
\msg model context ->
page.update context.global msg model
|> tuple toModel toMsg
, bundle =
\model context ->
{ title = page.title { global = context.global, model = model }
, view = page.view context.global model |> map toMsg |> context.map context.fromPageMsg
, subscriptions = page.subscriptions context.global model |> Sub.map (toMsg >> context.fromPageMsg)
}
}
)
-- COMPONENT
{-| Create an `component` page from a record. [Here's an example](https://github.com/ryannhg/elm-spa/examples/html/src/Pages/SignIn.elm)
-}
component :
{ title : { global : globalModel, model : pageModel } -> String
, init : globalModel -> pageRoute -> ( pageModel, Cmd pageMsg, Cmd globalMsg )
, update : globalModel -> pageMsg -> pageModel -> ( pageModel, Cmd pageMsg, Cmd globalMsg )
, view : globalModel -> pageModel -> uiPageMsg
, subscriptions : globalModel -> pageModel -> Sub pageMsg
}
-> Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
component page =
Page
(\{ toModel, toMsg, map } ->
{ init =
\pageRoute context ->
page.init context.global pageRoute
|> truple toModel toMsg
, update =
\msg model context ->
page.update context.global msg model
|> truple toModel toMsg
, bundle =
\model context ->
{ title = page.title { global = context.global, model = model }
, view = page.view context.global model |> map toMsg |> context.map context.fromPageMsg
, subscriptions = page.subscriptions context.global model |> Sub.map (toMsg >> context.fromPageMsg)
}
}
)
{-| Useful for sending `Global.Msg` from a component.
update : Global.Model -> Msg -> Model -> ( Model, Cmd Msg, Cmd Global.Msg )
update global msg model =
case msg of
SignIn ->
( model
, Cmd.none
, Page.send Global.SignIn
)
-}
send : msg -> Cmd msg
send =
Utils.send
-- LAYOUT
{-| These are used by top-level files like `src/Generated/Pages.elm`
to compose together pages and layouts.
We'll get a better understanding of `init`, `update`, and `bundle` below!
Page.layout
{ map = Html.map
, layout = Layout.view
, pages =
{ init = init
, update = update
, bundle = bundle
}
}
-}
layout :
{ map : (pageMsg -> msg) -> uiPageMsg -> uiMsg
, view :
{ page : uiMsg
, global : globalModel
, toMsg : globalMsg -> msg
}
-> uiMsg
, recipe : Recipe pageRoute pageModel pageMsg pageModel pageMsg uiPageMsg globalModel globalMsg msg uiMsg
}
-> Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
layout options =
Page
(\{ toModel, toMsg } ->
{ init =
\pageRoute global ->
options.recipe.init pageRoute global
|> truple toModel toMsg
, update =
\msg model global ->
options.recipe.update msg model global
|> truple toModel toMsg
, bundle =
\model context ->
let
bundle : { title : String, view : uiMsg, subscriptions : Sub msg }
bundle =
options.recipe.bundle
model
{ fromGlobalMsg = context.fromGlobalMsg
, fromPageMsg = toMsg >> context.fromPageMsg
, global = context.global
, map = options.map
}
in
{ title = bundle.title
, view =
options.view
{ page = bundle.view
, global = context.global
, toMsg = context.fromGlobalMsg
}
, subscriptions = bundle.subscriptions
}
}
)
-- UTILS
tuple :
(model -> bigModel)
-> (msg -> bigMsg)
-> ( model, Cmd msg )
-> ( bigModel, Cmd bigMsg, Cmd a )
tuple toModel toMsg ( model, cmd ) =
( toModel model
, Cmd.map toMsg cmd
, Cmd.none
)
truple :
(model -> bigModel)
-> (msg -> bigMsg)
-> ( model, Cmd msg, Cmd a )
-> ( bigModel, Cmd bigMsg, Cmd a )
truple toModel toMsg ( a, b, c ) =
( toModel a, Cmd.map toMsg b, c )

100
src/App/Route.elm Normal file
View File

@ -0,0 +1,100 @@
module App.Route exposing
( Route
, top
, path
, folder
, dynamic, dynamicFolder
)
{-|
# Routing
The [cli tool](https://github.com/ryannhg/elm-spa/tree/master/cli) is able to generate routes based on the folder
structure and files in the `src/Pages` folder.
If you're choosing to type out the routes manually, these are just
convenience functions for creating `Url.Parser` types for your application.
@docs Route
@docs top
@docs path
@docs slug
@docs folder
-}
import Url exposing (Url)
import Url.Parser as Parser exposing ((</>), Parser)
{-| Literally just a Url.Parser under the hood.
-}
type alias Route route a =
Parser (route -> a) a
{-| A route for a path like. These are generated by other file names.
Route.path "about-us" AboutUs
-}
path : String -> (() -> route) -> Route route a
path path_ toRoute =
Parser.map toRoute (Parser.s path_ |> Parser.map ())
{-| A top level route.
Usually generated by an `Top.elm` file.
Route.top Top
-}
top : (() -> route) -> Route route a
top toRoute =
Parser.map toRoute (Parser.top |> Parser.map ())
{-| A dynamic route, that passes the `String` value.
Usually generated by an `Dynamic.elm` file.
Route.dynamic Dynamic
-}
dynamic : (String -> route) -> Route route a
dynamic toRoute =
Parser.map toRoute Parser.string
{-| A route for nested routes, generated by a folder.
Route.folder "settings" Settings_ Generated.Settings.Route.routes
-}
folder :
String
-> (subRoute -> route)
-> List (Route subRoute route)
-> Route route a
folder path_ toRoute children =
Parser.map toRoute
(Parser.s path_ </> Parser.oneOf children)
{-| A route for nested dynamic routes, generated by a folder.
Route.dynamicFolder Dynamic_ Dynamic_.routes
-}
dynamicFolder :
(String -> subRoute -> route)
-> List (Route subRoute route)
-> Route route a
dynamicFolder toRoute children =
Parser.map toRoute
(Parser.string </> Parser.oneOf children)

29
src/App/Types.elm Normal file
View File

@ -0,0 +1,29 @@
module App.Types exposing
( Bundle
, Init
, Page
, Recipe
, Update
)
import Internals.Page as Page
type alias Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg =
Page.Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
type alias Recipe pageRoute pageModel pageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg =
Page.Recipe pageRoute pageModel pageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
type alias Init layoutModel layoutMsg globalModel globalMsg =
Page.Init layoutModel layoutMsg globalModel globalMsg
type alias Update layoutModel layoutMsg globalModel globalMsg =
Page.Update layoutModel layoutMsg globalModel globalMsg
type alias Bundle layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg =
Page.Bundle layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg

80
src/Internals/Page.elm Normal file
View File

@ -0,0 +1,80 @@
module Internals.Page exposing
( Bundle
, Init
, Page(..)
, Recipe
, Update
, upgrade
)
{-| Page docs
-}
type Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
= Page (Page_ pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg)
type alias Page_ pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg =
{ toModel : pageModel -> layoutModel
, toMsg : pageMsg -> layoutMsg
, map : (pageMsg -> layoutMsg) -> uiPageMsg -> uiLayoutMsg
}
-> Recipe pageRoute pageModel pageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
{-| Recipe docs
-}
type alias Recipe pageRoute pageModel pageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg =
{ init : pageRoute -> Init layoutModel layoutMsg globalModel globalMsg
, update : pageMsg -> pageModel -> Update layoutModel layoutMsg globalModel globalMsg
, bundle : pageModel -> Bundle layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
}
upgrade :
{ page : Page pageRoute pageModel pageMsg uiPageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
, toModel : pageModel -> layoutModel
, toMsg : pageMsg -> layoutMsg
, map : (pageMsg -> layoutMsg) -> uiPageMsg -> uiLayoutMsg
}
-> Recipe pageRoute pageModel pageMsg layoutModel layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg
upgrade config =
let
(Page page) =
config.page
in
page
{ toModel = config.toModel
, toMsg = config.toMsg
, map = config.map
}
{-| Init docs
-}
type alias Init layoutModel layoutMsg globalModel globalMsg =
{ global : globalModel }
-> ( layoutModel, Cmd layoutMsg, Cmd globalMsg )
{-| Update docs
-}
type alias Update layoutModel layoutMsg globalModel globalMsg =
{ global : globalModel }
-> ( layoutModel, Cmd layoutMsg, Cmd globalMsg )
{-| Bundle docs
-}
type alias Bundle layoutMsg uiLayoutMsg globalModel globalMsg msg uiMsg =
{ global : globalModel
, fromGlobalMsg : globalMsg -> msg
, fromPageMsg : layoutMsg -> msg
, map : (layoutMsg -> msg) -> uiLayoutMsg -> uiMsg
}
->
{ title : String
, view : uiMsg
, subscriptions : Sub msg
}

18
src/Internals/Utils.elm Normal file
View File

@ -0,0 +1,18 @@
module Internals.Utils exposing
( delay
, send
)
import Process
import Task
delay : Int -> msg -> Cmd msg
delay ms msg =
Process.sleep (toFloat ms)
|> Task.perform (\_ -> msg)
send : msg -> Cmd msg
send =
Task.succeed >> Task.perform identity

2
src/README.md Normal file
View File

@ -0,0 +1,2 @@
# ryannhg/elm-app
> the elm package that makes building `elm-spa` super easy!