elm-spa/README.md

485 lines
10 KiB
Markdown
Raw Normal View History

2019-10-05 01:16:02 +03:00
# ryannhg/elm-app
> a way to build single page apps with Elm.
2019-10-05 04:25:25 +03:00
__Note:__ This package is still under design/development. (but one day, it may become an actual package!)
2019-10-05 01:16:02 +03:00
## try it out
```
2019-10-05 04:25:25 +03:00
git clone https://github.com/ryannhg/elm-app
2019-10-05 04:27:57 +03:00
cd elm-app/examples/basic
2019-10-05 04:25:25 +03:00
npm install
npm run dev
2019-10-05 01:16:02 +03:00
```
2019-10-05 04:25:25 +03:00
Mess around with the files in `examples/basic/src` and change things!
## overview for readme scrollers
2019-10-05 01:16:02 +03:00
this package is a wrapper around Elm's `Browser.application`, adding in page transitions and utilities for adding in new pages and routes.
here's what it looks like to use it:
### src/Main.elm
2019-10-05 01:18:12 +03:00
> Uses `Application.create`
2019-10-05 01:16:02 +03:00
2019-10-05 01:18:12 +03:00
This is the entrypoint to the app, it imports a few things:
2019-10-05 01:16:02 +03:00
2019-10-05 01:18:12 +03:00
- `Application` - (this package)
2019-10-05 01:16:02 +03:00
- `App` - the top level `Model`, `Msg`, `init`, `update`, `subscriptions`, and `view`
2019-10-05 01:18:12 +03:00
- `Context` - the shared state between pages.
- `Route` - the routes for your application
- `Flags` - the initial JSON sent into the app
2019-10-05 01:16:02 +03:00
```elm
module Main exposing (main)
import App
2019-10-05 04:25:25 +03:00
import Application exposing (Application)
2019-10-05 01:16:02 +03:00
import Context
import Flags exposing (Flags)
2019-10-05 04:25:25 +03:00
import Route exposing (Route)
2019-10-05 01:16:02 +03:00
main : Application Flags Context.Model Context.Msg App.Model App.Msg
main =
2019-10-05 04:25:25 +03:00
Application.create
{ transition = 200
, context =
{ init = Context.init
, update = Context.update
, view = Context.view
, subscriptions = Context.subscriptions
}
, page =
{ init = App.init
, update = App.update
, bundle = App.bundle
}
, route =
{ fromUrl = Route.fromUrl
, toPath = Route.toPath
}
2019-10-05 01:16:02 +03:00
}
```
### src/Pages/Homepage.elm
2019-10-05 01:18:12 +03:00
> uses `Application.Page.static`
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
The homepage is just a static page, so it's just a `view` and a `title` for the browser tab:
2019-10-05 01:16:02 +03:00
```elm
2019-10-05 04:25:25 +03:00
module Pages.Homepage exposing
( title
, view
)
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
import Html exposing (..)
title : String
title =
"Homepage"
2019-10-05 01:16:02 +03:00
view : Html Never
view =
2019-10-05 04:25:25 +03:00
div []
[ h1 [] [ text "Homepage!" ]
, p [] [ text "It's boring, but it works!" ]
]
2019-10-05 01:16:02 +03:00
```
### src/Pages/Counter.elm
2019-10-05 01:18:12 +03:00
> uses `Application.Page.Sandbox`
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
The counter page doesn't has a `Model` to maintain, so it needs an `init` and an `update`:
2019-10-05 01:16:02 +03:00
```elm
2019-10-05 04:25:25 +03:00
module Pages.Counter exposing
( Model
, Msg
, init
, title
, update
, view
)
2019-10-05 01:16:02 +03:00
import Html exposing (..)
import Html.Events as Events
type alias Model =
2019-10-05 04:25:25 +03:00
{ counter : Int
}
2019-10-05 01:16:02 +03:00
type Msg
2019-10-05 04:25:25 +03:00
= Increment
| Decrement
title : Model -> String
title model =
"Counter: " ++ String.fromInt model.counter ++ " | elm-app"
2019-10-05 01:16:02 +03:00
init : Model
init =
2019-10-05 04:25:25 +03:00
{ counter = 0
}
2019-10-05 01:16:02 +03:00
update : Msg -> Model -> Model
update msg model =
2019-10-05 04:25:25 +03:00
case msg of
Decrement ->
{ model | counter = model.counter - 1 }
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
Increment ->
{ model | counter = model.counter + 1 }
2019-10-05 01:16:02 +03:00
view : Model -> Html Msg
view model =
2019-10-05 04:25:25 +03:00
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 "+" ]
]
]
2019-10-05 01:16:02 +03:00
```
2019-10-05 04:25:25 +03:00
### src/App.elm
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
There's one file where you wire these pages up, and the `Application.Page` module has a bunch of helpers for dealing with pages of different shapes and complexities.
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
(In theory, this file could be generated, but typing it isn't too hard either!)
2019-10-05 01:16:02 +03:00
```elm
2019-10-05 04:25:25 +03:00
module App exposing
2019-10-05 01:16:02 +03:00
( Model
, Msg
2019-10-05 04:25:25 +03:00
, bundle
, init
, update
)
import Application.Page as Page exposing (Context)
import Browser
import Context
import Flags exposing (Flags)
import Html exposing (Html)
import Pages.Counter
import Pages.Homepage
import Pages.NotFound
import Route exposing (Route)
type Model
= HomepageModel ()
| CounterModel Pages.Counter.Model
| NotFoundModel ()
type Msg
= HomepageMsg Never
| CounterMsg Pages.Counter.Msg
| NotFoundMsg Never
pages =
{ homepage =
Page.static
{ title = Pages.Homepage.title
, view = Pages.Homepage.view
, toModel = HomepageModel
}
, counter =
Page.sandbox
{ title = Pages.Counter.title
, init = Pages.Counter.init
, update = Pages.Counter.update
, view = Pages.Counter.view
, toModel = CounterModel
, toMsg = CounterMsg
}
, notFound =
Page.static
{ title = Pages.NotFound.title
, view = Pages.NotFound.view
, toModel = NotFoundModel
}
}
init :
Context Flags Route Context.Model
-> ( Model, Cmd Msg, Cmd Context.Msg )
init context =
case context.route of
Route.Homepage ->
Page.init
{ page = pages.homepage
, context = context
}
Route.Counter ->
Page.init
{ page = pages.counter
, context = context
}
Route.NotFound ->
Page.init
{ page = pages.notFound
, context = context
}
update :
Context Flags Route Context.Model
-> Msg
-> Model
-> ( Model, Cmd Msg, Cmd Context.Msg )
update context appMsg appModel =
case ( appModel, appMsg ) of
( HomepageModel model, HomepageMsg msg ) ->
Page.update
{ page = pages.homepage
, msg = msg
, model = model
, context = context
}
( HomepageModel _, _ ) ->
( appModel
, Cmd.none
, Cmd.none
)
( CounterModel model, CounterMsg msg ) ->
Page.update
{ page = pages.counter
, msg = msg
, model = model
, context = context
}
( CounterModel _, _ ) ->
( appModel
, Cmd.none
, Cmd.none
)
( NotFoundModel model, NotFoundMsg msg ) ->
Page.update
{ page = pages.notFound
, msg = msg
, model = model
, context = context
}
( NotFoundModel _, _ ) ->
( appModel
, Cmd.none
, Cmd.none
)
bundle :
Context Flags Route Context.Model
-> Model
-> Page.Bundle Msg
bundle context appModel =
case appModel of
HomepageModel model ->
Page.bundle
{ page = pages.homepage
, model = model
, context = context
}
CounterModel model ->
Page.bundle
{ page = pages.counter
, model = model
, context = context
}
NotFoundModel model ->
Page.bundle
{ page = pages.notFound
, model = model
, context = context
}
```
### src/Context.elm
This is the "component" that wraps your whole single page app.
It's `update` function has access to `navigateTo`, allowing page navigation.
The `view` function also has access to your page, so you can insert it where you like in the layout!
```elm
module Context exposing
( Model
, Msg(..)
2019-10-05 01:16:02 +03:00
, init
, subscriptions
, update
, view
)
2019-10-05 04:25:25 +03:00
import Application
import Data.User exposing (User)
2019-10-05 01:16:02 +03:00
import Flags exposing (Flags)
import Html exposing (..)
2019-10-05 04:25:25 +03:00
import Html.Attributes as Attr exposing (class)
2019-10-05 01:16:02 +03:00
import Html.Events as Events
2019-10-05 04:25:25 +03:00
import Route exposing (Route)
import Utils.Cmd
2019-10-05 01:16:02 +03:00
type alias Model =
2019-10-05 04:25:25 +03:00
{ user : Maybe User
}
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
type Msg
= SignIn (Result String User)
| SignOut
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
init : Route -> Flags -> ( Model, Cmd Msg )
init route flags =
( { user = Nothing }
, Cmd.none
)
2019-10-05 01:16:02 +03:00
2019-10-05 04:25:25 +03:00
update :
Application.Messages Route msg
-> Route
-> Msg
-> Model
-> ( Model, Cmd Msg, Cmd msg )
update { navigateTo } route msg model =
case msg of
SignIn (Ok user) ->
( { model | user = Just user }
, Cmd.none
, navigateTo Route.Homepage
)
SignIn (Err _) ->
Utils.Cmd.pure model
SignOut ->
Utils.Cmd.pure { model | user = Nothing }
view :
{ route : Route
, context : Model
, toMsg : Msg -> msg
, viewPage : Html msg
}
-> Html msg
view { context, route, toMsg, viewPage } =
div [ class "layout" ]
[ Html.map toMsg (viewNavbar route context)
, div [ class "container" ] [ viewPage ]
, Html.map toMsg (viewFooter context)
]
viewNavbar : Route -> Model -> Html Msg
viewNavbar currentRoute model =
header [ class "navbar" ]
[ div [ class "navbar__links" ]
(List.map
(viewLink currentRoute)
[ Route.Homepage, Route.Counter, Route.Random ]
)
, case model.user of
Just _ ->
button [ Events.onClick SignOut ] [ text <| "Sign out" ]
2019-10-05 01:16:02 +03:00
Nothing ->
2019-10-05 04:25:25 +03:00
a [ Attr.href "/sign-in" ] [ text "Sign in" ]
]
viewLink : Route -> Route -> Html msg
viewLink currentRoute route =
a
[ class "navbar__link-item"
, Attr.href (Route.toPath route)
, Attr.style "font-weight"
(if route == currentRoute then
"bold"
else
"normal"
)
]
[ text (linkLabel route) ]
linkLabel : Route -> String
linkLabel route =
case route of
Route.Homepage ->
"Home"
Route.Counter ->
"Counter"
Route.SignIn ->
"Sign In"
Route.Random ->
"Random"
Route.NotFound ->
"Not found"
viewFooter : Model -> Html Msg
viewFooter model =
footer [ Attr.class "footer" ]
[ model.user
|> Maybe.map Data.User.username
|> Maybe.withDefault "not signed in"
|> (++) "Current user: "
|> text
]
subscriptions : Route -> Model -> Sub Msg
subscriptions route model =
Sub.none
```
## Still reading?
Oh wow. Maybe you should just check out the [basic example](./examples/basic) included in the repo.
Just clone, `npm install` and `npm run dev` for a hot-reloading magical environment.
Add a page or something- and let me know how it goes!