hey man the examples are up

This commit is contained in:
Ryan Haskell-Glatz 2021-04-27 01:36:07 -05:00
parent f757b10371
commit 8553d14b85
11 changed files with 510 additions and 61 deletions

View File

@ -1,31 +1,38 @@
# Examples
Prefer to learn by example? Wonderful! The source code for all of the examples on this site can be found in the Github repo's [examples](https://github.com/ryannhg/elm-spa/tree/main/examples) folder.
Prefer to learn by example? Wonderful! The source code for all of the examples on this site can be found in the GitHub repo's [examples](https://github.com/ryannhg/elm-spa/tree/main/examples) folder.
### Hello, world!
Get an introduction to the framework with a simple app.
[![Hello, world!](/content/images/01-hello-world.png)](/examples/01-hello-world)
[![Example 1 screenshot](/content/images/01-hello-world.png)](/examples/01-hello-world)
### Pages
Learn how pages and URL routing work together.
[![Hello, world!](/content/images/02-pages.png)](/examples/02-pages)
[![Example 2 screenshot](/content/images/02-pages.png)](/examples/02-pages)
### Local storage
Use ports and local storage to persist data on refresh.
[![Hello, world!](/content/images/03-storage.png)](/examples/03-storage)
[![Example 3 screenshot](/content/images/03-storage.png)](/examples/03-storage)
### User authentication
Explore the elm-spa's user authentication API.
[![Hello, world!](/content/images/04-authentication.png)](/examples/04-authentication)
[![Example 4 screenshot](/content/images/04-authentication.png)](/examples/04-authentication)
## Realworld example
Implements the [Realworld app project](), inspired by Richard Feldman's "elm-spa-example" project.
[![Realworld app screenshot](/content/images/realworld.png)](https://realworld.elm-spa.dev)
Source code: [GitHub](https://github.com/ryannhg/elm-spa-realworld)
## More examples

View File

@ -1,5 +1,7 @@
# Hello, world!
__Source code__: [GitHub](https://github.com/ryannhg/elm-spa/tree/main/examples/01-hello-world)
Welcome to __elm-spa__! This guide is a breakdown of the simplest project you can make: the "Hello, world!" example.
### Installation
@ -134,3 +136,7 @@ elm-spa build
This command will also minify your `/dist/elm.js` file so it's production ready.
---
__Next up:__ [Pages](./02-pages)

View File

@ -1,8 +1,9 @@
# Pages & routing
__Source code__: [GitHub](https://github.com/ryannhg/elm-spa/tree/main/examples/01-pages)
This next guide will show you how pages, routing, and the `elm-spa add` command work together to automatically handle URLs in your __elm-spa__ application.
> You can see the source code in the [examples](https://github.com/ryannhg/elm-spa/tree/next/examples/02-pages) folder on GitHub.
### The setup
@ -202,3 +203,7 @@ After creating `style.css`, we can import the file in our `public/index.html` en
Using the `<link>` tag as shown above (with the leading slash!) imports our CSS file. All files in the `public` folder are available at the root of our web application. That means a file stored at `public/images/dog.png` would be at `http://localhost:1234/images/dog`, without including `public` in the URL at all.
---
__Next up:__ [Storage](./03-storage)

View File

@ -1,3 +1,394 @@
# Storage
> Coming soon!
__Source code__: [GitHub](https://github.com/ryannhg/elm-spa/tree/main/examples/03-storage)
Let's start by creating a new project with the __elm-spa__ CLI:
```terminal
elm-spa new
```
## Creating a stateful page
Let's create a simple interactive app, based on the official Elm [counter example](https://elm-lang.org/examples/buttons). The `elm-spa add` command will make this a breeze:
```terminal
elm-spa add / sandbox
```
This will stub out the `init`, `update`, and `view` function for us, and wire them together with `Page.static` like this:
```elm
-- src/Pages/Home_.elm
page : Shared.Model -> Request -> Page.With Model Msg
page =
Page.static
{ init = init
, update = update
, view = view
}
```
Let's add in the implementation from the counter example to get a working app!
### init
```elm
-- src/Pages/Home_.elm
type alias Model =
{ counter : Int
}
init : Model
init =
{ counter = 0
}
```
### update
```elm
-- src/Pages/Home_.elm
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | counter = model.counter + 1 }
Decrement ->
{ model | counter = model.counter - 1 }
```
### view
```elm
-- src/Pages/Home_.elm
view : Model -> View Msg
view model =
{ title = "Homepage"
, body =
[ Html.h1 [] [ Html.text "Local storage" ]
, Html.button [ Html.Events.onClick Increment ] [ Html.text "+" ]
, Html.p [] [ Html.text ("Count: " ++ String.fromInt model.counter) ]
, Html.button [ Html.Events.onClick Decrement ] [ Html.text "-" ]
]
}
```
After these functions are in place, we can spin up our server with the __elm-spa__ CLI:
```terminal
elm-spa server
```
And this is what we should see at [http://localhost:1234](http://localhost:1234):
![counter app](/content/images/03-storage.png)
### Playing with the counter
As we click the "+" and "-" buttons, the counter value should be working great. When we __refresh__ the page, the counter value is 0 again.
Let's use local storage to keep the counter value around!
## The JS side
To do this, we'll be using [flags and ports](https://guide.elm-lang.org/interop/ports.html), a typesafe way to work with JavaScript without causing runtime errors in our Elm application!
Let's edit `public/index.html` as a starting point:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<script src="/dist/elm.js"></script>
<!-- EDIT THIS LINE -->
<script src="/main.js"></script>
</body>
</html>
```
Here we replace the inline `Elm.Main.init()` script generated by the `elm-spa new` command with a reference to a new file we'll create in `public/main.js`
```js
// public/main.js
const app = Elm.Main.init()
// ...
```
At this point, nothing has changed yet, but we now have access to `app` which will allow us to interact with our Elm app from the JS file!
Let's add in some ports like this:
```js
// public/main.js
const app = Elm.Main.init({
flags: JSON.parse(localStorage.getItem('storage'))
})
app.ports.save.subscribe(storage => {
localStorage.setItem('storage', JSON.stringify(storage))
app.ports.load.send(storage)
})
```
This JS code is doing a few things:
1. __When our Elm app starts up,__ we pass in the current value of `localStorage` via flags. Initially, this will pass in `null`, because no data has been stored yet.
2. We subscribe to the `save` port for events __from Elm__, which we'll wire up on the Elm side shortly.
3. When Elm sends a `save` event, we'll store the data in localStorage (making it ready for the next time the app starts up!) as well as send a message back to Elm via the `load` port.
## The Elm side
None of this code is working yet, because we need to define these `save` and `load` ports on the Elm side too!
Let's create a new file at `src/Storage.elm` that defines the ports referenced on the JS side:
```elm
port module Storage exposing (..)
import Json.Decode as Json
port save : Json.Value -> Cmd msg
port load : (Json.Value -> msg) -> Sub msg
```
Above, we've created a `port module` that defines our `save` and `load` ports. Next, we'll describe the data we want to store, as well as how to convert it to and from JSON:
```elm
port module Storage exposing (..)
import Json.Encode as Encode
-- ... port definitions from before ...
type alias Storage =
{ counter : Int
}
-- Converting to JSON
toJson : Storage -> Json.Value
toJson storage =
Encode.object
[ ( "counter", Encode.int storage.counter )
]
-- Converting from JSON
fromJson : Json.Value -> Storage
fromJson value =
value
|> Json.decodeValue decoder
|> Result.withDefault initial
decoder : Json.Decoder Storage
decoder =
Json.map Storage
(Json.field "counter" Json.int)
initial : Storage
initial =
{ counter = 0
}
```
If this decoder stuff is new to you, please check out the [JSON section of the Elm guide](https://guide.elm-lang.org/effects/json.html). It will lay a solid foundation for understanding decoders and encode functions!
### Sending data to JS
For this example, we're going to define `increment` and `decrement` as side-effects because they change the state of the world. We'll be using the `save` port to send these events to JS:
```elm
-- src/Storage.elm
increment : Storage -> Cmd msg
increment storage =
{ storage | counter = storage.counter + 1 }
|> toJson
|> save
decrement : Storage -> Cmd msg
decrement storage =
{ storage | counter = storage.counter - 1 }
|> toJson
|> save
```
This should look pretty similar to how our homepage handled the `Increment` and `Decrement` messages, but this time we use `toJson` and `save` to send an event for JS to handle.
( As a last step, we'll revisit `Home_.elm` and swap out the old behavior with the new )
### Listening for data from JS
We're going to add one final function to `Storage.elm` that will allow us to subscribe to events from the `load` port, that use's our `fromJson` function to safely parse the message we get back:
```elm
onChange : (Storage -> msg) -> Sub msg
onChange fromStorage =
load (\json -> fromJson json |> fromStorage)
```
Here, the `onChange` function will allow the outside world to handle the `load` event without having to deal with raw JSON values by hand.
That's it for this file- now we're ready to use our `Storage` module in our app!
### Wiring up the shared state
Let's eject `Shared.elm` by moving it from `.elm-spa/defaults` into our `src` folder. This will allow us to make local changes to it, as explained in the [shared state section](/guides/05-shared-state) of the guide.
Our first step is to add `Storage` to our `Shared.Model`, so we can access `storage` from _any_ page in our application:
```elm
-- src/Shared.elm
import Storage
type alias Model =
{ storage : Storage
}
```
The `Shared.init` function is the __only place__ we have access to `Flags`, which is how JS passed in our initial value earlier. We can use `Storage.fromJson` to convert that raw JSON into our nice `Storage` type.
```elm
-- src/Shared.elm
init : Request -> Flags -> ( Model, Cmd Msg )
init _ flags =
( { storage = Storage.fromJson flags }
, Cmd.none
)
```
Now let's listen for those `load` events from JS, so we can update the `Shared.Model` as soon as we get them. This code will use the `Storage.onChange` function we made to send a `Shared.Msg` to our `Shared.update` function:
```elm
-- src/Shared.elm
subscriptions : Request -> Model -> Sub Msg
subscriptions _ _ =
Storage.onChange StorageUpdated
type Msg
= StorageUpdated Storage
update : Request -> Msg -> Model -> ( Model, Cmd Msg )
update _ msg model =
case msg of
StorageUpdated storage ->
( { model | storage = storage }
, Cmd.none
)
```
That's all for `src/Shared.elm`. The last step is to upgrade our homepage to send side-effects instead of changing the data locally.
### Upgrading Home_.elm
To gain access to `Cmd msg`, we'll start by using `Page.element` instead of `Page.static`. The signature of our `init` and `update` functions will need to change to handle the new capabilities:
Our `Model` no longer needs to track the state of the application. This means the `Home_.init` function won't be doing much at all:
```elm
-- src/Pages/Home_.elm
type alias Model =
{}
init : ( Model, Cmd Msg )
init =
( {}, Cmd.none )
```
This time around, the `update` function will need access to the current `Storage` value and use `Storage.increment` and `Storage.decrement` to send commands to the JS side.
```elm
-- src/Pages/Home_.elm
type Msg
= Increment
| Decrement
update : Storage -> Msg -> Model -> ( Model, Cmd Msg )
update storage msg model =
case msg of
Increment ->
( model
, Storage.increment storage
)
Decrement ->
( model
, Storage.decrement storage
)
```
When the `load` event comes in from JS, it triggers our `Storage.onChange` subscription. This updates the `storage` for us, meaning the `storage.counter` we get in our `view` will be the latest counter value.
```elm
-- src/Pages/Home_.elm
view : Storage -> Model -> View Msg
view storage _ =
{ title = "Homepage"
, body =
[ Html.h1 [] [ Html.text "Local storage" ]
, Html.button [ Html.Events.onClick Increment ] [ Html.text "+" ]
, Html.p [] [ Html.text ("Count: " ++ String.fromInt storage.counter) ]
, Html.button [ Html.Events.onClick Decrement ] [ Html.text "-" ]
]
}
```
We can use `Page.element` to wire all these things up, and even pass `Storage` into our `view` and `update` functions, which depend on the current value to do their thing:
```elm
-- src/Pages/Home_.elm
page : Shared.Model -> Request -> Page.With Model Msg
page shared _ =
Page.element
{ init = init
, update = update shared.storage
, view = view shared.storage
, subscriptions = \_ -> Sub.none
}
```
> Here, I've stubbed out `subscriptions` with an inline function, we won't be needing it, because Shared.subscriptions listens to `Storage.onChange` for us.
#### Hooray!
In the browser, we now have a working counter app that persists on refresh. Even if you close the browser and open it up again, you'll see your previous counter value on the screen.
As a reminder, all the source code for this example is available on [GitHub](https://github.com/ryannhg/elm-spa/tree/main/examples/03-storage)
---
__Next up:__ [User Authentication](./04-authentication)

View File

@ -1,5 +1,7 @@
# User authentication
__Source code__: [GitHub](https://github.com/ryannhg/elm-spa/tree/main/examples/04-authentication)
In a real world application, it's common to have the notion of a signed-in users. When it comes to routing, it's often useful to only allow signed-in users to visit specific pages.
It would be wonderful if we could define logic in _one place_ that guarantees only signed-in users could view those pages:
@ -380,6 +382,30 @@ view user model =
Now everything is working! Visiting the `/sign-in` page and clicking "Sign In" signs in the user and redirects to the homepage. Clicking "Sign out" on the homepage signs out the user, and our `Auth.elm` logic automatically redirects to the `SignIn` page.
#### But wait...
## Persisting the user
When we refresh the page, the user is signed out... how can we keep them signed in after refresh? Sounds like a job for [local storage](/examples/local-storage)!
When we refresh the page, the user is signed out... how can we keep them signed in after refresh? Let's tweak the `Storage.elm` file we made in the [last example](./03-storage):
```elm
-- src/Storage.elm
type alias Storage =
{ user : Maybe User
}
```
If we store this on the `Shared.Model` we can ensure the user is still signed in after they refresh their browser, or visit the app later.
```elm
-- src/Shared.elm
type alias Model =
{ storage : Storage
}
```
For more explanation of how this works, check out the [Storage example](./03-storage) in the last section, it will give a better basic understanding of how this mechanism works!
---
__Next up:__ [More examples](/examples#more-examples)

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

View File

@ -2,7 +2,7 @@ const app = Elm.Main.init({
flags: JSON.parse(localStorage.getItem('storage'))
})
app.ports.save_.subscribe(storage => {
app.ports.save.subscribe(storage => {
localStorage.setItem('storage', JSON.stringify(storage))
app.ports.load_.send(storage)
app.ports.load.send(storage)
})

View File

@ -1,18 +1,16 @@
module Pages.Home_ exposing (Model, Msg, init, page, update, view)
import Gen.Params.Home_ exposing (Params)
import Html exposing (Html)
import Html
import Html.Events
import Page
import Ports
import Request
import Request exposing (Request)
import Shared
import Storage exposing (Storage)
import View exposing (View)
page : Shared.Model -> Request.With Params -> Page.With Model Msg
page shared req =
page : Shared.Model -> Request -> Page.With Model Msg
page shared _ =
Page.element
{ init = init
, update = update shared.storage
@ -48,12 +46,12 @@ update storage msg model =
case msg of
Increment ->
( model
, Ports.save (Storage.increment storage)
, Storage.increment storage
)
Decrement ->
( model
, Ports.save (Storage.decrement storage)
, Storage.decrement storage
)
@ -62,7 +60,7 @@ update storage msg model =
subscriptions : Model -> Sub Msg
subscriptions model =
subscriptions _ =
Sub.none
@ -71,7 +69,7 @@ subscriptions model =
view : Storage -> Model -> View Msg
view storage model =
view storage _ =
{ title = "Homepage"
, body =
[ Html.h1 [] [ Html.text "Local storage" ]

View File

@ -1,20 +0,0 @@
port module Ports exposing (load, save)
import Json.Decode as Json
import Storage exposing (Storage)
save : Storage -> Cmd msg
save =
Storage.save >> save_
load : (Storage -> msg) -> Sub msg
load fromStorage =
load_ (\json -> Storage.load json |> fromStorage)
port save_ : Json.Value -> Cmd msg
port load_ : (Json.Value -> msg) -> Sub msg

View File

@ -8,7 +8,6 @@ module Shared exposing
)
import Json.Decode as Json
import Ports
import Request exposing (Request)
import Storage exposing (Storage)
@ -24,7 +23,7 @@ type alias Model =
init : Request -> Flags -> ( Model, Cmd Msg )
init _ flags =
( { storage = Storage.load flags }
( { storage = Storage.fromJson flags }
, Cmd.none
)
@ -37,9 +36,11 @@ update : Request -> Msg -> Model -> ( Model, Cmd Msg )
update _ msg model =
case msg of
StorageUpdated storage ->
( { model | storage = storage }, Cmd.none )
( { model | storage = storage }
, Cmd.none
)
subscriptions : Request -> Model -> Sub Msg
subscriptions _ _ =
Ports.load StorageUpdated
Storage.onChange StorageUpdated

View File

@ -1,11 +1,11 @@
module Storage exposing
( Storage, save, load
port module Storage exposing
( Storage, fromJson, onChange
, increment, decrement
)
{-|
@docs Storage, save, load
@docs Storage, fromJson, onChange
@docs increment, decrement
-}
@ -14,13 +14,42 @@ import Json.Decode as Json
import Json.Encode as Encode
-- PORTS
port save : Json.Value -> Cmd msg
port load : (Json.Value -> msg) -> Sub msg
-- STORAGE
type alias Storage =
{ counter : Int
}
load : Json.Value -> Storage
load json =
-- Converting to JSON
toJson : Storage -> Json.Value
toJson storage =
Encode.object
[ ( "counter", Encode.int storage.counter )
]
-- Converting from JSON
fromJson : Json.Value -> Storage
fromJson json =
json
|> Json.decodeValue decoder
|> Result.withDefault init
@ -38,22 +67,28 @@ decoder =
(Json.field "counter" Json.int)
save : Storage -> Json.Value
save storage =
Encode.object
[ ( "counter", Encode.int storage.counter )
]
-- Updating storage
-- UPDATING STORAGE
increment : Storage -> Storage
increment : Storage -> Cmd msg
increment storage =
{ storage | counter = storage.counter + 1 }
|> toJson
|> save
decrement : Storage -> Storage
decrement : Storage -> Cmd msg
decrement storage =
{ storage | counter = storage.counter - 1 }
|> toJson
|> save
-- LISTENING FOR STORAGE UPDATES
onChange : (Storage -> msg) -> Sub msg
onChange fromStorage =
load (\json -> fromJson json |> fromStorage)