mirror of
https://github.com/ryan-haskell/elm-spa.git
synced 2024-09-11 12:35:44 +03:00
Merge branch 'next' of github.com:ryannhg/elm-spa into next
This commit is contained in:
commit
1ee15ed79c
@ -1,4 +1,4 @@
|
||||
# [![elm-spa](https://v6-elm-spa.netlify.app/images/outlined-to-edge.png)](https://elm-spa.dev)
|
||||
# [![elm-spa](https://next.elm-spa.dev/images/rounded-logo-bg.png)](https://elm-spa.dev)
|
||||
|
||||
# **Installation**
|
||||
|
||||
|
@ -1,6 +1,13 @@
|
||||
[build]
|
||||
publish = "public"
|
||||
command = "npm i elm-spa@beta && node scripts/generate-index.js && npx elm-spa build"
|
||||
command = "npm i elm elm-spa@beta && node scripts/generate-index.js && npx elm-spa build"
|
||||
|
||||
# Prevents missing markdown files from redirecting to index.html
|
||||
[[redirects]]
|
||||
from = "/content/*"
|
||||
to = "/content/:splat"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Guide
|
||||
# Docs
|
||||
|
||||
Welcome to __elm-spa__, a framework for building web applications with [Elm](https://elm-lang.org)!
|
||||
If you are new to Elm, you should check out [the official guide](https://guide.elm-lang.org), which
|
||||
@ -44,7 +44,7 @@ npx elm-spa server
|
||||
So far, we've used [npx](https://www.npmjs.com/package/npx) so we don't need to install __elm-spa__ directly. If you'd like to run commands from the terminal, without the `npx` prefix, you can install __elm-spa__ like this:
|
||||
|
||||
```terminal
|
||||
npm install -g elm-spa@next
|
||||
npm install -g elm-spa@latest
|
||||
```
|
||||
|
||||
You can verify the install succeeded by running `elm-spa help` from your terminal:
|
||||
@ -61,11 +61,11 @@ elm-spa build . . . . . one-time production build
|
||||
elm-spa watch . . . . . . runs build as you code
|
||||
elm-spa server . . . . . start a live dev server
|
||||
|
||||
Visit https://next.elm-spa.dev for more!
|
||||
Visit https://elm-spa.dev for more!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
__Ready for more?__
|
||||
|
||||
Let's check out [the CLI](/guide/cli) to learn more about those five commands!
|
||||
Let's check out [the CLI](/docs/cli) to learn more about those five commands!
|
@ -3,16 +3,16 @@
|
||||
At the end of the last section, we installed the __elm-spa__ CLI using [npm](https://npmjs.org) like this:
|
||||
|
||||
```terminal
|
||||
npm install -g elm-spa@next
|
||||
npm install -g elm-spa@latest
|
||||
```
|
||||
|
||||
This gave us the ability to run a few commands:
|
||||
|
||||
1. __[`elm-spa new`](#elm-spa-new)__ - creates a new project
|
||||
1. __[`elm-spa server`](#elm-spa-server)__ - runs a dev server as you code
|
||||
1. __[`elm-spa watch`](#elm-spa-watch)__ - builds as you code
|
||||
1. __[`elm-spa build`](#elm-spa-build)__ - one-time production build
|
||||
1. __[`elm-spa add`](#elm-spa-add)__ - adds a page to an existing project
|
||||
1. [__elm-spa new__](#elm-spa-new) - creates a new project
|
||||
1. [__elm-spa server__](#elm-spa-server) - runs a dev server as you code
|
||||
1. [__elm-spa watch__](#elm-spa-watch) - builds as you code
|
||||
1. [__elm-spa build__](#elm-spa-build) - one-time production build
|
||||
1. [__elm-spa add__](#elm-spa-add) - adds a page to an existing project
|
||||
|
||||
What do these do? This section of the guide dives into more detail on each command!
|
||||
|
||||
@ -57,7 +57,7 @@ If you want the automatic compilation on change, but don't need a HTTP server, y
|
||||
elm-spa watch
|
||||
```
|
||||
|
||||
This will automatically generate and compile on save, but without the server. This is a great choice when combining __elm-spa__ with another tool like [Parcel](https://parceljs.org/elm.html)
|
||||
This will automatically generate code and compile your Elm files on save, but without the server. This is a great command to combine __elm-spa__ with another tool like [Parcel](https://parceljs.org/elm.html).
|
||||
|
||||
|
||||
## elm-spa build
|
||||
@ -103,9 +103,3 @@ elm-spa add /example static
|
||||
elm-spa add /example sandbox
|
||||
elm-spa add /example element
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
__So, what's a page?__
|
||||
|
||||
Let's answer that next in the [pages section](./pages)!
|
253
docs/public/content/docs/pages.md
Normal file
253
docs/public/content/docs/pages.md
Normal file
@ -0,0 +1,253 @@
|
||||
# Pages
|
||||
|
||||
In __elm-spa__, every URL connects to a single page. Let's take a closer look at the homepage we created earlier with the `elm-spa new` command:
|
||||
|
||||
```elm
|
||||
module Pages.Home_ exposing (view)
|
||||
|
||||
import Html
|
||||
import View exposing (View)
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
}
|
||||
```
|
||||
|
||||
This homepage renders __"Homepage"__ in the browser tab, and __"Hello, world!"__ onto the page.
|
||||
|
||||
Because the file is named `Home_.elm`, we know it's the homepage. Visiting `http://localhost:1234` in a web browser will render the page.
|
||||
|
||||
A `view` function is perfect when all you need is to render some HTML on the screen. But many web pages in the real world do more interesting things!
|
||||
|
||||
### Upgrading "Hello, world!"
|
||||
|
||||
Let's start by adding a `page` function, the first step in our journey from "Hello, world!" to the real world:
|
||||
|
||||
```elm
|
||||
module Pages.Home_ exposing (page)
|
||||
|
||||
import Html
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
|
||||
page : Shared.Model -> Request -> Page
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view
|
||||
}
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
}
|
||||
```
|
||||
|
||||
We haven't changed our original code much- except we've added a new `page` function that:
|
||||
|
||||
1. Accepts 2 inputs: `Shared.Model` and `Request`
|
||||
2. Returns a `Page` value
|
||||
3. Has been __exposed__ at the top of the file.
|
||||
|
||||
> Exposing `page` from this module lets __elm-spa__ know to use it instead of the plain `view` function from before.
|
||||
|
||||
The `view` function we had before is passed into `page`, so our user still sees __"Hello, world!"__ when they visit the homepage. However, this page now has access to two new bits of information!
|
||||
|
||||
1. `Shared.Model` is our global application state, which might contain the signed-in user, settings, or other things that should persist as we move from one page to another.
|
||||
2. `Request` is a record with access to the current route, query parameters, and any other information about the current URL.
|
||||
|
||||
You can rely on the fact that the `page` will always be passed the latest `Shared.Model` and `Request` value. If we want either of these values to be available in our `view` function, we can pass them in:
|
||||
|
||||
```elm
|
||||
page : Shared.Model -> Request -> Page
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view req -- passing in req here!
|
||||
}
|
||||
```
|
||||
|
||||
Now our `view` function can read the current `URL` value:
|
||||
|
||||
```elm
|
||||
view : Request -> View msg
|
||||
view req =
|
||||
{ title = "Homepage"
|
||||
, body =
|
||||
[ Html.text ("Hello, " ++ req.url.host ++ "!")
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If we are running `elm-spa server`, this will print __"Hello, localhost!"__ on our screen.
|
||||
|
||||
### Beyond static pages
|
||||
|
||||
You might have noticed `Page.static` earlier in our page function. This is one of the built in __page types__ that is built-in to __elm-spa__.
|
||||
|
||||
The rest of this section will introduce you to the other __page types__ exposed by the `Page` module, so you know which one to reach for.
|
||||
|
||||
> Always choose the __simplest__ page type for the job– and reach for the more advanced ones when your page needs the extra features!
|
||||
|
||||
- __[Page.static](#pagestatic)__ - for pages that only render a view.
|
||||
- __[Page.sandbox](#pagesandbox)__ - for pages that need to keep track of state.
|
||||
- __[Page.element](#pageelement)__ - for pages that send HTTP requests or continually listen for events from the browser or user.
|
||||
- __[Page.advanced](#pageadvanced)__ - for pages that need to sign in a user or work with other details that should persist between page navigation.
|
||||
|
||||
|
||||
## Page.static
|
||||
|
||||
```terminal
|
||||
elm-spa add /example static
|
||||
```
|
||||
|
||||
This was the page type we took a look at earlier, perfect for pages that render static HTML, but might need access to the `Shared.Model` or `Request` values.
|
||||
|
||||
```elm
|
||||
module Pages.Example exposing (page)
|
||||
|
||||
|
||||
page : Shared.Model -> Request -> Page
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view
|
||||
}
|
||||
|
||||
|
||||
view : View msg
|
||||
```
|
||||
|
||||
|
||||
## Page.sandbox
|
||||
|
||||
```terminal
|
||||
elm-spa add /example sandbox
|
||||
```
|
||||
|
||||
This is the first __page type__ that introduces [the Elm architecture](https://guide.elm-lang.org/architecture/), which uses `Model` to store the current page state and `Msg` to define what actions users can take on this page.
|
||||
|
||||
It's time to upgrade to `Page.sandbox` when you need to track state on the page. Here are a few examples of things you'd store in page state:
|
||||
|
||||
- The current slide of a carousel
|
||||
- The selected tab section to view
|
||||
- The open / close state of a modal
|
||||
|
||||
All these examples require us to be able to __initialize__ a `Model`, __update__ it based on `Msg` values sent from the __view__.
|
||||
|
||||
If you are new to the Elm architecture, be sure to visit [guide.elm-lang.org](https://guide.elm-lang.org/architecture/). We'll be using it for all the upcoming page types!
|
||||
|
||||
|
||||
```elm
|
||||
module Pages.Example exposing (Model, Msg, page)
|
||||
|
||||
|
||||
page : Shared.Model -> Request -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.sandbox
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
}
|
||||
|
||||
|
||||
init : Model
|
||||
update : Msg -> Model -> Model
|
||||
view : Model -> View Msg
|
||||
```
|
||||
|
||||
> Our `page` function now returns `Page.With Model Msg` instead of `Page`. This is because our page is now __stateful__.
|
||||
|
||||
_( Inspired by [__Browser.sandbox__](https://package.elm-lang.org/packages/elm/browser/latest/Browser#sandbox) )_
|
||||
|
||||
## Page.element
|
||||
|
||||
```terminal
|
||||
elm-spa add /example element
|
||||
```
|
||||
|
||||
When you are ready to send __HTTP requests__ or __subscribe to events__ like keyboard presses, mouse move, or incoming data from JS– upgrade to `Page.element`.
|
||||
|
||||
This is the same as `Page.sandbox`, but introduces `Cmd Msg` and `Sub Msg` to handle side effects.
|
||||
|
||||
```elm
|
||||
module Pages.Example exposing (Model, Msg, page)
|
||||
|
||||
|
||||
page : Shared.Model -> Request -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.element
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
init : ( Model, Cmd Msg )
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
view : Model -> View Msg
|
||||
subscriptions : Model -> Sub Msg
|
||||
```
|
||||
|
||||
_( Inspired by [__Browser.element__](https://package.elm-lang.org/packages/elm/browser/latest/Browser#element) )_
|
||||
|
||||
## Page.advanced
|
||||
|
||||
```terminal
|
||||
elm-spa add /example advanced
|
||||
```
|
||||
|
||||
For many applications, `Page.element` is all you need to store a `Model`, handle `Msg` values, and work with side-effects.
|
||||
|
||||
Some Elm users prefer sending global updates directly from their pages, so we've included this `Page.advanced` page type.
|
||||
|
||||
Using a custom `Effect` module, users are able to send `Cmd Msg` value via `Effect.fromCmd` or `Shared.Msg` values with `Effect.fromSharedMsg`.
|
||||
|
||||
|
||||
```elm
|
||||
module Pages.Example exposing (Model, Msg, page)
|
||||
|
||||
|
||||
page : Shared.Model -> Request -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.advanced
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
init : ( Model, Effect Msg )
|
||||
update : Msg -> Model -> ( Model, Effect Msg )
|
||||
view : Model -> View Msg
|
||||
subscriptions : Model -> Sub Msg
|
||||
```
|
||||
|
||||
This `Effect Msg` value also allows support for folks using [elm-program-test](https://package.elm-lang.org/packages/avh4/elm-program-test/latest/), which requires users to define their own custom type on top of `Cmd Msg`. More about that in the [testing guide](/guides/06-testing)
|
||||
|
||||
|
||||
## Page.protected
|
||||
|
||||
Each of those four __page types__ also have a __protected__ version. This means pages are guaranteed to receive a `User` or redirect if no user is signed in.
|
||||
|
||||
```elm
|
||||
-- not protected
|
||||
Page.sandbox
|
||||
{ init : Model
|
||||
, update : Msg -> Model -> Model
|
||||
, view : Model -> View Msg
|
||||
}
|
||||
|
||||
-- protected
|
||||
Page.protected.sandbox
|
||||
{ init : User -> Model
|
||||
, update : User -> Msg -> Model -> Model
|
||||
, view : User -> Model -> View Msg
|
||||
}
|
||||
```
|
||||
|
||||
When you are ready for user authentication, you can learn more about using `Page.protected` in the [authentication guide](/guides/04-authentication).
|
130
docs/public/content/docs/routing.md
Normal file
130
docs/public/content/docs/routing.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Routing
|
||||
|
||||
One of the best features in __elm-spa__ is the automatic routing system! Inspired by popular JS frameworks, the names of your files determine the routes in your application.
|
||||
|
||||
Every __elm-spa__ project will have a `src/Pages` folder containing all the pages in your app:
|
||||
|
||||
URL | File
|
||||
--- | ---
|
||||
`/` | `src/Pages/Home_.elm`
|
||||
`/people` | `src/Pages/People.elm`
|
||||
`/people/:name` | `src/Pages/People/Name_.elm`
|
||||
`/about-us` | `src/Pages/AboutUs.elm`
|
||||
`/settings/users` | `src/Pages/Settings/Users.elm`
|
||||
|
||||
In this section, we'll cover the different kinds of routes you'll find in every __elm-spa__ application.
|
||||
|
||||
## The homepage
|
||||
|
||||
`Home_.elm` is a reserved filename that handles requests to your homepage. The easiest way to add a new homepage is with the [`elm-spa add`](/docs/cli#elm-spa-add) covered in the CLI docs:
|
||||
|
||||
```terminal
|
||||
elm-spa add /
|
||||
```
|
||||
|
||||
> `Home.elm` (without the underscore) is seen as a route to `/home`! To handle requests to the homepage, make sure to include the trailing underscore.
|
||||
|
||||
## Static routes
|
||||
|
||||
Most pages will be __static routes__, meaning the filepath will translate to a single URL.
|
||||
|
||||
```terminal
|
||||
elm-spa add /people
|
||||
```
|
||||
|
||||
This command creates a file called `People.elm` that will be shown when the user visits `/people` in your application.
|
||||
|
||||
These are a few more examples of static routes:
|
||||
|
||||
URL | File
|
||||
--- | ---
|
||||
`/dashboard` | `src/Pages/Dashboard.elm`
|
||||
`/people` | `src/Pages/People.elm`
|
||||
`/about-us` | `src/Pages/AboutUs.elm`
|
||||
`/settings/users` | `src/Pages/Settings/Users.elm`
|
||||
|
||||
### Capitalization matters
|
||||
|
||||
Notice how the filename `AboutUs.elm` translated to `/about-us`?
|
||||
|
||||
If we named the file `Aboutus.elm` (with a lowercase "u"), then we'd have a path to `/aboutus` (without the dash between words).
|
||||
|
||||
> In __elm-spa__, we use "kebab-case" rather than "snake_case" as the convention for separating words.
|
||||
|
||||
### Nested static routes
|
||||
|
||||
You can even have __nested static routes__ within folders:
|
||||
|
||||
```terminal
|
||||
elm-spa add /settings/users
|
||||
```
|
||||
|
||||
This example creates a file at `Settings/Users.elm`, which will handle all requests to `/settings/user`. This pattern continues, supporting nesting things multiple levels deep:
|
||||
|
||||
```terminal
|
||||
elm-spa add /settings/user/contact
|
||||
```
|
||||
|
||||
|
||||
## Dynamic routes
|
||||
|
||||
Sometimes a 1:1 mapping is what you need, but other times, its useful to have a single page that will handles requests to similar URL structures.
|
||||
|
||||
A common example is providing a different ID for a blog post, user, or another item in a collection.
|
||||
|
||||
```terminal
|
||||
elm-spa add /people/:name
|
||||
```
|
||||
|
||||
This creates a file at `People/Name_.elm`. In __elm-spa__, we call this a __dynamic route__. It handles requests to any URLs that match `/people/____` and provides the dynamic bit in the `req.params` value passed into your page!
|
||||
|
||||
URL | `req.params`
|
||||
--- | ---
|
||||
`/people/ryan` | `{ name = "ryan" }`
|
||||
`/people/alexa` | `{ name = "alexa" }`
|
||||
`/people/erik` | `{ name = "erik" }`
|
||||
|
||||
> The __underscore__ at the end of the filename (`Name_.elm`) indicates that this route is __dynamic__. Without the underscore, it would only handle requests to a single URL: `/people/name`
|
||||
|
||||
The name of the `req.params` variable (`name` in this example) is determined by the name of the file! If we named the file `Id_.elm` instead, the dynamic value would be at `req.params.id`.
|
||||
|
||||
### Nested dynamic routes
|
||||
|
||||
Just like we saw earlier with nested static routes, you can use nested folders to create __nested dynamic routes__!
|
||||
|
||||
```terminal
|
||||
elm-spa add /users/:name/posts/:id
|
||||
```
|
||||
|
||||
This creates a file at `src/Users/Name_/Posts/Id_.elm`, which handles any request that matches `/users/___/posts/___`:
|
||||
|
||||
URL | `req.params`
|
||||
--- | ---
|
||||
`/users/ryan/posts/123` | `{ name = "ryan", id = "123" }`
|
||||
`/users/alexa/posts/456` | `{ name = "alexa", id = "456" }`
|
||||
`/users/erik/posts/789` | `{ name = "erik", id = "789" }`
|
||||
|
||||
|
||||
## Not found page
|
||||
|
||||
If a user visits a URL that doesn't have a corresponding page, it will redirect to the `NotFound.elm` page.
|
||||
|
||||
By default, the not found page is generated for you in the `.elm-spa/defaults/Pages` folder. When you are ready to customize your 404 page, move it from the defaults folder into `src/Pages`:
|
||||
|
||||
```elm
|
||||
.elm-spa/
|
||||
|- defaults/
|
||||
|- Pages/
|
||||
|- NotFound.elm
|
||||
|
||||
-- move into
|
||||
|
||||
src/
|
||||
|- Pages/
|
||||
|- NotFound.elm
|
||||
```
|
||||
|
||||
Once you have a `NotFound.elm` within your `src/Pages` folder, __elm-spa__ will stop generating the other one, and use your custom 404 file instead.
|
||||
|
||||
The technique of moving a file from the `.elm-spa/defaults` folder is known as "ejecting a default file". Throughout the guide, we'll find more examples of files that we might want to move into our `src` folder.
|
||||
|
102
docs/public/content/docs/shared-state.md
Normal file
102
docs/public/content/docs/shared-state.md
Normal file
@ -0,0 +1,102 @@
|
||||
# Shared state
|
||||
|
||||
With __elm-spa__, any time we move from one page to another, the `init` function for that new page is called. This means that the state of the previous page you were looking at has been replaced by the new page.
|
||||
|
||||
So if we sign in a user at the `SignIn` page, we'll need a place to store the user before navigating over to the `Dashboard`.
|
||||
|
||||
This is where the `Shared` module comes in– the perfect place to store things that every page needs to access!
|
||||
|
||||
### Ejecting the default file
|
||||
|
||||
By default, an empty `Shared.elm` file is generated for us in `.elm-spa/defaults`. When you are ready to share data between pages– move that file from the defaults folder to the `src` folder.
|
||||
|
||||
```elm
|
||||
.elm-spa/
|
||||
|- defaults/
|
||||
|- Shared.elm
|
||||
|
||||
-- move into
|
||||
|
||||
src/
|
||||
|- Shared.elm
|
||||
```
|
||||
|
||||
Once you've done that, `src/Shared.elm` is under your control– and __elm-spa__ will stop generating the old one. Let's dive into the different parts of that file!
|
||||
|
||||
## Shared.Flags
|
||||
|
||||
The first thing you'll see is a `Flags` type exposed from the top of the file. If we need to load some initial data from Javascript when our Elm app starts up, we can pass that data in as flags.
|
||||
|
||||
When you have the need to send in initial JSON data, take a look at [Elm's official guide on JS interop](https://guide.elm-lang.org/interop/).
|
||||
|
||||
## Shared.Model
|
||||
|
||||
By default, our `Model` is just an empty record:
|
||||
|
||||
```elm
|
||||
type alias Model =
|
||||
{}
|
||||
```
|
||||
|
||||
If we wanted to store a signed-in user, adding it to the model would make it available to all pages:
|
||||
|
||||
```elm
|
||||
type alias Model =
|
||||
{ user : Maybe User
|
||||
}
|
||||
|
||||
type alias User =
|
||||
{ name : String
|
||||
, email : String
|
||||
, token : String
|
||||
}
|
||||
```
|
||||
|
||||
As we saw in the [pages guide](/docs/pages), this `Shared.Model` will be passed into every page– so we can check if `shared.user` has a value or not!
|
||||
|
||||
## Shared.init
|
||||
|
||||
```elm
|
||||
init : Flags -> Request -> ( Model, Cmd Msg )
|
||||
init flags req =
|
||||
...
|
||||
```
|
||||
|
||||
The `init` function is called when your application loads for the first time. It takes in two inputs:
|
||||
|
||||
- `Flags` - initial JS values passed in on startup.
|
||||
- `Request` - the [Request](/docs/request) value with current URL information.
|
||||
|
||||
The `init` function returns the initial `Shared.Model`, as well as any side effect's you'd like to run (like initial HTTP requests, etc)
|
||||
|
||||
## Shared.Msg
|
||||
|
||||
Once you become familiar with [the Elm architecture](https://guide.elm-lang.org/architecture/), you'll recognize the `Msg` type as the only way to update `Shared.Model`.
|
||||
|
||||
Maybe it looks something like this for our user example
|
||||
|
||||
```elm
|
||||
type Msg
|
||||
= SignedIn User
|
||||
| SignedOut
|
||||
```
|
||||
|
||||
These are used in the next section on `Shared.update`!
|
||||
|
||||
## Shared.update
|
||||
|
||||
```elm
|
||||
update : Request -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
```
|
||||
|
||||
The `update` function allows you to respond when one of your pages or this module send a `Shared.Msg`. Just like pages, you define a `Msg` type to handle how they update the shared state here.
|
||||
|
||||
## Shared.subscriptions
|
||||
|
||||
```elm
|
||||
subscriptions : Request -> Model -> Sub Msg
|
||||
```
|
||||
|
||||
If you want all pages to listen for keyboard events, window resizing, or other external updates, this `subscriptions` function is a great place to wire those up!
|
||||
|
||||
It also has access to the current URL request value, so you can conditionally subscribe to events.
|
107
docs/public/content/docs/url-requests.md
Normal file
107
docs/public/content/docs/url-requests.md
Normal file
@ -0,0 +1,107 @@
|
||||
# Requests
|
||||
|
||||
Every page in your application gets access to a `Request` value, containing details about the current URL.
|
||||
|
||||
```elm
|
||||
page : Shared.Model -> Request -> Page
|
||||
page shared req =
|
||||
...
|
||||
```
|
||||
|
||||
This might be useful when you need to show the active link in your navbar, or navigate to a different page programmicatically. Let's look at the properties on `req` that you might find useful!
|
||||
|
||||
## req.params
|
||||
|
||||
Every [dynamic route](/docs/routing#dynamic-routes) has parameters that you'll want to get access to. For [static routes](/docs/routing@static-routes), those parameters will be `()`:
|
||||
|
||||
URL | Request
|
||||
--- | ---
|
||||
`/` | `Request`
|
||||
`/about-us` | `Request`
|
||||
`/people/:name` | `Request.With { name : String }`
|
||||
`/posts/:post/users/:user` | `Request.With { post : String, user : String }`
|
||||
|
||||
Here's an example for a file at `People/Name_.elm`:
|
||||
|
||||
URL | `req.params`
|
||||
--- | ---
|
||||
`/people/alexa` | `{ name = "alexa" }`
|
||||
`/people/erik` | `{ name = "erik" }`
|
||||
`/people/ryan` | `{ name = "ryan" }`
|
||||
|
||||
## req.query
|
||||
|
||||
For convenience, query parameters are automatically turned into a `Dict String String`, making it easy to handle common query URL parameters like these:
|
||||
|
||||
```
|
||||
/people?team=design&ascending
|
||||
```
|
||||
|
||||
```elm
|
||||
Dict.get "team" req.query == Just "design"
|
||||
Dict.get "ascending" req.query == Just ""
|
||||
Dict.get "name" req.query == Nothing
|
||||
```
|
||||
|
||||
> If you need ever access to the raw query string, you can with the `req.url.query` value!
|
||||
|
||||
## req.route
|
||||
|
||||
The `req.route` value has the current `Route`, so you can safely check if you are on a specific page.
|
||||
|
||||
All the routes generated by __elm-spa__ are available at `Gen.Route`.
|
||||
|
||||
```elm
|
||||
-- "/"
|
||||
req.route == Gen.Route.Home_
|
||||
|
||||
-- "/about-us"
|
||||
req.route == Gen.Route.AboutUs
|
||||
|
||||
-- "/people/ryan"
|
||||
req.route == Gen.Route.People.Name_ { name = "ryan" }
|
||||
```
|
||||
|
||||
## req.url
|
||||
|
||||
If you need the `port`, `hostname`, or anything else it is available at `req.url`, which contains the original [elm/url](https://package.elm-lang.org/packages/elm/url/latest/Url) URL value.
|
||||
|
||||
```elm
|
||||
type alias Url =
|
||||
{ protocol : Protocol
|
||||
, host : String
|
||||
, port_ : Maybe Int
|
||||
, path : String
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
```
|
||||
|
||||
This is less commonly used than `req.params` and `req.query`, but can be useful in certain cases.
|
||||
|
||||
## Programmatic Navigation
|
||||
|
||||
Most of the time, navigation in Elm is as easy as giving an `href` attribute to an anchor tag:
|
||||
|
||||
```elm
|
||||
a [ href "/guide" ] [ text "elm-spa guide" ]
|
||||
```
|
||||
|
||||
Other times, you'll want to do __programmatic navigation__ – navigating to another page after some event completes. Maybe you want to __redirect__ to a sign in page, or head to the __dashboard after signing in successfully__.
|
||||
|
||||
In that case we store `req.key` in order to use `Request.pushRoute` or `Request.replaceRoute`. Here's a quick example of what that looks like:
|
||||
|
||||
```elm
|
||||
type Msg = SignedIn User
|
||||
|
||||
update : Request Params -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update req msg model =
|
||||
case msg of
|
||||
SignedIn user ->
|
||||
( model
|
||||
, Request.pushRoute Gen.Route.Dashboard req
|
||||
)
|
||||
```
|
||||
|
||||
When the `SignedIn` message is fired, this code will redirect the user to the `Dashboard` route.
|
||||
|
105
docs/public/content/docs/views.md
Normal file
105
docs/public/content/docs/views.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Views
|
||||
|
||||
With __elm-spa__, you can choose any Elm view library you like. Whether it's [elm/html](https://package.elm-lang.org/packages/elm/html/latest/), [Elm UI](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/), or even your own custom library, the `View` module has you covered!
|
||||
|
||||
### Ejecting the default view
|
||||
|
||||
If you would like to switch to another UI library– you can move the `View.elm` file from `.elm-spa/defaults` into your `src` folder:
|
||||
|
||||
```elm
|
||||
.elm-spa/
|
||||
|- defaults/
|
||||
|- View.elm
|
||||
|
||||
-- move into
|
||||
|
||||
src/
|
||||
|- View.elm
|
||||
```
|
||||
|
||||
From here on out, __elm-spa__ will use your `View` module as the return type for all `view` functions across your pages!
|
||||
|
||||
## View msg
|
||||
|
||||
By default, a `View` lets you set the tab title as well as render some `Html` in the `body` value.
|
||||
|
||||
```elm
|
||||
type alias View msg =
|
||||
{ title : String
|
||||
, body : List (Html msg)
|
||||
}
|
||||
```
|
||||
|
||||
### Using Elm UI
|
||||
|
||||
If you wanted to use Elm UI, a popular HTML/CSS alternative in the community, you would tweak this `View msg` type to not use `Html msg`:
|
||||
|
||||
```elm
|
||||
import Element exposing (Element)
|
||||
|
||||
type alias View msg =
|
||||
{ title : String
|
||||
, element : Element msg
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## View.toBrowserDocument
|
||||
|
||||
Whichever library you use, Elm needs a way to convert it to a `Browser.Document` type. Make sure to provide this function, so __elm-spa__ can convert your UI at the top level.
|
||||
|
||||
Here's an example for Elm UI:
|
||||
|
||||
```elm
|
||||
toBrowserDocument : View msg -> Browser.Document msg
|
||||
toBrowserDocument view =
|
||||
{ title = view.title
|
||||
, body =
|
||||
[ Element.layout [] view.element
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## View.map
|
||||
|
||||
When connecting pages together, __elm-spa__ needs a way to map from one `View msg` to another. For `elm/html`, this is the `Html.map` function.
|
||||
|
||||
But when using a different library, you'll need to specify the `map` function for things to work.
|
||||
|
||||
Fortunately, most UI libraries ship with their own! Here's another example with Elm UI:
|
||||
|
||||
```elm
|
||||
map : (a -> b) -> View a -> View b
|
||||
map fn view =
|
||||
{ title = view.title
|
||||
, element = Element.map fn view.element
|
||||
}
|
||||
```
|
||||
|
||||
## View.empty
|
||||
|
||||
When loading between pages, __elm-spa__ also needs a `View.empty` to be specified for your custom `View` type.
|
||||
|
||||
For Elm UI, that is just `Element.none`:
|
||||
|
||||
```elm
|
||||
empty : View msg
|
||||
empty =
|
||||
{ title = ""
|
||||
, element = Element.none
|
||||
}
|
||||
```
|
||||
|
||||
## View.placeholder
|
||||
|
||||
The last thing you need to provide is a `View.placeholder`, used by the __elm-spa add__ command to provide a stubbed out `view` function implementation.
|
||||
|
||||
Here's an example of a `placeholder` with Elm UI:
|
||||
|
||||
```elm
|
||||
placeholder : String -> View msg
|
||||
placeholder pageName =
|
||||
{ title = pageName
|
||||
, element = Element.text pageName
|
||||
}
|
||||
```
|
@ -1 +1,7 @@
|
||||
# Examples
|
||||
# Examples
|
||||
|
||||
Here are real-world applications using __elm-spa__:
|
||||
|
||||
- This site
|
||||
- Realworld example app
|
||||
- User featured projects
|
@ -1 +0,0 @@
|
||||
# User Authentication
|
@ -1,228 +0,0 @@
|
||||
# Pages
|
||||
|
||||
In __elm-spa__, every URL connects to a single page. Let's take a closer look at the homepage we created earlier with the `elm-spa new` command:
|
||||
|
||||
```elm
|
||||
module Pages.Home_ exposing (view)
|
||||
|
||||
import Html
|
||||
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
}
|
||||
```
|
||||
|
||||
This homepage renders the tab `title`, and a HTML `body` onto the page. This is great when you have a static page that just needs to render some elements.
|
||||
|
||||
Because the file is named `Home_.elm`, we know it's the homepage. These 8 lines of code are all we need to tell __elm-spa__ we'd like to render this when users visit the homepage.
|
||||
|
||||
For real world applications, we'll need pages that can do more. That's where the `Page` module comes in handy.
|
||||
|
||||
### Upgrading "Hello World!"
|
||||
|
||||
Let's start by introducing the `page` function, marking the start of our journey from "Hello world!" to the real world:
|
||||
|
||||
```elm
|
||||
module Pages.Home_ exposing (page)
|
||||
|
||||
-- our other imports
|
||||
import Page exposing (Page)
|
||||
|
||||
page :
|
||||
Shared.Model
|
||||
-> Request Params
|
||||
-> Page () Never
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view
|
||||
}
|
||||
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
}
|
||||
```
|
||||
|
||||
Here, our code hasn't changed very much- except now we have this new `page` function that:
|
||||
|
||||
1. Takes in two inputs: `Shared.Model` and `Request Params`
|
||||
2. Returns a `Page () Never` value.
|
||||
3. Is exposed at the top of our `Pages.Home_` module.
|
||||
|
||||
> Without exposing `page`, __elm-spa__ will not understand how to compile your application. Make sure to _always_ expose `page` from modules within the `src/Pages` folder.
|
||||
|
||||
This new `page` will always get the latest `Shared.Model` and URL information, which means you don't have to worry about tracking that stuff yourself.
|
||||
|
||||
This is great, but there is still more that our `page` function can do other than render a view!
|
||||
|
||||
### Beyond static pages
|
||||
|
||||
The right page to use is the __simplest page__ that can support what you need! As we move from `Page.static` to `Page.advanced`, we'll have more capabilities, but at the cost of more code.
|
||||
|
||||
This section of the guide will introduce you to the functions exposed by the `Page` module, so you have all the information you need.
|
||||
|
||||
__[Page.static](#pagestatic)__ - for pages that only render a view.
|
||||
|
||||
```elm
|
||||
Page.static
|
||||
{ view : View Never
|
||||
}
|
||||
```
|
||||
|
||||
__[Page.sandbox](#pagesandbox)__ - for pages that need to keep track of state.
|
||||
|
||||
```elm
|
||||
Page.sandbox
|
||||
{ init : Model
|
||||
, update : Msg -> Model -> Model
|
||||
, view : Model -> View Msg
|
||||
}
|
||||
```
|
||||
|
||||
__[Page.element](#pageelement)__ - for pages that send HTTP requests or continually listen for events from the browser or user.
|
||||
|
||||
```elm
|
||||
Page.element
|
||||
{ init : ( Model, Cmd Msg )
|
||||
, update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
, view : Model -> View Msg
|
||||
, subscriptions : Model -> Sub Msg
|
||||
}
|
||||
```
|
||||
|
||||
__[Page.advanced](#pageadvanced)__ - For pages that need to sign in a user or work with other details that should persist between page navigation.
|
||||
|
||||
```elm
|
||||
Page.advanced
|
||||
{ init : ( Model, Effect Msg )
|
||||
, update : Msg -> Model -> ( Model, Effect Msg )
|
||||
, view : Model -> View Msg
|
||||
, subscriptions : Model -> Sub Msg
|
||||
}
|
||||
```
|
||||
|
||||
### Working with pages
|
||||
|
||||
The `page` function allows us to pass `Shared.Model` and `Request` information to any inner function that needs it.
|
||||
|
||||
```elm
|
||||
page shared req =
|
||||
Page.sandbox
|
||||
{ init = init shared -- pass init "shared"
|
||||
, update = update req -- pass update "req"
|
||||
, view = view
|
||||
}
|
||||
```
|
||||
|
||||
Imagine your `init` function needs access to `shared` data, and your `update` function needs URL information from the current `req`.
|
||||
|
||||
Because `page` is a function, you can pass those values in where you see fit. This means the type annotations of those inner functions should also update:
|
||||
|
||||
__Before__
|
||||
```elm
|
||||
page shared req =
|
||||
Page.sandbox
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
}
|
||||
|
||||
init : Model
|
||||
update : Msg -> Model -> Model
|
||||
view : Model -> View Msg
|
||||
```
|
||||
|
||||
__After__
|
||||
|
||||
```elm
|
||||
page shared req =
|
||||
Page.sandbox
|
||||
{ init = init shared
|
||||
, update = update req
|
||||
, view = view
|
||||
}
|
||||
|
||||
init : Shared.Model -> Model
|
||||
update : Request Params -> Msg -> Model -> Model
|
||||
view : Model -> View Msg
|
||||
```
|
||||
|
||||
Notice how the type annotations of `init` and `update` changed to accept their input? (The `view` function didn't change because it didn't get any new values)
|
||||
|
||||
## Page.static
|
||||
|
||||
```elm
|
||||
Page.static
|
||||
{ view : View Never
|
||||
}
|
||||
```
|
||||
|
||||
( video introducing concept )
|
||||
|
||||
## Page.sandbox
|
||||
|
||||
```elm
|
||||
Page.sandbox
|
||||
{ init : Model
|
||||
, update : Msg -> Model -> Model
|
||||
, view : Model -> View Msg
|
||||
}
|
||||
```
|
||||
|
||||
( video introducing concept )
|
||||
|
||||
## Page.element
|
||||
|
||||
```elm
|
||||
Page.element
|
||||
{ init : ( Model, Cmd Msg )
|
||||
, update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
, view : Model -> View Msg
|
||||
, subscriptions : Model -> Sub Msg
|
||||
}
|
||||
```
|
||||
|
||||
( video introducing concept )
|
||||
|
||||
## Page.advanced
|
||||
|
||||
```elm
|
||||
Page.advanced
|
||||
{ init : ( Model, Effect Msg )
|
||||
, update : Msg -> Model -> ( Model, Effect Msg )
|
||||
, view : Model -> View Msg
|
||||
, subscriptions : Model -> Sub Msg
|
||||
}
|
||||
```
|
||||
|
||||
( video introducing concept )
|
||||
|
||||
|
||||
## Page.protected
|
||||
|
||||
Each of the four page types also have a "protected" version, that is guaranteed to receive a `User` or redirect if no user is signed in.
|
||||
|
||||
```elm
|
||||
Page.protected.static
|
||||
{ view : User -> View Never
|
||||
}
|
||||
|
||||
Page.protected.sandbox
|
||||
{ init : User -> Model
|
||||
, update : User -> Msg -> Model -> Model
|
||||
, view : User -> Model -> View Msg
|
||||
}
|
||||
|
||||
-- Page.protected.element
|
||||
|
||||
-- Page.protected.advanced
|
||||
```
|
||||
|
||||
When you are ready, you can learn more about this in the [user authentication example](/examples/authentication).
|
||||
|
||||
---
|
||||
|
||||
__What's next?__
|
||||
|
||||
Let me introduce you to the `Request Params` type we pass into our pages in the [next section on requests](./requests)
|
@ -1,113 +0,0 @@
|
||||
# Requests
|
||||
|
||||
Every URL that a user visits in your application contains useful information. When __elm-spa__ gets an updated URL, it passes that information to every [Page](/guide/pages) as a `Request` value.
|
||||
|
||||
|
||||
This section of the guide breaks down the [Request](https://package.elm-lang.org/packages/ryannhg/elm-spa/latest/ElmSpa-Request) type exposed by the official Elm package:
|
||||
|
||||
```elm
|
||||
type alias Request params =
|
||||
{ params : params
|
||||
, query : Dict String String
|
||||
, url : Url
|
||||
, route : Route
|
||||
, key : Nav.Key
|
||||
}
|
||||
```
|
||||
|
||||
## URL Parameters
|
||||
|
||||
Every request has parameters that you can rely on. If you are on a [dynamic route](/guide/routing#dynamic-routes), you have access to that route's URL parameters:
|
||||
|
||||
URL | Params
|
||||
--- | ---
|
||||
`/` | `()`
|
||||
`/about-us` | `()`
|
||||
`/people/:name` | `{ name : String }`
|
||||
`/posts/:post/comments/:comment` | `{ post : String, comment : String }`
|
||||
|
||||
The first two examples from that table are __static routes__, so there are no dynamic parameters available. The last two examples are guaranteed to have values at `req.params`.
|
||||
|
||||
All dynamic parameters are `String` types, so feel free to validate them at the page level.
|
||||
|
||||
```elm
|
||||
greet : Request { name : String } -> String
|
||||
greet req =
|
||||
"Hello, " ++ req.params.name ++ "!"
|
||||
```
|
||||
|
||||
__Note:__ When working with [shared state](/guide/shared-state), all requests are `Request ()`.
|
||||
|
||||
## Query Parameters
|
||||
|
||||
For convenience, query parameters are automatically turned into a `Dict String String`, making it easy to handle common query URL parameters like these:
|
||||
|
||||
```
|
||||
/people?team=design&ascending
|
||||
```
|
||||
|
||||
```elm
|
||||
Dict.get "team" req.query == Just "design"
|
||||
Dict.get "ascending" req.query == Just ""
|
||||
Dict.get "name" req.query == Nothing
|
||||
```
|
||||
|
||||
__Note:__ If you need ever access to the raw URL query string, you can with the `req.url.query` value!
|
||||
|
||||
## Raw URLs
|
||||
|
||||
If you need the `port`, `hostname`, or anything else it is available at `req.url`, which contains the original [elm/url](https://package.elm-lang.org/packages/elm/url/latest/Url) URL value.
|
||||
|
||||
```elm
|
||||
type alias Url =
|
||||
{ protocol : Protocol
|
||||
, host : String
|
||||
, port_ : Maybe Int
|
||||
, path : String
|
||||
, query : Maybe String
|
||||
, fragment : Maybe String
|
||||
}
|
||||
```
|
||||
|
||||
This is less common than `req.params` and `req.query`, but can be useful for getting the `hash` at the end of a URL too!
|
||||
|
||||
## Getting the current route
|
||||
|
||||
The `Request` type also has access to the `Route` value, so you can easily do comparisons against the current route!
|
||||
|
||||
```elm
|
||||
-- "/"
|
||||
req.route == Gen.Route.Home_
|
||||
|
||||
-- "/about-us"
|
||||
req.route == Gen.Route.AboutUs
|
||||
|
||||
-- "/people/ryan"
|
||||
req.route == Gen.Route.People.Name_ { name = "ryan" }
|
||||
```
|
||||
|
||||
## Programmatic Navigation
|
||||
|
||||
Most of the time, navigation in Elm is as easy as giving an `href` attribute to an anchor tag:
|
||||
|
||||
```elm
|
||||
a [ href "/guide" ] [ text "elm-spa guide" ]
|
||||
```
|
||||
|
||||
Other times, you'll want to do __programmatic navigation__ – navigating to another page after some event completes. Maybe you want to __redirect__ to a sign in page, or head to the __dashboard after signing in successfully__.
|
||||
|
||||
In that case we store `req.key` in order to use `Request.pushRoute` or `Request.replaceRoute`. Here's a quick example of what that looks like:
|
||||
|
||||
```elm
|
||||
type Msg = SignedIn User
|
||||
|
||||
update : Request Params -> Msg -> Model -> ( Model, Effect Msg )
|
||||
update req msg model =
|
||||
case msg of
|
||||
SignedIn user ->
|
||||
( model
|
||||
, Request.pushRoute Gen.Route.Dashboard req
|
||||
)
|
||||
```
|
||||
|
||||
When the `SignedIn` message is fired, this code will redirect the user to the `Dashboard` route.
|
@ -1,106 +0,0 @@
|
||||
# Routing
|
||||
|
||||
One of the best reasons to use __elm-spa__ is the automatic routing! Inspired by popular JS frameworks like _NuxtJS_, we use file names to determine routes in your application.
|
||||
|
||||
Every __elm-spa__ project will have a `Pages` folder with all the pages in the application.
|
||||
|
||||
URL | File
|
||||
--- | ---
|
||||
`/` | `src/Pages/Home_.elm`
|
||||
`/people` | `src/Pages/People.elm`
|
||||
`/people/:name` | `src/Pages/People/Name_.elm`
|
||||
`/about-us` | `src/Pages/AboutUs.elm`
|
||||
`/settings/users` | `src/Pages/Settings/Users.elm`
|
||||
|
||||
In this section, we'll cover the 3 kinds of routes you can find in an __elm-spa__ application.
|
||||
|
||||
## The homepage
|
||||
|
||||
The `src/Pages/Home_.elm` is a reserved filename that handles requests to /. The easiest way to make a new homepage is with the [`add` command](/guide/cli#adding-a-homepage) covered in the CLI section:
|
||||
|
||||
```terminal
|
||||
elm-spa add /
|
||||
```
|
||||
|
||||
__Note:__ Without the trailing underscore, __elm-spa__ will treat `Home.elm` as a route to `/home`! This is called a "static route", and will be covered next.
|
||||
|
||||
## Static routes
|
||||
|
||||
Most pages will be __static routes__, meaning the filepath will translate to a single URL.
|
||||
|
||||
```terminal
|
||||
elm-spa add /people
|
||||
```
|
||||
|
||||
This command creates a page at `src/Pages/People.elm` that will be shown when the user visits `/people` in your app!
|
||||
|
||||
These are more examples of static routes:
|
||||
|
||||
URL | File
|
||||
--- | ---
|
||||
`/dashboard` | `src/Pages/Dashboard.elm`
|
||||
`/people` | `src/Pages/People.elm`
|
||||
`/about-us` | `src/Pages/AboutUs.elm`
|
||||
`/settings/users` | `src/Pages/Settings/Users.elm`
|
||||
|
||||
### Nested static routes
|
||||
|
||||
You can use folders to have __nested static routes__:
|
||||
|
||||
```terminal
|
||||
elm-spa add /settings/users
|
||||
```
|
||||
|
||||
This example creates a file at `src/Pages/Settings/Users.elm`, which will handle all requests to `/settings/user`. You can nest things multiple levels by creating even more nested folders:
|
||||
|
||||
```terminal
|
||||
elm-spa add /settings/user/contact
|
||||
```
|
||||
|
||||
|
||||
## Dynamic routes
|
||||
|
||||
Sometimes a 1:1 mapping is what you need, but other times, its useful to have a route that handles requests to many items.
|
||||
|
||||
```terminal
|
||||
elm-spa add /people/:name
|
||||
```
|
||||
|
||||
This will create a file at `src/Pages/People/Name_.elm`. In __elm-spa__, this is called a __dynamic route__. It will handle requests to any URLs that match `/people/____` and provide the dynamic part in the parameters.
|
||||
|
||||
URL | Params
|
||||
--- | ---
|
||||
`/people/ryan` | `{ name = "ryan" }`
|
||||
`/people/alexa` | `{ name = "alexa" }`
|
||||
`/people/erik` | `{ name = "erik" }`
|
||||
|
||||
The __trailing underscore__ at the end of the filename (`Name_.elm`) indicates that this route is __dynamic__. Without the underscore, it would only handle requests to `/people/name`
|
||||
|
||||
The name of the route parameter variable (`name` in this example) is determined by the name of the file! If we named the file `Id_.elm`, the dynamic value would be available at `params.id` instead.
|
||||
|
||||
Every page gets access to these dynamic parameters, via the [`Request params`](/guide/pages#requests) value passed in. We'll cover that in the next section!
|
||||
|
||||
### Nested dynamic routes
|
||||
|
||||
Just like we saw with __nested static routes__, you can use nested folders to create nested dynamic routes!
|
||||
|
||||
```terminal
|
||||
elm-spa add /users/:name/posts/:id
|
||||
```
|
||||
|
||||
This creates a file at `src/Users/Name_/Posts/Id_.elm`
|
||||
|
||||
URL | Params
|
||||
--- | ---
|
||||
`/users/ryan/posts/123` | `{ name = "ryan", id = "123" }`
|
||||
`/users/alexa/posts/456` | `{ name = "alexa", id = "456" }`
|
||||
`/users/erik/posts/789` | `{ name = "erik", id = "789" }`
|
||||
|
||||
It will handle any request to `/users/___/posts/___`
|
||||
|
||||
|
||||
## Not found page
|
||||
|
||||
If a user visits a URL that doesn't have a corresponding page, it will redirect to the `NotFound.elm` page. This is generated for you by default in the `.elm-spa/defaults/Pages` folder. When you are ready to customize it, move it into `src/Pages` and customize it like you would any other page!
|
||||
|
||||
In __elm-spa__, this technique is called "ejecting" a default file. Throughout the guide, we'll find more default files that we might want to control in our project.
|
@ -1,45 +0,0 @@
|
||||
# Shared State
|
||||
|
||||
With __elm-spa__, every time you navigate from one page to another, the `init` function for that page is called. This means that the `Model` for the page you we're previously looking at has been cleared out. Most of the time, that's a good thing!
|
||||
|
||||
Other times, it makes sense to __share state between pages__! Maybe you have a signed-in user, an API token, or settings like "dark mode" that you want to persist from one page to another. This section of the guide will show you how to do that!
|
||||
|
||||
## Ejecting the shared file
|
||||
|
||||
Default files are automatically generated for you in the `.elm-spa/defaults`, and when you need to tweak them, you can move them into your project's `src` folder. This process is known as "ejecting default files", and comes up for advanced features.
|
||||
|
||||
__To get started__ with shared state between pages, move the `.elm-spa/defaults/Shared.elm` file into your `src` folder! After you move that file, `src/Shared.elm` will be the place to make changes!
|
||||
|
||||
The rest of this section walks through the different functions in the `Shared` module, so you know what's going on.
|
||||
|
||||
|
||||
### init
|
||||
|
||||
```elm
|
||||
init : Flags -> Request () -> Model -> ( Model, Effect Msg )
|
||||
```
|
||||
|
||||
The `init` function is called when your page loads for the first time. It takes in two inputs:
|
||||
|
||||
- `Flags` - initial JSON value passed in from `public/main.js
|
||||
- `Request ()` - a [Request](/guide/request) value with the current URL information
|
||||
|
||||
The `init` function returns the initial `Model`, as well as any `Effect`s you'd like to run (like initial HTTP requests, etc)
|
||||
|
||||
__Note:__ The [Effect msg] type is just an alias for `Cmd msg`, but adds support for [elm-program-test]()
|
||||
|
||||
### update
|
||||
|
||||
```elm
|
||||
update : Request () -> Msg -> Model -> ( Model, Effect Msg )
|
||||
```
|
||||
|
||||
The `update` function allows you to respond when one of your pages or this module send a `Shared.Msg`. Just like pages, you define `Msg` types and handle how they update the shared state here.
|
||||
|
||||
### subscriptions
|
||||
|
||||
```elm
|
||||
subscriptions : Request () -> Model -> Sub Msg
|
||||
```
|
||||
|
||||
If you want all pages to listen for keyboard events, window resizing, or other external updates, this `subscriptions` function is a great place to wire those up! It also has access to the current URL request value, so you can conditionally subscribe to events.
|
@ -1,15 +0,0 @@
|
||||
# Views
|
||||
|
||||
With __elm-spa__, you can choose any Elm view library you like. Whether it's
|
||||
[elm/html](#), [Elm UI](#), or even your own custom library, the `View` module
|
||||
has got you covered!
|
||||
|
||||
```elm
|
||||
type alias View msg =
|
||||
{ title : String
|
||||
, body : List (Html msg)
|
||||
}
|
||||
```
|
||||
|
||||
By default, a `View` lets you set the tab title as well as render some `Html` in
|
||||
the `body` value.
|
7
docs/public/content/guides.md
Normal file
7
docs/public/content/guides.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Guides
|
||||
|
||||
Ready to get started with __elm-spa__? Me too!
|
||||
|
||||
...
|
||||
|
||||
Unfortunately, I haven't uploaded any guides yet...
|
136
docs/public/content/guides/01-hello-world.md
Normal file
136
docs/public/content/guides/01-hello-world.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Hello, world!
|
||||
|
||||
Welcome to __elm-spa__! This guide is a breakdown of the simplest project you can make: the "Hello, world!" example.
|
||||
|
||||
### Installation
|
||||
|
||||
In case you are starting from scratch, you can install __elm-spa__ via NPM:
|
||||
|
||||
```terminal
|
||||
npm install -g elm-spa@latest
|
||||
```
|
||||
|
||||
### Creating a project
|
||||
|
||||
This will allow you to create a new project using the following commands:
|
||||
|
||||
```
|
||||
mkdir 01-hello-world
|
||||
cd 01-hello-world
|
||||
|
||||
elm-spa new
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
When we ran `elm-spa new`, only __three__ files were created:
|
||||
|
||||
- __public/index.html__ - the entrypoint for our web app.
|
||||
- __src/Pages/Home\_.elm__ - the homepage.
|
||||
- __elm.json__ - our project dependencies.
|
||||
|
||||
### Running the server
|
||||
|
||||
With only these files, we can get an application up-and-running:
|
||||
|
||||
```terminal
|
||||
elm-spa server
|
||||
```
|
||||
|
||||
This runs a server at [http://localhost:1234](http://localhost:1234). If everything worked, you should see this in your browser:
|
||||
|
||||
![A page that reads "Hello World"](/content/images/01-hello-world.png)
|
||||
|
||||
|
||||
### The entrypoint
|
||||
|
||||
Earlier, I mentioned that `public/index.html` was the "entrypoint" to our web app. Let's take a look at that file:
|
||||
|
||||
```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>
|
||||
<script> Elm.Main.init() </script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
This HTML file defines some standard tags, and then runs our Elm application. Because our Elm compiles to JavaScript, the `elm-spa server` command generates a `/dist/elm.js` file anytime we make changes.
|
||||
|
||||
Once we import that with a `<script>` tag, we can call `Elm.Main.init()` to startup our Elm application.
|
||||
|
||||
### The homepage
|
||||
|
||||
Next, let's look at `src/Pages/Home_.elm`:
|
||||
|
||||
```elm
|
||||
module Pages.Home_ exposing (view)
|
||||
|
||||
import Html
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
}
|
||||
```
|
||||
|
||||
This `view` function has two parts:
|
||||
- `title` - the tab title
|
||||
- `body` - the HTML we render, with [elm/html](https://package.elm-lang.org/packages/elm/html/latest/)
|
||||
|
||||
Try changing `"Hello, world!"` to something else– it should replace what you see in the browser.
|
||||
|
||||
The `elm-spa server` you ran is designed to __automatically refresh__ when your Elm code changes, but you _may_ need to refresh manually to see the change.
|
||||
|
||||
### The dependencies
|
||||
|
||||
The `elm.json` tracks all our project dependencies. Elm packages are available at [package.elm-lang.org](https://package.elm-lang.org/). Here's our initial file:
|
||||
|
||||
```js
|
||||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src",
|
||||
".elm-spa/defaults",
|
||||
".elm-spa/generated"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": { /* ... */ },
|
||||
"test-dependencies": { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
Normally, `source-directories` in Elm projects only contain the `"src"` folder, but __elm-spa__ projects automatically generate code and provide some default files.
|
||||
|
||||
When we start getting into more advanced guides, we can move files from `.elm-spa/defaults` into our `src` folder. That will track them in git, and let us make changes.
|
||||
|
||||
The files in `.elm-spa/generated` should not be changed, so they are stored in a separate folder. Feel free to browse these if you are curious, they are just normal Elm code.
|
||||
|
||||
|
||||
### The .gitignore
|
||||
|
||||
By default, a `.gitignore` file is generated to promote best practices when working with __elm-spa__ and your git repo:
|
||||
|
||||
```
|
||||
.elm-spa
|
||||
elm-stuff
|
||||
dist
|
||||
```
|
||||
|
||||
Notice that the `.elm-spa` folder is __ignored from git__. You shouldn't push any generated __elm-spa__ code to your repo. Instead, use commands like `elm-spa build` to reliably regenerate these files during deployments.
|
||||
|
||||
```terminal
|
||||
elm-spa build
|
||||
```
|
||||
|
||||
This command will also minify your `/dist/elm.js` file so it's production ready.
|
||||
|
204
docs/public/content/guides/02-pages.md
Normal file
204
docs/public/content/guides/02-pages.md
Normal file
@ -0,0 +1,204 @@
|
||||
# Pages & routing
|
||||
|
||||
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
|
||||
|
||||
Just like with the last guide, we can use `elm-spa new` and `elm-spa server` to get a brand new __elm-spa__ project up and running:
|
||||
|
||||
```terminal
|
||||
mkdir 02-pages
|
||||
cd 02-pages
|
||||
|
||||
elm-spa new
|
||||
elm-spa server
|
||||
```
|
||||
|
||||
This generates the "Hello, world!" homepage from before:
|
||||
|
||||
![A browser displaying "Hello world"](/content/images/01-hello-world.png)
|
||||
|
||||
### Adding a static page
|
||||
|
||||
```terminal
|
||||
elm-spa add /static static
|
||||
```
|
||||
|
||||
This command adds a page at [http://localhost:1234/static](http://localhost:1234/static) with the `static` template. This is similar to `Home_.elm`, but it has access to `Shared.Model` and `Request` in case we need data from either of those.
|
||||
|
||||
Here is the complete `Static.elm` file:
|
||||
|
||||
```elm
|
||||
module Pages.Static exposing (page)
|
||||
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request -> Page
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view
|
||||
}
|
||||
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
View.placeholder "Static"
|
||||
```
|
||||
|
||||
The `View.placeholder` function just stubs out the `view` function with an empty page that only renders "Static" in the browser.
|
||||
|
||||
Visit [http://localhost:1234/static](http://localhost:1234/static) to see it in action!
|
||||
|
||||
### Making a layout
|
||||
|
||||
Before we continue, I want to make a layout with a navbar so that we can easily navigate between pages without manually editing the URL.
|
||||
|
||||
I'll create a file at `src/UI.elm` that looks like this:
|
||||
|
||||
```elm
|
||||
module UI exposing (layout)
|
||||
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
|
||||
|
||||
layout : List (Html msg) -> List (Html msg)
|
||||
layout children =
|
||||
let
|
||||
viewLink : String -> String -> Html msg
|
||||
viewLink label url =
|
||||
Html.a [ Attr.href url ] [ Html.text label ]
|
||||
in
|
||||
[ Html.div [ Attr.class "container" ]
|
||||
[ Html.header [ Attr.class "navbar" ]
|
||||
[ viewLink "Home" "/"
|
||||
, viewLink "Static" "/static"
|
||||
]
|
||||
, Html.main_ [] children
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
### Using the layout in a page
|
||||
|
||||
Because it works from one `List (Html msg)` to another, we can add `UI.layout` in front of the `body` list on both pages:
|
||||
|
||||
```elm
|
||||
-- src/Pages/Home_.elm
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = UI.layout [ Html.text "Homepage" ]
|
||||
}
|
||||
```
|
||||
|
||||
```elm
|
||||
-- src/Pages/Static.elm
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Static"
|
||||
, body = UI.layout [ Html.text "Static" ]
|
||||
}
|
||||
```
|
||||
|
||||
### Use routes, not strings
|
||||
|
||||
In `src/UI.elm`, we had a function for rendering our navbar links that looked like this:
|
||||
|
||||
```elm
|
||||
viewLink : String -> String -> Html msg
|
||||
viewLink label url =
|
||||
Html.a [ Attr.href url ] [ Html.text label ]
|
||||
```
|
||||
|
||||
This function works great– but it's possible to provide a URL that our application doesn't have!
|
||||
|
||||
```elm
|
||||
[ viewLink "Home" "/"
|
||||
, viewLink "Static" "/satic"
|
||||
]
|
||||
```
|
||||
|
||||
Here, I mistyped the URL `/satic`, but the compiler didn't warn me about it! Let's use the `Route` values generated by __elm-spa__ to improve this experience:
|
||||
|
||||
```elm
|
||||
import Gen.Route as Route exposing (Route)
|
||||
|
||||
viewLink : String -> Route -> Html msg
|
||||
viewLink label route =
|
||||
Html.a [ Attr.href (Route.toHref route) ] [ Html.text label ]
|
||||
```
|
||||
|
||||
By using the `Gen.Route` module from `.elm-spa/generated`, we can pass in a `Route` instead of a `String`:
|
||||
|
||||
```elm
|
||||
[ viewLink "Home" Route.Home_
|
||||
, viewLink "Static" Route.Static
|
||||
]
|
||||
```
|
||||
|
||||
This will prevent typos, but __more importantly__ it allows the Elm compiler to remind us to update the navbar in case we remove `Home_.elm` or `Static.elm` in the future.
|
||||
|
||||
Deleting either of those pages changes the generated `Gen.Route` module, so the compiler can let us know that our `UI.layout` function has a broken link– before our users do!
|
||||
|
||||
### Adding CSS
|
||||
|
||||
In `UI.layout`, we used `Attr.class` to provide our HTML with some CSS classes:
|
||||
|
||||
```elm
|
||||
Html.div [ Attr.class "container" ]
|
||||
[ Html.header [ Attr.class "navbar" ]
|
||||
[ viewLink "Home" Route.Home_
|
||||
, viewLink "Static" Route.Static
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
The `container` and `navbar` classes are used in our code, but not defined in a CSS file. Let's fix that by creating a new CSS file at `public/style.css`:
|
||||
|
||||
```css
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar a {
|
||||
margin-right: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
After creating `style.css`, we can import the file in our `public/index.html` entrypoint:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- import our new CSS file -->
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<script src="/dist/elm.js"></script>
|
||||
<script> Elm.Main.init() </script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
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.
|
||||
|
385
docs/public/content/guides/03-user-auth.md
Normal file
385
docs/public/content/guides/03-user-auth.md
Normal file
@ -0,0 +1,385 @@
|
||||
# User 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:
|
||||
|
||||
```elm
|
||||
case currentVisitor of
|
||||
SignedIn user ->
|
||||
ProvidePageWith user
|
||||
|
||||
NotSignedIn ->
|
||||
RedirectTo Route.SignIn
|
||||
```
|
||||
|
||||
__Great news:__ This is exactly what we can do in __elm-spa__!
|
||||
|
||||
## Protected pages
|
||||
|
||||
At the end of the [pages docs](/docs/pages#pageprotected), we learned that there are also `protected` versions of every __page type__.
|
||||
|
||||
These protected pages have slightly different signatures:
|
||||
|
||||
```elm
|
||||
Page.sandbox :
|
||||
{ init : Model
|
||||
, update : Msg -> Model -> Model
|
||||
, view : Model -> View Msg
|
||||
}
|
||||
|
||||
Page.protected.sandbox :
|
||||
User ->
|
||||
{ init : Model
|
||||
, update : Msg -> Model -> Model
|
||||
, view : Model -> View Msg
|
||||
}
|
||||
```
|
||||
|
||||
Protected pages are __guaranteed__ to have access to a `User`, so you don't need to handle the impossible case where you are viewing a page without one.
|
||||
|
||||
## Following along
|
||||
|
||||
Feel free to follow along by creating a new __elm-spa__ project:
|
||||
|
||||
```terminal
|
||||
npm install -g elm-spa@latest
|
||||
```
|
||||
|
||||
```
|
||||
mkdir user-auth-demo
|
||||
cd user-auth-demo
|
||||
elm-spa new
|
||||
```
|
||||
|
||||
This will create a new project that you can run with the `elm-spa server` command!
|
||||
|
||||
The complete working example is also available at [examples/03-user-auth](https://github.com/ryannhg/elm-spa/tree/master/examples/03-user-auth) on GitHub.
|
||||
|
||||
### Ejecting Auth.elm
|
||||
|
||||
There's a default file that has this code stubbed out for you in the `.elm-spa/defaults` folder. Let's eject that file into our `src` folder so we can edit it:
|
||||
|
||||
```elm
|
||||
.elm-spa/
|
||||
|- defaults/
|
||||
|- Auth.elm
|
||||
|
||||
-- move into
|
||||
|
||||
src/
|
||||
|- Auth.elm
|
||||
```
|
||||
|
||||
Now that we have `Auth.elm` in our `src` folder, we can start adding the code that makes __elm-spa__ protect certain pages.
|
||||
|
||||
The `Auth.elm` file only needs to expose two things:
|
||||
- __User__ - The type that we want to provide all protected pages.
|
||||
- __beforeProtectedInit__ - The logic that runs before any `Page.protected.*` page loads
|
||||
|
||||
|
||||
```elm
|
||||
module Auth exposing (User, beforeProtectedInit)
|
||||
|
||||
import Gen.Route
|
||||
import ElmSpa.Internals as ElmSpa
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
|
||||
|
||||
type alias User =
|
||||
()
|
||||
|
||||
|
||||
beforeProtectedInit : Shared.Model -> Request -> ElmSpa.Protected User Route
|
||||
beforeProtectedInit shared req =
|
||||
ElmSpa.RedirectTo Gen.Route.NotFound
|
||||
```
|
||||
|
||||
By default, this code redirects all protected pages to the `NotFound` page. Instead we want something like this:
|
||||
|
||||
```elm
|
||||
beforeProtectedInit : Shared.Model -> Request -> ElmSpa.Protected User Route
|
||||
beforeProtectedInit shared req =
|
||||
case shared.user of
|
||||
Just user ->
|
||||
ElmSpa.Provide user
|
||||
|
||||
Nothing ->
|
||||
ElmSpa.RedirectTo Gen.Route.SignIn
|
||||
```
|
||||
|
||||
But before that code will work we need to take care of two things:
|
||||
|
||||
1. Updating Shared.elm
|
||||
2. Adding a sign in page
|
||||
|
||||
## Updating Shared.elm
|
||||
|
||||
If you haven't already ejected `Shared.elm`, you should move it from `.elm-spa/defaults` into your `src` folder. The [shared state](/docs/shared-state) docs cover this file in depth, but we'll provide all the code you'll need to change here.
|
||||
|
||||
Let's change `Shared.Model` to keep track of a `Maybe User`, the value that can either be a user or nothing:
|
||||
|
||||
```elm
|
||||
-- src/Shared.elm
|
||||
|
||||
type alias Model =
|
||||
{ user : Maybe User
|
||||
}
|
||||
|
||||
type alias User =
|
||||
{ name : String
|
||||
}
|
||||
```
|
||||
|
||||
> For now, a user is just going to have a `name` field. This might also store an `email`, `profilePictureUrl`, or `token` too.
|
||||
|
||||
Next, we should initially set our user to `Nothing` when our Elm application starts up:
|
||||
|
||||
```elm
|
||||
-- src/Shared.elm
|
||||
|
||||
init : Request -> Flags -> ( Model, Cmd Msg )
|
||||
init _ _ =
|
||||
( { user = Nothing }
|
||||
, Cmd.none
|
||||
)
|
||||
```
|
||||
|
||||
To make sure that `Auth.elm` is using the same type, __let's expose__ the `User` type from our `Shared` module and reuse it:
|
||||
|
||||
```elm
|
||||
-- src/Shared.elm
|
||||
|
||||
module Shared exposing ( ..., User )
|
||||
```
|
||||
|
||||
```elm
|
||||
-- src/Auth.elm
|
||||
|
||||
type alias User =
|
||||
Shared.User
|
||||
```
|
||||
|
||||
As the final update to `Shared`, lets add some sign in/sign out logic
|
||||
|
||||
```elm
|
||||
module Shared exposing ( ..., Msg(..))
|
||||
|
||||
import Gen.Route
|
||||
|
||||
-- ...
|
||||
|
||||
type Msg
|
||||
= SignIn User
|
||||
| SignOut
|
||||
|
||||
|
||||
update : Request -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update req msg model =
|
||||
case msg of
|
||||
SignIn user ->
|
||||
( { model | user = Just user }
|
||||
, Request.pushRoute Gen.Route.Home_ req
|
||||
)
|
||||
|
||||
SignOut ->
|
||||
( { model | user = Nothing }
|
||||
, Cmd.none
|
||||
)
|
||||
```
|
||||
|
||||
> Make sure that you expose `Msg(..)` as shown above (instead of just `Msg`). This allows `SignIn` and `SignOut` to be available to pages that send shared updates.
|
||||
|
||||
Great work! Let's use that `SignIn` message on a new sign in page.
|
||||
|
||||
## Adding a sign in page
|
||||
|
||||
With __elm-spa__, adding a new page from the terminal is easy:
|
||||
|
||||
```terminal
|
||||
elm-spa add /sign-in advanced
|
||||
```
|
||||
|
||||
Here we'll start with an "advanced" page, because we'll need to send `Shared.Msg` to sign in and sign out users.
|
||||
|
||||
Let's add a few lines of code to `src/Pages/SignIn.elm`:
|
||||
|
||||
```elm
|
||||
-- Import some HTML
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Events as Events
|
||||
```
|
||||
|
||||
```elm
|
||||
-- Replace Msg with this
|
||||
|
||||
type Msg = ClickedSignIn
|
||||
```
|
||||
|
||||
```elm
|
||||
-- Replace update with this
|
||||
|
||||
update : Msg -> Model -> ( Model, Effect Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
ClickedSignIn ->
|
||||
( model
|
||||
, Effect.fromShared (Shared.SignIn "Ryan")
|
||||
)
|
||||
```
|
||||
|
||||
```elm
|
||||
-- Make view show a sign out button
|
||||
|
||||
view : Model -> Html msg
|
||||
view model =
|
||||
{ title = "Sign In"
|
||||
, body =
|
||||
[ button
|
||||
[ Events.onClick ClickedSignIn ]
|
||||
[ text "Sign in" ]
|
||||
]
|
||||
```
|
||||
|
||||
Nice work- we're only a step away from getting auth set up!
|
||||
|
||||
### Final touches to Auth.elm
|
||||
|
||||
Now that we have a `shared.user` and a `SignIn` route, let's bring it all together in the `Auth.elm` file
|
||||
|
||||
```elm
|
||||
-- src/Auth.elm
|
||||
|
||||
beforeProtectedInit : Shared.Model -> Request -> ElmSpa.Protected User Route
|
||||
beforeProtectedInit shared req =
|
||||
case shared.user of
|
||||
Just user ->
|
||||
ElmSpa.Provide user
|
||||
|
||||
Nothing ->
|
||||
ElmSpa.RedirectTo Gen.Route.SignIn
|
||||
```
|
||||
|
||||
Now visiting [http://localhost:1234/sign-in](http://localhost:1234/sign-in) will show us our sign in page, complete with a sign in button!
|
||||
|
||||
Clicking the "Sign in" button signs in the user when clicked. Because of the logic we added in `Shared.elm`, this also redirects the user to the homepage after sign in!
|
||||
|
||||
## Protecting our homepage
|
||||
|
||||
Let's make it so the homepage is only available to signed in users.
|
||||
|
||||
Let's create a fresh homepage with the __elm-spa add__:
|
||||
|
||||
```terminal
|
||||
elm-spa add / advanced
|
||||
```
|
||||
|
||||
Now that `Auth.elm` is set up, we only need to change the `page` function to guarantee signed-in users are viewing the homepage:
|
||||
|
||||
```elm
|
||||
-- src/Pages/Home_.elm
|
||||
|
||||
Page.advanced
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
-- this becomes
|
||||
|
||||
Page.protected.advanced
|
||||
(\user ->
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
If you want to pass a `User` into any of these functions, you can do it like this:
|
||||
|
||||
```elm
|
||||
-- Only the view is passed a user
|
||||
|
||||
Page.protected.advanced
|
||||
(\user ->
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view user
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
)
|
||||
|
||||
view : User -> Model -> View Msg
|
||||
view user model =
|
||||
...
|
||||
```
|
||||
|
||||
Let's use that `user` so the homepage greets them by name:
|
||||
|
||||
```elm
|
||||
-- src/Pages/Home_.elm
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Events as Events
|
||||
|
||||
-- ...
|
||||
|
||||
view : User -> Model -> View Msg
|
||||
view user model =
|
||||
{ title = "Homepage"
|
||||
, body =
|
||||
[ h1 [] [ text ("Hello, " ++ user.name ++ "!") ]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Try it out!
|
||||
|
||||
Now if we visit [http://localhost:1234](http://localhost:1234), we will immediately be redirected to `/sign-in`, because we haven't signed in yet!
|
||||
|
||||
Clicking the "Sign in" button takes us back to the homepage, and we should see "Hello, Ryan!" printed on the screen.
|
||||
|
||||
|
||||
### The cherry on top
|
||||
|
||||
Let's wrap things up by wiring up a "Sign out" button to the homepage:
|
||||
|
||||
```elm
|
||||
-- src/Pages/Home_.elm
|
||||
|
||||
type Msg = ClickedSignOut
|
||||
|
||||
update : Msg -> Model -> ( Model, Effect Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
ClickedSignOut ->
|
||||
( model
|
||||
, Effect.fromShared Shared.SignOut
|
||||
)
|
||||
|
||||
-- ...
|
||||
|
||||
view : User -> Model -> View Msg
|
||||
view user model =
|
||||
{ title = "Homepage"
|
||||
, body =
|
||||
[ h1 [] [ text ("Hello, " ++ user.name ++ "!") ]
|
||||
, button
|
||||
[ Events.onClick ClickedSignOut ]
|
||||
[ text "Sign out" ]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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...
|
||||
|
||||
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](/guides/local-storage)!
|
1
docs/public/content/guides/04-storage.md
Normal file
1
docs/public/content/guides/04-storage.md
Normal file
@ -0,0 +1 @@
|
||||
# Local storage
|
1
docs/public/content/guides/05-npm.md
Normal file
1
docs/public/content/guides/05-npm.md
Normal file
@ -0,0 +1 @@
|
||||
# Working with NPM
|
BIN
docs/public/content/images/01-hello-world.png
Normal file
BIN
docs/public/content/images/01-hello-world.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
BIN
docs/public/images/rounded-logo-bg.png
Normal file
BIN
docs/public/images/rounded-logo-bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
@ -2,9 +2,12 @@ const app = Elm.Main.init({ flags: window.__FLAGS__ })
|
||||
|
||||
// Handle smoothly scrolling to links
|
||||
const scrollToHash = () => {
|
||||
const BREAKPOINT_XL = 1920
|
||||
const NAVBAR_HEIGHT_PX = window.innerWidth > BREAKPOINT_XL ? 127 : 102
|
||||
const element = window.location.hash && document.querySelector(window.location.hash)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
// element.scrollIntoView({ behavior: 'smooth' })
|
||||
window.scroll({ behavior: 'smooth', top: window.pageYOffset + element.getBoundingClientRect().top - NAVBAR_HEIGHT_PX })
|
||||
} else {
|
||||
window.scroll({ behavior: 'smooth', top: 0 })
|
||||
}
|
||||
@ -28,11 +31,11 @@ window.addEventListener('keypress', (e) => {
|
||||
|
||||
// HighlightJS custom element
|
||||
customElements.define('prism-js', class HighlightJS extends HTMLElement {
|
||||
constructor () { super() }
|
||||
connectedCallback () {
|
||||
constructor() { super() }
|
||||
connectedCallback() {
|
||||
const pre = document.createElement('pre')
|
||||
|
||||
pre.className = `language-elm`
|
||||
pre.className = this.language ? `language-${this.language}` : `language-elm`
|
||||
pre.textContent = this.body
|
||||
|
||||
this.appendChild(pre)
|
||||
@ -42,10 +45,10 @@ customElements.define('prism-js', class HighlightJS extends HTMLElement {
|
||||
|
||||
// Dropdown arrow key support
|
||||
customElements.define('dropdown-arrow-keys', class DropdownArrowKeys extends HTMLElement {
|
||||
constructor () {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
connectedCallback () {
|
||||
connectedCallback() {
|
||||
const component = this
|
||||
const arrows = { ArrowUp: -1, ArrowDown: 1 }
|
||||
const interactiveChildren = () => component.querySelectorAll('input, a, button')
|
||||
|
@ -6,7 +6,7 @@
|
||||
--weight--light: 300;
|
||||
--weight--semibold: 600;
|
||||
--weight--bold: 800;
|
||||
|
||||
|
||||
--color--white: #ffffff;
|
||||
--color--grey-100: #f0f0f0;
|
||||
--color--grey-200: #cccccc;
|
||||
@ -15,22 +15,25 @@
|
||||
--color--grey-700: #333333;
|
||||
--color--green: #407742;
|
||||
--color--green-light: #d7ead8;
|
||||
|
||||
--size--h1: 4em;
|
||||
|
||||
--size--h1: 3em;
|
||||
--size--h2: 2em;
|
||||
--size--h3: 1.5em;
|
||||
--size--h4: 1.2em;
|
||||
--size--h5: 1.2em;
|
||||
--size--h6: 0.75em;
|
||||
--size--paragraph: 1.2em;
|
||||
|
||||
|
||||
--shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
|
||||
--shadow-dark: 0 0.5em 2em rgb(0, 0, 0, 0.2);
|
||||
|
||||
--height--header: 102px;
|
||||
}
|
||||
|
||||
/* Resets */
|
||||
@media screen and (min-width: 1920px ) {
|
||||
html { font-size: 20px; }
|
||||
:root { --height--header: 127px; }
|
||||
}
|
||||
|
||||
body {
|
||||
@ -39,6 +42,7 @@ body {
|
||||
font-family: var(--font--body);
|
||||
font-weight: var(--weight--light);
|
||||
overflow-y: scroll;
|
||||
padding-top: var(--height--header);
|
||||
}
|
||||
|
||||
* {
|
||||
@ -64,6 +68,16 @@ pre {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.aside {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-of-contents {
|
||||
min-width: 14em;
|
||||
max-width: 14em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
main {
|
||||
animation: fadeIn 200ms 400ms ease-in forwards;
|
||||
opacity: 0;
|
||||
@ -163,12 +177,14 @@ hr { border: 0; }
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.markdown p code {
|
||||
.markdown p code,
|
||||
.markdown li code {
|
||||
font-size: 0.92em;
|
||||
color: var(--color--green);
|
||||
}
|
||||
|
||||
.markdown p code::before { content: '`'; opacity: 0.75; pointer-events: none; user-select: none; }
|
||||
.markdown p code::after { content: '`'; opacity: 0.75; pointer-events: none; user-select: none; }
|
||||
.markdown p code::before, .markdown li code::before { content: '`'; opacity: 0.75; pointer-events: none; user-select: none; }
|
||||
.markdown p code::after, .markdown li code::after { content: '`'; opacity: 0.75; pointer-events: none; user-select: none; }
|
||||
|
||||
.markdown blockquote {
|
||||
padding-left: 1rem;
|
||||
@ -207,6 +223,8 @@ hr { border: 0; }
|
||||
background-color: var(--color--grey-700);
|
||||
color: var(--color--grey-100);
|
||||
padding: 1rem;
|
||||
font-size: 0.9em;
|
||||
font-family: var(--font--monospace);
|
||||
}
|
||||
|
||||
.markdown pre.language-terminal {
|
||||
@ -236,6 +254,7 @@ hr { border: 0; }
|
||||
overflow: hidden;
|
||||
background-color: var(--color--white);
|
||||
border-color: var(--color--grey-200);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.markdown tr {
|
||||
@ -243,9 +262,7 @@ hr { border: 0; }
|
||||
}
|
||||
|
||||
.markdown th,
|
||||
.markdown td {
|
||||
padding: 0.75em;
|
||||
}
|
||||
.markdown td { padding: 0.75em }
|
||||
|
||||
.markdown tbody tr:nth-child(2n + 1) {
|
||||
background-color: var(--color--grey-100);
|
||||
@ -315,10 +332,29 @@ hr { border: 0; }
|
||||
border-color: var(--color--green);
|
||||
}
|
||||
|
||||
/* STICKY SCROLLING */
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
background: var(--color--grey-100);
|
||||
box-shadow: 0 1em 1em var(--color--grey-100);
|
||||
}
|
||||
|
||||
.header__logo {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.header__logo { font-size: 1.25em }
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
top: 0;
|
||||
top: var(--height--header);
|
||||
}
|
||||
|
||||
/* Images & Icons */
|
||||
|
9
docs/public/vendor/prism.js
vendored
9
docs/public/vendor/prism.js
vendored
File diff suppressed because one or more lines are too long
@ -6,6 +6,10 @@ const config = {
|
||||
output: path.join(__dirname, '..', 'public', 'dist')
|
||||
}
|
||||
|
||||
// Terminal color output
|
||||
const green = ``
|
||||
const reset = ``
|
||||
|
||||
// Recursively lists all files in the given folder
|
||||
const listContainedFiles = async (folder) => {
|
||||
let files = []
|
||||
@ -54,7 +58,7 @@ const main = () =>
|
||||
await fs.mkdir(config.output, { recursive: true })
|
||||
return fs.writeFile(path.join(config.output, 'flags.js'), contents, { encoding: 'utf-8' })
|
||||
})
|
||||
.then(_ => console.info(`\n ✓ Indexed the content folder\n`))
|
||||
.then(_ => console.info(`\n ${green}✓${reset} Indexed the content folder\n`))
|
||||
.catch(console.error)
|
||||
|
||||
// Run the program
|
||||
|
@ -106,7 +106,8 @@ sections : Index -> List Section
|
||||
sections index =
|
||||
let
|
||||
sectionOrder =
|
||||
[ "Guide"
|
||||
[ "Docs"
|
||||
, "Guides"
|
||||
, "Examples"
|
||||
]
|
||||
|
||||
|
@ -3,10 +3,11 @@ module Main exposing (main)
|
||||
import Browser
|
||||
import Browser.Navigation as Nav exposing (Key)
|
||||
import Effect
|
||||
import Gen.Model
|
||||
import Gen.Pages as Pages
|
||||
import Gen.Route as Route
|
||||
import Ports
|
||||
import Request exposing (Request)
|
||||
import Request
|
||||
import Shared
|
||||
import Url exposing (Url)
|
||||
import View
|
||||
@ -40,7 +41,7 @@ init : Shared.Flags -> Url -> Key -> ( Model, Cmd Msg )
|
||||
init flags url key =
|
||||
let
|
||||
( shared, sharedCmd ) =
|
||||
Shared.init (request { url = url, key = key }) flags
|
||||
Shared.init (Request.create () url key) flags
|
||||
|
||||
( page, effect ) =
|
||||
Pages.init (Route.fromUrl url) shared url key
|
||||
@ -69,11 +70,7 @@ update msg model =
|
||||
case msg of
|
||||
ClickedLink (Browser.Internal url) ->
|
||||
( model
|
||||
, if url.path == model.url.path then
|
||||
Nav.replaceUrl model.key (Url.toString url)
|
||||
|
||||
else
|
||||
Nav.pushUrl model.key (Url.toString url)
|
||||
, Nav.pushUrl model.key (Url.toString url)
|
||||
)
|
||||
|
||||
ClickedLink (Browser.External url) ->
|
||||
@ -82,12 +79,7 @@ update msg model =
|
||||
)
|
||||
|
||||
ChangedUrl url ->
|
||||
if url.path == model.url.path then
|
||||
( { model | url = url }
|
||||
, Ports.onUrlChange ()
|
||||
)
|
||||
|
||||
else
|
||||
if url.path /= model.url.path then
|
||||
let
|
||||
( page, effect ) =
|
||||
Pages.init (Route.fromUrl url) model.shared url model.key
|
||||
@ -99,14 +91,31 @@ update msg model =
|
||||
]
|
||||
)
|
||||
|
||||
else
|
||||
( { model | url = url }
|
||||
, Ports.onUrlChange ()
|
||||
)
|
||||
|
||||
Shared sharedMsg ->
|
||||
let
|
||||
( shared, sharedCmd ) =
|
||||
Shared.update (request model) sharedMsg model.shared
|
||||
Shared.update (Request.create () model.url model.key) sharedMsg model.shared
|
||||
|
||||
( page, effect ) =
|
||||
Pages.init (Route.fromUrl model.url) shared model.url model.key
|
||||
in
|
||||
( { model | shared = shared }
|
||||
, Cmd.map Shared sharedCmd
|
||||
)
|
||||
if page == Gen.Model.Redirecting_ then
|
||||
( { model | shared = shared, page = page }
|
||||
, Cmd.batch
|
||||
[ Cmd.map Shared sharedCmd
|
||||
, Effect.toCmd ( Shared, Page ) effect
|
||||
]
|
||||
)
|
||||
|
||||
else
|
||||
( { model | shared = shared }
|
||||
, Cmd.map Shared sharedCmd
|
||||
)
|
||||
|
||||
Page pageMsg ->
|
||||
let
|
||||
@ -137,14 +146,5 @@ subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.batch
|
||||
[ Pages.subscriptions model.page model.shared model.url model.key |> Sub.map Page
|
||||
, Shared.subscriptions (request model) model.shared |> Sub.map Shared
|
||||
, Shared.subscriptions (Request.create () model.url model.key) model.shared |> Sub.map Shared
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- REQUESTS
|
||||
|
||||
|
||||
request : { model | url : Url, key : Key } -> Request ()
|
||||
request model =
|
||||
Request.create () model.url model.key
|
||||
|
19
docs/src/Pages/Docs.elm
Normal file
19
docs/src/Pages/Docs.elm
Normal file
@ -0,0 +1,19 @@
|
||||
module Pages.Docs exposing (Model, Msg, page)
|
||||
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With params -> Page.With Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
||||
type alias Model =
|
||||
UI.Docs.Model
|
||||
|
||||
|
||||
type alias Msg =
|
||||
UI.Docs.Msg
|
19
docs/src/Pages/Docs/Section_.elm
Normal file
19
docs/src/Pages/Docs/Section_.elm
Normal file
@ -0,0 +1,19 @@
|
||||
module Pages.Docs.Section_ exposing (Model, Msg, page)
|
||||
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With params -> Page.With Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
||||
type alias Model =
|
||||
UI.Docs.Model
|
||||
|
||||
|
||||
type alias Msg =
|
||||
UI.Docs.Msg
|
@ -1,12 +1,12 @@
|
||||
module Pages.Examples exposing (Model, Msg, page)
|
||||
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With params -> Page.With Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
@ -1,19 +0,0 @@
|
||||
module Pages.Examples.Section_ exposing (Model, Msg, page)
|
||||
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request params -> Page Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
||||
type alias Model =
|
||||
UI.Docs.Model
|
||||
|
||||
|
||||
type alias Msg =
|
||||
UI.Docs.Msg
|
@ -1,19 +0,0 @@
|
||||
module Pages.Guide exposing (Model, Msg, page)
|
||||
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request params -> Page Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
||||
type alias Model =
|
||||
UI.Docs.Model
|
||||
|
||||
|
||||
type alias Msg =
|
||||
UI.Docs.Msg
|
@ -1,19 +0,0 @@
|
||||
module Pages.Guide.Section_ exposing (Model, Msg, page)
|
||||
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request params -> Page Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
||||
type alias Model =
|
||||
UI.Docs.Model
|
||||
|
||||
|
||||
type alias Msg =
|
||||
UI.Docs.Msg
|
19
docs/src/Pages/Guides.elm
Normal file
19
docs/src/Pages/Guides.elm
Normal file
@ -0,0 +1,19 @@
|
||||
module Pages.Guides exposing (Model, Msg, page)
|
||||
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With params -> Page.With Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
||||
type alias Model =
|
||||
UI.Docs.Model
|
||||
|
||||
|
||||
type alias Msg =
|
||||
UI.Docs.Msg
|
19
docs/src/Pages/Guides/Section_.elm
Normal file
19
docs/src/Pages/Guides/Section_.elm
Normal file
@ -0,0 +1,19 @@
|
||||
module Pages.Guides.Section_ exposing (Model, Msg, page)
|
||||
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI.Docs
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With params -> Page.With Model Msg
|
||||
page =
|
||||
UI.Docs.page
|
||||
|
||||
|
||||
type alias Model =
|
||||
UI.Docs.Model
|
||||
|
||||
|
||||
type alias Msg =
|
||||
UI.Docs.Msg
|
@ -1,15 +1,15 @@
|
||||
module Pages.Home_ exposing (Model, Msg, page)
|
||||
|
||||
import Gen.Params.Home_ exposing (Params)
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import UI.Layout
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request Params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page =
|
||||
UI.Layout.page
|
||||
{ view = view
|
||||
@ -33,19 +33,16 @@ view =
|
||||
, description = "single page apps made easy"
|
||||
}
|
||||
, UI.markdown { withHeaderLinks = False } """
|
||||
## Build reliable applications.
|
||||
## A work in progress!
|
||||
|
||||
I need to verify that the line height for paragraphs is reasonable, because if it isn't then I'll need to tweak it a bit until it's actually readable.
|
||||
Only the most readable lines should be included in the __official__ [guide](/guide), ya dig?
|
||||
This next release of elm-spa isn't _quite_ ready yet. It's currently available in `beta`, and can be installed via __npm__:
|
||||
|
||||
Bippity boppity, my guy.
|
||||
```
|
||||
npm install -g elm-spa@beta
|
||||
```
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## Effortless routing.
|
||||
|
||||
Use `elm-spa` to automatically wire up routes and pages.
|
||||
For now, feel free to [read the docs](/docs), see the [incomplete guides](/guides), or check the bulleted list of [example projects](/examples)
|
||||
that aren't available yet.
|
||||
"""
|
||||
]
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
module Pages.NotFound exposing (Model, Msg, page)
|
||||
|
||||
import Gen.Params.NotFound exposing (Params)
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import UI.Layout
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request Params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page =
|
||||
UI.Layout.page
|
||||
{ view = view
|
||||
|
@ -36,7 +36,7 @@ type Msg
|
||||
-- INIT
|
||||
|
||||
|
||||
init : Request () -> Flags -> ( Model, Cmd Msg )
|
||||
init : Request -> Flags -> ( Model, Cmd Msg )
|
||||
init _ flags =
|
||||
( Model
|
||||
(flags
|
||||
@ -51,7 +51,7 @@ init _ flags =
|
||||
-- UPDATE
|
||||
|
||||
|
||||
update : Request () -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update : Request -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update request msg model =
|
||||
case msg of
|
||||
NoOp ->
|
||||
@ -62,6 +62,6 @@ update request msg model =
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Request () -> Model -> Sub Msg
|
||||
subscriptions : Request -> Model -> Sub Msg
|
||||
subscriptions request model =
|
||||
Sub.none
|
||||
|
@ -160,23 +160,35 @@ markdown options str =
|
||||
, link = link_
|
||||
, codeBlock =
|
||||
\{ body, language } ->
|
||||
if language == Just "elm" then
|
||||
Html.Keyed.node "div"
|
||||
[]
|
||||
[ ( body
|
||||
, Html.node "prism-js"
|
||||
[ Attr.property "body" (Json.string body)
|
||||
, Attr.property "language" (Json.string "elm")
|
||||
]
|
||||
[]
|
||||
)
|
||||
]
|
||||
let
|
||||
supported =
|
||||
[ "html", "css", "js", "elm" ]
|
||||
|
||||
else
|
||||
Html.pre [ Attr.class ("language-" ++ (language |> Maybe.withDefault "none")) ]
|
||||
[ Html.code [ Attr.class ("language-" ++ (language |> Maybe.withDefault "none")) ]
|
||||
[ Html.text body ]
|
||||
]
|
||||
simplePre =
|
||||
Html.pre [ Attr.class ("language-" ++ (language |> Maybe.withDefault "none")) ]
|
||||
[ Html.code [ Attr.class ("language-" ++ (language |> Maybe.withDefault "none")) ]
|
||||
[ Html.text body ]
|
||||
]
|
||||
in
|
||||
case language of
|
||||
Just lang ->
|
||||
if List.member lang supported then
|
||||
Html.Keyed.node "div"
|
||||
[]
|
||||
[ ( body
|
||||
, Html.node "prism-js"
|
||||
[ Attr.property "body" (Json.string body)
|
||||
, Attr.property "language" (Json.string lang)
|
||||
]
|
||||
[]
|
||||
)
|
||||
]
|
||||
|
||||
else
|
||||
simplePre
|
||||
|
||||
Nothing ->
|
||||
simplePre
|
||||
}
|
||||
in
|
||||
Markdown.Parser.parse str
|
||||
|
@ -1,8 +1,8 @@
|
||||
module UI.Docs exposing (Model, Msg, page)
|
||||
|
||||
import Http
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import UI.Layout
|
||||
@ -10,7 +10,7 @@ import Url exposing (Url)
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.element
|
||||
{ init = init req.url
|
||||
@ -106,7 +106,7 @@ view shared url model =
|
||||
{ title =
|
||||
case model.markdown of
|
||||
Loading ->
|
||||
"elm-spa"
|
||||
""
|
||||
|
||||
Success content ->
|
||||
let
|
||||
|
@ -13,6 +13,7 @@ module UI.Layout exposing
|
||||
|
||||
-}
|
||||
|
||||
import Gen.Route as Route exposing (Route)
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
import Page exposing (Page)
|
||||
@ -72,9 +73,9 @@ viewDocumentation :
|
||||
-> List (Html msg)
|
||||
viewDocumentation options markdownContent view =
|
||||
[ navbar options
|
||||
, Html.div [ Attr.class "container pad-lg" ]
|
||||
[ UI.row.lg [ UI.align.top, UI.padY.lg ]
|
||||
[ Html.aside [ Attr.class "only-desktop sticky pad-y-lg", Attr.style "width" "13em" ]
|
||||
, Html.div [ Attr.class "container pad-md" ]
|
||||
[ UI.row.xl [ UI.align.top, UI.padY.lg ]
|
||||
[ Html.aside [ Attr.class "only-desktop sticky pad-y-lg aside" ]
|
||||
[ UI.Sidebar.viewSidebar
|
||||
{ index = options.shared.index
|
||||
, url = options.url
|
||||
@ -83,7 +84,7 @@ viewDocumentation options markdownContent view =
|
||||
, Html.main_ [ Attr.class "flex" ]
|
||||
[ UI.row.lg [ UI.align.top ]
|
||||
[ Html.div [ Attr.class "col flex" ] view
|
||||
, Html.div [ Attr.class "hidden-mobile sticky pad-y-lg", Attr.style "width" "16em" ]
|
||||
, Html.div [ Attr.class "hidden-mobile sticky pad-y-lg table-of-contents" ]
|
||||
[ UI.Sidebar.viewTableOfContents
|
||||
{ content = markdownContent
|
||||
, url = options.url
|
||||
@ -105,23 +106,23 @@ navbar :
|
||||
-> Html msg
|
||||
navbar { onMsg, model, shared, url } =
|
||||
let
|
||||
navLink : { text : String, url : String } -> Html msg
|
||||
navLink : { text : String, route : Route } -> Html msg
|
||||
navLink options =
|
||||
Html.a
|
||||
[ Attr.class "link"
|
||||
, Attr.href options.url
|
||||
, Attr.classList [ ( "bold text-blue", String.startsWith options.url url.path ) ]
|
||||
, Attr.href (Route.toHref options.route)
|
||||
, Attr.classList [ ( "bold text-blue", String.startsWith (Route.toHref options.route) url.path ) ]
|
||||
]
|
||||
[ Html.text options.text ]
|
||||
in
|
||||
Html.header [ Attr.class "container pad-md" ]
|
||||
Html.header [ Attr.class "header container pad-y-lg pad-x-md" ]
|
||||
[ Html.div [ Attr.class "row gap-md spread" ]
|
||||
[ Html.div [ Attr.class "row align-center gap-md" ]
|
||||
[ Html.a [ Attr.href "/" ] [ UI.logo ]
|
||||
[ Html.div [ Attr.class "row align-center gap-lg" ]
|
||||
[ Html.a [ Attr.class "header__logo", Attr.href "/" ] [ UI.logo ]
|
||||
, Html.nav [ Attr.class "row gap-md hidden-mobile pad-left-xs" ]
|
||||
[ navLink { text = "guide", url = "/guide" }
|
||||
, navLink { text = "docs", url = "/docs" }
|
||||
, navLink { text = "examples", url = "/examples" }
|
||||
[ navLink { text = "docs", route = Route.Docs }
|
||||
, navLink { text = "guides ", route = Route.Guides }
|
||||
, navLink { text = "examples", route = Route.Examples }
|
||||
]
|
||||
]
|
||||
, Html.div [ Attr.class "row gap-md spread" ]
|
||||
@ -144,7 +145,7 @@ navbar { onMsg, model, shared, url } =
|
||||
-- PAGE
|
||||
|
||||
|
||||
page : { view : View Msg } -> Shared.Model -> Request params -> Page Model Msg
|
||||
page : { view : View Msg } -> Shared.Model -> Request.With params -> Page.With Model Msg
|
||||
page options shared req =
|
||||
Page.sandbox
|
||||
{ init = init
|
||||
|
@ -122,8 +122,13 @@ viewSidebar { url, index } =
|
||||
|
||||
viewSidebarSection : Section -> Html msg
|
||||
viewSidebarSection section =
|
||||
UI.col.sm []
|
||||
[ Html.a [ Attr.href section.url, Attr.class "h4 bold" ] [ Html.text section.header ]
|
||||
UI.col.sm [ UI.align.left ]
|
||||
[ Html.a
|
||||
[ Attr.href section.url
|
||||
, Attr.classList [ ( "bold text-blue", url.path == section.url ) ]
|
||||
, Attr.class "h4 bold underline"
|
||||
]
|
||||
[ Html.text section.header ]
|
||||
, if List.isEmpty section.pages then
|
||||
Html.text ""
|
||||
|
||||
|
2
elm.json
2
elm.json
@ -3,7 +3,7 @@
|
||||
"name": "ryannhg/elm-spa",
|
||||
"summary": "Single page apps made easy.",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "5.2.0",
|
||||
"version": "5.3.0",
|
||||
"exposed-modules": [
|
||||
"ElmSpa.Request",
|
||||
"ElmSpa.Page",
|
||||
|
5
examples/01-hello-world/.gitignore
vendored
Normal file
5
examples/01-hello-world/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.elm-spa
|
||||
elm-stuff
|
||||
node_modules
|
||||
dist
|
@ -1,16 +1,28 @@
|
||||
# examples/01-hello-world
|
||||
> A web application made with [elm-spa](https://elm-spa.dev)
|
||||
# my new project
|
||||
> 🌳 built with [elm-spa](https://elm-spa.dev)
|
||||
|
||||
## dependencies
|
||||
|
||||
This project requires the latest LTS version of [Node.js](https://nodejs.org/)
|
||||
|
||||
```bash
|
||||
npm install -g elm elm-spa
|
||||
```
|
||||
|
||||
## running locally
|
||||
|
||||
```bash
|
||||
elm-spa server
|
||||
elm-spa server # starts this app at http:/localhost:1234
|
||||
```
|
||||
|
||||
### other commands
|
||||
|
||||
```bash
|
||||
elm-spa add <url> # add a new page
|
||||
elm-spa build # production build
|
||||
elm-spa watch # compile as you code, without the server!
|
||||
elm-spa add # add a new page to the application
|
||||
elm-spa build # production build
|
||||
elm-spa watch # runs build as you code (without the server)
|
||||
```
|
||||
|
||||
## learn more
|
||||
|
||||
You can learn more at [elm-spa.dev](https://elm-spa.dev)
|
@ -3,8 +3,7 @@
|
||||
"source-directories": [
|
||||
"src",
|
||||
".elm-spa/defaults",
|
||||
".elm-spa/generated",
|
||||
"../../src"
|
||||
".elm-spa/generated"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
@ -13,7 +12,8 @@
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/url": "1.0.0"
|
||||
"elm/url": "1.0.0",
|
||||
"ryannhg/elm-spa": "5.3.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/time": "1.0.0",
|
||||
|
@ -1,11 +1,11 @@
|
||||
module Pages.Home_ exposing (view)
|
||||
|
||||
import Html
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body =
|
||||
[ Html.text "Hello, world!"
|
||||
]
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
}
|
||||
|
@ -11,11 +11,14 @@
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/http": "2.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/url": "1.0.0",
|
||||
"ryannhg/elm-spa": "5.2.0"
|
||||
"ryannhg/elm-spa": "5.3.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/bytes": "1.0.8",
|
||||
"elm/file": "1.0.5",
|
||||
"elm/time": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2"
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="/dist/elm.js"></script>
|
||||
|
27
examples/02-pages/public/style.css
Normal file
27
examples/02-pages/public/style.css
Normal file
@ -0,0 +1,27 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar .brand {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar .splitter { flex: 1 1 auto; }
|
||||
|
||||
.navbar a {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin-top: 3rem;
|
||||
}
|
86
examples/02-pages/src/Pages/Advanced.elm
Normal file
86
examples/02-pages/src/Pages/Advanced.elm
Normal file
@ -0,0 +1,86 @@
|
||||
module Pages.Advanced exposing (Model, Msg, page)
|
||||
|
||||
import Effect exposing (Effect)
|
||||
import Gen.Params.Advanced exposing (Params)
|
||||
import Html
|
||||
import Html.Events as Events
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.advanced
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view shared
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INIT
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
init : ( Model, Effect Msg )
|
||||
init =
|
||||
( {}, Effect.none )
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= IncrementShared
|
||||
| DecrementShared
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Effect Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
IncrementShared ->
|
||||
( model
|
||||
, Effect.fromShared Shared.Increment
|
||||
)
|
||||
|
||||
DecrementShared ->
|
||||
( model
|
||||
, Effect.fromShared Shared.Decrement
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.none
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Shared.Model -> Model -> View Msg
|
||||
view shared model =
|
||||
{ title = "Advanced"
|
||||
, body =
|
||||
UI.layout
|
||||
[ UI.h1 "Advanced"
|
||||
, Html.p [] [ Html.text "An advanced page uses Effects instead of Cmds, which allow you to send Shared messages directly from a page." ]
|
||||
, Html.h2 [] [ Html.text "Shared Counter" ]
|
||||
, Html.h3 [] [ Html.text (String.fromInt shared.counter) ]
|
||||
, Html.button [ Events.onClick DecrementShared ] [ Html.text "-" ]
|
||||
, Html.button [ Events.onClick IncrementShared ] [ Html.text "+" ]
|
||||
, Html.p [] [ Html.text "This value doesn't reset as you navigate from one page to another (but will on page refresh)!" ]
|
||||
]
|
||||
}
|
30
examples/02-pages/src/Pages/Dynamic/Name_.elm
Normal file
30
examples/02-pages/src/Pages/Dynamic/Name_.elm
Normal file
@ -0,0 +1,30 @@
|
||||
module Pages.Dynamic.Name_ exposing (page)
|
||||
|
||||
import Gen.Params.Dynamic.Name_ exposing (Params)
|
||||
import Html exposing (Html)
|
||||
import Page exposing (Page)
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With Params -> Page
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view req.params
|
||||
}
|
||||
|
||||
|
||||
view : Params -> View msg
|
||||
view params =
|
||||
{ title = "Dynamic: " ++ params.name
|
||||
, body =
|
||||
UI.layout
|
||||
[ UI.h1 "Dynamic Page"
|
||||
, Html.p [] [ Html.text "Dynamic pages with underscores can safely access URL parameters." ]
|
||||
, Html.p [] [ Html.text "Because this file is named \"Name_.elm\", it has a \"name\" parameter." ]
|
||||
, Html.p [] [ Html.text "Try changing the URL above to something besides \"apple\" or \"banana\"! " ]
|
||||
, Html.h2 [] [ Html.text params.name ]
|
||||
]
|
||||
}
|
@ -1,13 +1,22 @@
|
||||
module Pages.Element exposing (Model, Msg, page)
|
||||
|
||||
import Browser.Dom exposing (Viewport)
|
||||
import Browser.Events
|
||||
import Gen.Params.Element exposing (Params)
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Html
|
||||
import Html.Attributes as Attr
|
||||
import Html.Events as Events
|
||||
import Http
|
||||
import Json.Decode as Json
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import Task
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request Params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.element
|
||||
{ init = init
|
||||
@ -22,12 +31,25 @@ page shared req =
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
{ window : { width : Int, height : Int }
|
||||
, image : WebRequest
|
||||
}
|
||||
|
||||
|
||||
type WebRequest
|
||||
= NotAsked
|
||||
| Success String
|
||||
| Failure
|
||||
|
||||
|
||||
init : ( Model, Cmd Msg )
|
||||
init =
|
||||
( {}, Cmd.none )
|
||||
( { window = { width = 0, height = 0 }
|
||||
, image = NotAsked
|
||||
}
|
||||
, Browser.Dom.getViewport
|
||||
|> Task.perform GotInitialViewport
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -35,14 +57,52 @@ init =
|
||||
|
||||
|
||||
type Msg
|
||||
= ReplaceMe
|
||||
= ResizedWindow Int Int
|
||||
| GotInitialViewport Viewport
|
||||
| ClickedFetchCat
|
||||
| GotCatGif (Result Http.Error String)
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
ReplaceMe ->
|
||||
( model, Cmd.none )
|
||||
GotInitialViewport { viewport } ->
|
||||
( { model
|
||||
| window =
|
||||
{ width = floor viewport.width
|
||||
, height = floor viewport.height
|
||||
}
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ResizedWindow w h ->
|
||||
( { model | window = { width = w, height = h } }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ClickedFetchCat ->
|
||||
let
|
||||
gifDecoder =
|
||||
Json.field "url" Json.string
|
||||
|> Json.map (\url -> "https://cataas.com" ++ url)
|
||||
in
|
||||
( model
|
||||
, Http.get
|
||||
{ url = "https://cataas.com/cat?json=true&type=sm"
|
||||
, expect = Http.expectJson GotCatGif gifDecoder
|
||||
}
|
||||
)
|
||||
|
||||
GotCatGif (Ok url) ->
|
||||
( { model | image = Success url }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
GotCatGif (Err _) ->
|
||||
( { model | image = Failure }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -51,7 +111,7 @@ update msg model =
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.none
|
||||
Browser.Events.onResize ResizedWindow
|
||||
|
||||
|
||||
|
||||
@ -60,4 +120,35 @@ subscriptions model =
|
||||
|
||||
view : Model -> View Msg
|
||||
view model =
|
||||
View.placeholder "Element"
|
||||
{ title = "Element"
|
||||
, body =
|
||||
UI.layout
|
||||
[ UI.h1 "Element"
|
||||
, Html.p [] [ Html.text "An element page can perform side-effects like HTTP requests and subscribe to events from the browser!" ]
|
||||
, Html.br [] []
|
||||
, Html.h2 [] [ Html.text "Commands" ]
|
||||
, Html.p []
|
||||
[ Html.button [ Events.onClick ClickedFetchCat ] [ Html.text "Get a cat" ]
|
||||
]
|
||||
, case model.image of
|
||||
NotAsked ->
|
||||
Html.text ""
|
||||
|
||||
Failure ->
|
||||
Html.text "Something went wrong, please try again."
|
||||
|
||||
Success image ->
|
||||
Html.img [ Attr.src image, Attr.alt "Cat" ] []
|
||||
, Html.br [] []
|
||||
, Html.h2 [] [ Html.text "Subscriptions" ]
|
||||
, Html.p []
|
||||
[ Html.strong [] [ Html.text "Window size:" ]
|
||||
, Html.text (windowSizeToString model.window)
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
windowSizeToString : { width : Int, height : Int } -> String
|
||||
windowSizeToString { width, height } =
|
||||
"( " ++ String.fromInt width ++ ", " ++ String.fromInt height ++ " )"
|
||||
|
@ -1,11 +1,16 @@
|
||||
module Pages.Home_ exposing (view)
|
||||
|
||||
import Html
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
view : View Never
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
, body =
|
||||
UI.layout
|
||||
[ Html.h1 [] [ Html.text "Homepage" ]
|
||||
, Html.p [] [ Html.text "This homepage is just a view function, click the links in the navbar to see more pages!" ]
|
||||
]
|
||||
}
|
||||
|
71
examples/02-pages/src/Pages/Sandbox.elm
Normal file
71
examples/02-pages/src/Pages/Sandbox.elm
Normal file
@ -0,0 +1,71 @@
|
||||
module Pages.Sandbox exposing (Model, Msg, page)
|
||||
|
||||
import Gen.Params.Sandbox exposing (Params)
|
||||
import Html
|
||||
import Html.Events
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.sandbox
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INIT
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ counter : Int
|
||||
}
|
||||
|
||||
|
||||
init : Model
|
||||
init =
|
||||
{ counter = 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
view : Model -> View Msg
|
||||
view model =
|
||||
{ title = "Sandbox"
|
||||
, body =
|
||||
UI.layout
|
||||
[ UI.h1 "Sandbox"
|
||||
, Html.p [] [ Html.text "A sandbox page can keep track of state!" ]
|
||||
, Html.h3 [] [ Html.text (String.fromInt model.counter) ]
|
||||
, Html.button [ Html.Events.onClick Decrement ] [ Html.text "-" ]
|
||||
, Html.button [ Html.Events.onClick Increment ] [ Html.text "+" ]
|
||||
]
|
||||
}
|
@ -1,14 +1,27 @@
|
||||
module Pages.Static exposing (page)
|
||||
|
||||
import Page
|
||||
import View
|
||||
import Gen.Params.Static exposing (Params)
|
||||
import Html
|
||||
import Page exposing (Page)
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request -> Page
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view
|
||||
}
|
||||
|
||||
|
||||
view : View msg
|
||||
view =
|
||||
View.placeholder "Static"
|
||||
{ title = "Static"
|
||||
, body =
|
||||
UI.layout
|
||||
[ UI.h1 "Static"
|
||||
, Html.p [] [ Html.text "A static page only renders a view, but has access to shared state and URL information." ]
|
||||
]
|
||||
}
|
||||
|
49
examples/02-pages/src/Shared.elm
Normal file
49
examples/02-pages/src/Shared.elm
Normal file
@ -0,0 +1,49 @@
|
||||
module Shared exposing
|
||||
( Flags
|
||||
, Model
|
||||
, Msg(..)
|
||||
, init
|
||||
, subscriptions
|
||||
, update
|
||||
)
|
||||
|
||||
import Json.Decode as Json
|
||||
import Request exposing (Request)
|
||||
|
||||
|
||||
type alias Flags =
|
||||
Json.Value
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ counter : Int
|
||||
}
|
||||
|
||||
|
||||
type Msg
|
||||
= Increment
|
||||
| Decrement
|
||||
|
||||
|
||||
init : Request -> Flags -> ( Model, Cmd Msg )
|
||||
init _ _ =
|
||||
( { counter = 0 }, Cmd.none )
|
||||
|
||||
|
||||
update : Request -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update _ msg model =
|
||||
case msg of
|
||||
Increment ->
|
||||
( { model | counter = model.counter + 1 }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
Decrement ->
|
||||
( { model | counter = model.counter - 1 }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
subscriptions : Request -> Model -> Sub Msg
|
||||
subscriptions _ _ =
|
||||
Sub.none
|
33
examples/02-pages/src/UI.elm
Normal file
33
examples/02-pages/src/UI.elm
Normal file
@ -0,0 +1,33 @@
|
||||
module UI exposing (h1, layout)
|
||||
|
||||
import Gen.Route as Route exposing (Route)
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
|
||||
|
||||
layout : List (Html msg) -> List (Html msg)
|
||||
layout children =
|
||||
let
|
||||
viewLink : String -> Route -> Html msg
|
||||
viewLink label route =
|
||||
Html.a [ Attr.href (Route.toHref route) ] [ Html.text label ]
|
||||
in
|
||||
[ Html.div [ Attr.class "container" ]
|
||||
[ Html.header [ Attr.class "navbar" ]
|
||||
[ Html.strong [ Attr.class "brand" ] [ viewLink "Home" Route.Home_ ]
|
||||
, viewLink "Static" Route.Static
|
||||
, viewLink "Sandbox" Route.Sandbox
|
||||
, viewLink "Element" Route.Element
|
||||
, viewLink "Advanced" Route.Advanced
|
||||
, Html.div [ Attr.class "splitter" ] []
|
||||
, viewLink "Dynamic: Apple" (Route.Dynamic__Name_ { name = "apple" })
|
||||
, viewLink "Dynamic: Banana" (Route.Dynamic__Name_ { name = "banana" })
|
||||
]
|
||||
, Html.main_ [] children
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
h1 : String -> Html msg
|
||||
h1 label =
|
||||
Html.h1 [] [ Html.text label ]
|
@ -13,7 +13,7 @@
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/url": "1.0.0",
|
||||
"ryannhg/elm-spa": "5.2.0"
|
||||
"ryannhg/elm-spa": "5.3.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/time": "1.0.0",
|
||||
|
5
examples/04-demo/.gitignore
vendored
Normal file
5
examples/04-demo/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.DS_Store
|
||||
.elm-spa
|
||||
elm-stuff
|
||||
node_modules
|
||||
dist
|
28
examples/04-demo/README.md
Normal file
28
examples/04-demo/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# my new project
|
||||
> 🌳 built with [elm-spa](https://elm-spa.dev)
|
||||
|
||||
## dependencies
|
||||
|
||||
This project requires the latest LTS version of [Node.js](https://nodejs.org/)
|
||||
|
||||
```bash
|
||||
npm install -g elm elm-spa
|
||||
```
|
||||
|
||||
## running locally
|
||||
|
||||
```bash
|
||||
elm-spa server # starts this app at http:/localhost:1234
|
||||
```
|
||||
|
||||
### other commands
|
||||
|
||||
```bash
|
||||
elm-spa add # add a new page to the application
|
||||
elm-spa build # production build
|
||||
elm-spa watch # runs build as you code (without the server)
|
||||
```
|
||||
|
||||
## learn more
|
||||
|
||||
You can learn more at [elm-spa.dev](https://elm-spa.dev)
|
27
examples/04-demo/elm.json
Normal file
27
examples/04-demo/elm.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "application",
|
||||
"source-directories": [
|
||||
"src",
|
||||
".elm-spa/defaults",
|
||||
".elm-spa/generated",
|
||||
"../../src"
|
||||
],
|
||||
"elm-version": "0.19.1",
|
||||
"dependencies": {
|
||||
"direct": {
|
||||
"elm/browser": "1.0.2",
|
||||
"elm/core": "1.0.5",
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/url": "1.0.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/time": "1.0.0",
|
||||
"elm/virtual-dom": "1.0.2"
|
||||
}
|
||||
},
|
||||
"test-dependencies": {
|
||||
"direct": {},
|
||||
"indirect": {}
|
||||
}
|
||||
}
|
12
examples/04-demo/public/index.html
Normal file
12
examples/04-demo/public/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<script src="/dist/elm.js"></script>
|
||||
<script> Elm.Main.init() </script>
|
||||
</body>
|
||||
</html>
|
29
examples/04-demo/public/style.css
Normal file
29
examples/04-demo/public/style.css
Normal file
@ -0,0 +1,29 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 960px;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar .brand {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navbar .splitter { flex: 1 1 auto; }
|
||||
|
||||
.navbar a {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
main { margin-top: 3rem; }
|
||||
|
||||
h1, h2 {
|
||||
margin-top: 3rem;
|
||||
}
|
34
examples/04-demo/src/Auth.elm
Normal file
34
examples/04-demo/src/Auth.elm
Normal file
@ -0,0 +1,34 @@
|
||||
module Auth exposing
|
||||
( User
|
||||
, beforeProtectedInit
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs User
|
||||
@docs beforeProtectedInit
|
||||
|
||||
-}
|
||||
|
||||
import ElmSpa.Internals.Page as ElmSpa
|
||||
import Gen.Route exposing (Route)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
|
||||
|
||||
type alias User =
|
||||
Shared.User
|
||||
|
||||
|
||||
beforeProtectedInit : Shared.Model -> Request -> ElmSpa.Protected User Route
|
||||
beforeProtectedInit shared req =
|
||||
let
|
||||
_ =
|
||||
Debug.log "user" shared.user
|
||||
in
|
||||
case shared.user of
|
||||
Just user ->
|
||||
ElmSpa.Provide user
|
||||
|
||||
Nothing ->
|
||||
ElmSpa.RedirectTo Gen.Route.SignIn
|
63
examples/04-demo/src/Pages/Home_.elm
Normal file
63
examples/04-demo/src/Pages/Home_.elm
Normal file
@ -0,0 +1,63 @@
|
||||
module Pages.Home_ exposing (Model, Msg, page, view)
|
||||
|
||||
import Auth
|
||||
import Effect exposing (Effect)
|
||||
import Html
|
||||
import Html.Events as Events
|
||||
import Page
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request -> Page.With Model Msg
|
||||
page _ _ =
|
||||
Page.protected.advanced <|
|
||||
\user ->
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INIT
|
||||
|
||||
|
||||
type alias Model =
|
||||
{}
|
||||
|
||||
|
||||
init : ( Model, Effect Msg )
|
||||
init =
|
||||
( {}, Effect.none )
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= ClickedSignOut
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Effect Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
ClickedSignOut ->
|
||||
( model
|
||||
, Effect.fromShared Shared.SignedOut
|
||||
)
|
||||
|
||||
|
||||
view : Auth.User -> Model -> View Msg
|
||||
view user _ =
|
||||
{ title = "Homepage"
|
||||
, body =
|
||||
UI.layout
|
||||
[ Html.h1 [] [ Html.text ("Hello, " ++ user.name ++ "!") ]
|
||||
, Html.button [ Events.onClick ClickedSignOut ] [ Html.text "Sign out" ]
|
||||
]
|
||||
}
|
95
examples/04-demo/src/Pages/SignIn.elm
Normal file
95
examples/04-demo/src/Pages/SignIn.elm
Normal file
@ -0,0 +1,95 @@
|
||||
module Pages.SignIn exposing (Model, Msg, page)
|
||||
|
||||
import Effect exposing (Effect)
|
||||
import Gen.Params.SignIn exposing (Params)
|
||||
import Html
|
||||
import Html.Attributes as Attr
|
||||
import Html.Events as Events
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import UI
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.advanced
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
, subscriptions = subscriptions
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INIT
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ name : String }
|
||||
|
||||
|
||||
init : ( Model, Effect Msg )
|
||||
init =
|
||||
( { name = "" }
|
||||
, Effect.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= UpdatedName String
|
||||
| SubmittedSignInForm
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Effect Msg )
|
||||
update msg model =
|
||||
case msg of
|
||||
UpdatedName name ->
|
||||
( { model | name = name }
|
||||
, Effect.none
|
||||
)
|
||||
|
||||
SubmittedSignInForm ->
|
||||
( model
|
||||
, Effect.fromShared (Shared.SignedIn model.name)
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
||||
|
||||
subscriptions : Model -> Sub Msg
|
||||
subscriptions model =
|
||||
Sub.none
|
||||
|
||||
|
||||
|
||||
-- VIEW
|
||||
|
||||
|
||||
view : Model -> View Msg
|
||||
view model =
|
||||
{ title = "Sign in"
|
||||
, body =
|
||||
UI.layout
|
||||
[ Html.form [ Events.onSubmit SubmittedSignInForm ]
|
||||
[ Html.label []
|
||||
[ Html.span [] [ Html.text "Name" ]
|
||||
, Html.input
|
||||
[ Attr.type_ "text"
|
||||
, Attr.value model.name
|
||||
, Events.onInput UpdatedName
|
||||
]
|
||||
[]
|
||||
]
|
||||
, Html.button [ Attr.disabled (String.isEmpty model.name) ]
|
||||
[ Html.text "Sign in" ]
|
||||
]
|
||||
]
|
||||
}
|
66
examples/04-demo/src/Shared.elm
Normal file
66
examples/04-demo/src/Shared.elm
Normal file
@ -0,0 +1,66 @@
|
||||
module Shared exposing
|
||||
( Flags
|
||||
, Model
|
||||
, Msg(..)
|
||||
, User
|
||||
, init
|
||||
, subscriptions
|
||||
, update
|
||||
)
|
||||
|
||||
import Gen.Route
|
||||
import Json.Decode as Json
|
||||
import Request exposing (Request)
|
||||
|
||||
|
||||
type alias User =
|
||||
{ name : String
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- INIT
|
||||
|
||||
|
||||
type alias Flags =
|
||||
Json.Value
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ user : Maybe User
|
||||
}
|
||||
|
||||
|
||||
init : Request -> Flags -> ( Model, Cmd Msg )
|
||||
init _ _ =
|
||||
( { user = Nothing }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
|
||||
-- UPDATE
|
||||
|
||||
|
||||
type Msg
|
||||
= SignedIn String
|
||||
| SignedOut
|
||||
|
||||
|
||||
update : Request -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update req msg model =
|
||||
case msg of
|
||||
SignedIn name ->
|
||||
( { model | user = Just { name = name } }
|
||||
, Request.pushRoute Gen.Route.Home_ req
|
||||
)
|
||||
|
||||
SignedOut ->
|
||||
( { model | user = Nothing }
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
|
||||
subscriptions : Request -> Model -> Sub Msg
|
||||
subscriptions _ _ =
|
||||
Sub.none
|
27
examples/04-demo/src/UI.elm
Normal file
27
examples/04-demo/src/UI.elm
Normal file
@ -0,0 +1,27 @@
|
||||
module UI exposing (h1, layout)
|
||||
|
||||
import Gen.Route as Route exposing (Route)
|
||||
import Html exposing (Html)
|
||||
import Html.Attributes as Attr
|
||||
|
||||
|
||||
layout : List (Html msg) -> List (Html msg)
|
||||
layout children =
|
||||
let
|
||||
viewLink : String -> Route -> Html msg
|
||||
viewLink label route =
|
||||
Html.a [ Attr.href (Route.toHref route) ] [ Html.text label ]
|
||||
in
|
||||
[ Html.div [ Attr.class "container" ]
|
||||
[ Html.header [ Attr.class "navbar" ]
|
||||
[ Html.strong [ Attr.class "brand" ] [ viewLink "Home" Route.Home_ ]
|
||||
, viewLink "Sign in" Route.SignIn
|
||||
]
|
||||
, Html.main_ [] children
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
h1 : String -> Html msg
|
||||
h1 label =
|
||||
Html.h1 [] [ Html.text label ]
|
@ -1,8 +1,8 @@
|
||||
module ElmSpa.Internals.Page exposing
|
||||
( Page, static, sandbox, element, advanced
|
||||
, Protected(..), protected2
|
||||
, Protected(..), protected3
|
||||
, Bundle, bundle
|
||||
, protected
|
||||
, protected, protected2
|
||||
)
|
||||
|
||||
{-|
|
||||
@ -15,7 +15,7 @@ module ElmSpa.Internals.Page exposing
|
||||
|
||||
# **User Authentication**
|
||||
|
||||
@docs Protected, protected2
|
||||
@docs Protected, protected3
|
||||
|
||||
|
||||
# For generated code
|
||||
@ -27,7 +27,7 @@ module ElmSpa.Internals.Page exposing
|
||||
|
||||
This will be removed before release, included to prevent bumping to 6.0.0 during beta!
|
||||
|
||||
@docs protected
|
||||
@docs protected, protected2
|
||||
|
||||
-}
|
||||
|
||||
@ -71,15 +71,7 @@ static :
|
||||
}
|
||||
-> Page shared route effect view () msg
|
||||
static none page =
|
||||
Page
|
||||
(\_ _ ->
|
||||
Ok
|
||||
{ init = \_ -> ( (), none )
|
||||
, update = \_ _ -> ( (), none )
|
||||
, view = \_ -> page.view
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
Page (\_ _ -> Ok (adapters.static none page))
|
||||
|
||||
|
||||
{-| A page that can keep track of application state.
|
||||
@ -110,15 +102,7 @@ sandbox :
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
sandbox none page =
|
||||
Page
|
||||
(\_ _ ->
|
||||
Ok
|
||||
{ init = \_ -> ( page.init, none )
|
||||
, update = \msg model -> ( page.update msg model, none )
|
||||
, view = page.view
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
Page (\_ _ -> Ok (adapters.sandbox none page))
|
||||
|
||||
|
||||
{-| A page that can handle effects like [HTTP requests or subscriptions](https://guide.elm-lang.org/effects/).
|
||||
@ -152,15 +136,7 @@ element :
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
element fromCmd page =
|
||||
Page
|
||||
(\_ _ ->
|
||||
Ok
|
||||
{ init = \_ -> page.init |> Tuple.mapSecond fromCmd
|
||||
, update = \msg model -> page.update msg model |> Tuple.mapSecond fromCmd
|
||||
, view = page.view
|
||||
, subscriptions = page.subscriptions
|
||||
}
|
||||
)
|
||||
Page (\_ _ -> Ok (adapters.element fromCmd page))
|
||||
|
||||
|
||||
{-| A page that can handles **custom** effects like sending a `Shared.Msg` or other general user-defined effects.
|
||||
@ -191,101 +167,7 @@ advanced :
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
advanced page =
|
||||
Page
|
||||
(\_ _ ->
|
||||
Ok
|
||||
{ init = always page.init
|
||||
, update = page.update
|
||||
, view = page.view
|
||||
, subscriptions = page.subscriptions
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
{-| Deprecated! Will be replaced by [protected2](#protected2)
|
||||
-}
|
||||
protected :
|
||||
{ effectNone : effect
|
||||
, fromCmd : Cmd msg -> effect
|
||||
, user : shared -> Request route () -> Maybe user
|
||||
, route : route
|
||||
}
|
||||
->
|
||||
{ static :
|
||||
{ view : user -> view
|
||||
}
|
||||
-> Page shared route effect view () msg
|
||||
, sandbox :
|
||||
{ init : user -> model
|
||||
, update : user -> msg -> model -> model
|
||||
, view : user -> model -> view
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
, element :
|
||||
{ init : user -> ( model, Cmd msg )
|
||||
, update : user -> msg -> model -> ( model, Cmd msg )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
, advanced :
|
||||
{ init : user -> ( model, effect )
|
||||
, update : user -> msg -> model -> ( model, effect )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
}
|
||||
protected options =
|
||||
let
|
||||
protect pageWithUser page =
|
||||
Page
|
||||
(\shared req ->
|
||||
case options.user shared req of
|
||||
Just user ->
|
||||
Ok (pageWithUser user page)
|
||||
|
||||
Nothing ->
|
||||
Err options.route
|
||||
)
|
||||
in
|
||||
{ static =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( (), options.effectNone )
|
||||
, update = \_ model -> ( model, options.effectNone )
|
||||
, view = \_ -> page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, sandbox =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( page.init user, options.effectNone )
|
||||
, update = \msg model -> ( page.update user msg model, options.effectNone )
|
||||
, view = page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, element =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user |> Tuple.mapSecond options.fromCmd
|
||||
, update = \msg model -> page.update user msg model |> Tuple.mapSecond options.fromCmd
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
, advanced =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user
|
||||
, update = page.update user
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
}
|
||||
Page (\_ _ -> Ok (adapters.advanced page))
|
||||
|
||||
|
||||
{-| Actions to take when a user visits a `protected` page
|
||||
@ -311,101 +193,88 @@ type Protected user route
|
||||
|
||||
Prefixing any of the four functions above with `protected` will guarantee that the page has access to a user. Here's an example with `sandbox`:
|
||||
|
||||
import Page
|
||||
-- before
|
||||
Page.sandbox
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
}
|
||||
|
||||
page : Page Model Msg
|
||||
page =
|
||||
Page.protected.sandbox
|
||||
-- after
|
||||
Page.protected.sandbox
|
||||
(\user ->
|
||||
{ init = init
|
||||
, update = update
|
||||
, view = view
|
||||
}
|
||||
)
|
||||
|
||||
-- init : User -> Model
|
||||
-- update : User -> Msg -> Model -> Model
|
||||
-- update : User -> Model -> View Msg
|
||||
-- other functions have same API
|
||||
init : Model
|
||||
update : Msg -> Model -> Model
|
||||
view : Model -> View Msg
|
||||
|
||||
-}
|
||||
protected2 :
|
||||
protected3 :
|
||||
{ effectNone : effect
|
||||
, fromCmd : Cmd msg -> effect
|
||||
, beforeInit : shared -> Request route () -> Protected user route
|
||||
}
|
||||
->
|
||||
{ static :
|
||||
{ view : user -> view
|
||||
}
|
||||
(user
|
||||
->
|
||||
{ view : view
|
||||
}
|
||||
)
|
||||
-> Page shared route effect view () msg
|
||||
, sandbox :
|
||||
{ init : user -> model
|
||||
, update : user -> msg -> model -> model
|
||||
, view : user -> model -> view
|
||||
}
|
||||
(user
|
||||
->
|
||||
{ init : model
|
||||
, update : msg -> model -> model
|
||||
, view : model -> view
|
||||
}
|
||||
)
|
||||
-> Page shared route effect view model msg
|
||||
, element :
|
||||
{ init : user -> ( model, Cmd msg )
|
||||
, update : user -> msg -> model -> ( model, Cmd msg )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
(user
|
||||
->
|
||||
{ init : ( model, Cmd msg )
|
||||
, update : msg -> model -> ( model, Cmd msg )
|
||||
, view : model -> view
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
)
|
||||
-> Page shared route effect view model msg
|
||||
, advanced :
|
||||
{ init : user -> ( model, effect )
|
||||
, update : user -> msg -> model -> ( model, effect )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
(user
|
||||
->
|
||||
{ init : ( model, effect )
|
||||
, update : msg -> model -> ( model, effect )
|
||||
, view : model -> view
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
)
|
||||
-> Page shared route effect view model msg
|
||||
}
|
||||
protected2 options =
|
||||
protected3 options =
|
||||
let
|
||||
protect pageWithUser page =
|
||||
protect toPage toRecord =
|
||||
Page
|
||||
(\shared req ->
|
||||
case options.beforeInit shared req of
|
||||
Provide user ->
|
||||
Ok (pageWithUser user page)
|
||||
Ok (user |> toRecord |> toPage)
|
||||
|
||||
RedirectTo route ->
|
||||
Err route
|
||||
)
|
||||
in
|
||||
{ static =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( (), options.effectNone )
|
||||
, update = \_ model -> ( model, options.effectNone )
|
||||
, view = \_ -> page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, sandbox =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( page.init user, options.effectNone )
|
||||
, update = \msg model -> ( page.update user msg model, options.effectNone )
|
||||
, view = page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, element =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user |> Tuple.mapSecond options.fromCmd
|
||||
, update = \msg model -> page.update user msg model |> Tuple.mapSecond options.fromCmd
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
, advanced =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user
|
||||
, update = page.update user
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
{ static = protect (adapters.static options.effectNone)
|
||||
, sandbox = protect (adapters.sandbox options.effectNone)
|
||||
, element = protect (adapters.element options.fromCmd)
|
||||
, advanced = protect adapters.advanced
|
||||
}
|
||||
|
||||
|
||||
@ -527,3 +396,242 @@ type alias PageRecord effect view model msg =
|
||||
, view : model -> view
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
|
||||
|
||||
adapters :
|
||||
{ static :
|
||||
effect
|
||||
->
|
||||
{ view : view
|
||||
}
|
||||
-> PageRecord effect view () msg
|
||||
, sandbox :
|
||||
effect
|
||||
->
|
||||
{ init : model
|
||||
, update : msg -> model -> model
|
||||
, view : model -> view
|
||||
}
|
||||
-> PageRecord effect view model msg
|
||||
, element :
|
||||
(Cmd msg -> effect)
|
||||
->
|
||||
{ init : ( model, Cmd msg )
|
||||
, update : msg -> model -> ( model, Cmd msg )
|
||||
, view : model -> view
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
-> PageRecord effect view model msg
|
||||
, advanced :
|
||||
{ init : ( model, effect )
|
||||
, update : msg -> model -> ( model, effect )
|
||||
, view : model -> view
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
-> PageRecord effect view model msg
|
||||
}
|
||||
adapters =
|
||||
{ static =
|
||||
\none page ->
|
||||
{ init = \_ -> ( (), none )
|
||||
, update = \_ _ -> ( (), none )
|
||||
, view = \_ -> page.view
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
, sandbox =
|
||||
\none page ->
|
||||
{ init = \_ -> ( page.init, none )
|
||||
, update = \msg model -> ( page.update msg model, none )
|
||||
, view = page.view
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
, element =
|
||||
\fromCmd page ->
|
||||
{ init = \_ -> page.init |> Tuple.mapSecond fromCmd
|
||||
, update = \msg model -> page.update msg model |> Tuple.mapSecond fromCmd
|
||||
, view = page.view
|
||||
, subscriptions = page.subscriptions
|
||||
}
|
||||
, advanced =
|
||||
\page ->
|
||||
{ init = always page.init
|
||||
, update = page.update
|
||||
, view = page.view
|
||||
, subscriptions = page.subscriptions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-- DEPRECATED - will be removed in v6
|
||||
|
||||
|
||||
{-| Deprecated! Will be replaced by [protected3](#protected3)
|
||||
-}
|
||||
protected :
|
||||
{ effectNone : effect
|
||||
, fromCmd : Cmd msg -> effect
|
||||
, user : shared -> Request route () -> Maybe user
|
||||
, route : route
|
||||
}
|
||||
->
|
||||
{ static :
|
||||
{ view : user -> view
|
||||
}
|
||||
-> Page shared route effect view () msg
|
||||
, sandbox :
|
||||
{ init : user -> model
|
||||
, update : user -> msg -> model -> model
|
||||
, view : user -> model -> view
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
, element :
|
||||
{ init : user -> ( model, Cmd msg )
|
||||
, update : user -> msg -> model -> ( model, Cmd msg )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
, advanced :
|
||||
{ init : user -> ( model, effect )
|
||||
, update : user -> msg -> model -> ( model, effect )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
}
|
||||
protected options =
|
||||
let
|
||||
protect pageWithUser page =
|
||||
Page
|
||||
(\shared req ->
|
||||
case options.user shared req of
|
||||
Just user ->
|
||||
Ok (pageWithUser user page)
|
||||
|
||||
Nothing ->
|
||||
Err options.route
|
||||
)
|
||||
in
|
||||
{ static =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( (), options.effectNone )
|
||||
, update = \_ model -> ( model, options.effectNone )
|
||||
, view = \_ -> page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, sandbox =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( page.init user, options.effectNone )
|
||||
, update = \msg model -> ( page.update user msg model, options.effectNone )
|
||||
, view = page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, element =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user |> Tuple.mapSecond options.fromCmd
|
||||
, update = \msg model -> page.update user msg model |> Tuple.mapSecond options.fromCmd
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
, advanced =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user
|
||||
, update = page.update user
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
{-| Deprecated! Will be replaced by [protected3](#protected3)
|
||||
-}
|
||||
protected2 :
|
||||
{ effectNone : effect
|
||||
, fromCmd : Cmd msg -> effect
|
||||
, beforeInit : shared -> Request route () -> Protected user route
|
||||
}
|
||||
->
|
||||
{ static :
|
||||
{ view : user -> view
|
||||
}
|
||||
-> Page shared route effect view () msg
|
||||
, sandbox :
|
||||
{ init : user -> model
|
||||
, update : user -> msg -> model -> model
|
||||
, view : user -> model -> view
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
, element :
|
||||
{ init : user -> ( model, Cmd msg )
|
||||
, update : user -> msg -> model -> ( model, Cmd msg )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
, advanced :
|
||||
{ init : user -> ( model, effect )
|
||||
, update : user -> msg -> model -> ( model, effect )
|
||||
, view : user -> model -> view
|
||||
, subscriptions : user -> model -> Sub msg
|
||||
}
|
||||
-> Page shared route effect view model msg
|
||||
}
|
||||
protected2 options =
|
||||
let
|
||||
protect pageWithUser page =
|
||||
Page
|
||||
(\shared req ->
|
||||
case options.beforeInit shared req of
|
||||
Provide user ->
|
||||
Ok (pageWithUser user page)
|
||||
|
||||
RedirectTo route ->
|
||||
Err route
|
||||
)
|
||||
in
|
||||
{ static =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( (), options.effectNone )
|
||||
, update = \_ model -> ( model, options.effectNone )
|
||||
, view = \_ -> page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, sandbox =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> ( page.init user, options.effectNone )
|
||||
, update = \msg model -> ( page.update user msg model, options.effectNone )
|
||||
, view = page.view user
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
||||
)
|
||||
, element =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user |> Tuple.mapSecond options.fromCmd
|
||||
, update = \msg model -> page.update user msg model |> Tuple.mapSecond options.fromCmd
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
, advanced =
|
||||
protect
|
||||
(\user page ->
|
||||
{ init = \_ -> page.init user
|
||||
, update = page.update user
|
||||
, view = page.view user
|
||||
, subscriptions = page.subscriptions user
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ module ElmSpa.Page exposing
|
||||
# **( These docs are for CLI contributors )**
|
||||
|
||||
|
||||
### If you are using **elm-spa**, check out [the official guide](https://v6.elm-spa.dev/guide) instead!
|
||||
### If you are using **elm-spa**, check out [the official guide](https://elm-spa.dev/guide) instead!
|
||||
|
||||
---
|
||||
|
||||
|
@ -6,7 +6,7 @@ module ElmSpa.Request exposing (Request, create)
|
||||
# **( These docs are for CLI contributors )**
|
||||
|
||||
|
||||
### If you are using **elm-spa**, check out [the official guide](https://v6.elm-spa.dev/guide) instead!
|
||||
### If you are using **elm-spa**, check out [the official guide](https://elm-spa.dev/guide) instead!
|
||||
|
||||
---
|
||||
|
||||
|
227
src/cli/package-lock.json
generated
227
src/cli/package-lock.json
generated
@ -1,17 +1,17 @@
|
||||
{
|
||||
"name": "elm-spa",
|
||||
"version": "6.0.7--beta",
|
||||
"version": "6.0.15--beta",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "elm-spa",
|
||||
"version": "6.0.7--beta",
|
||||
"version": "6.0.15--beta",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"chokidar": "3.4.2",
|
||||
"elm": "0.19.1-3",
|
||||
"mime": "2.4.6",
|
||||
"node-elm-compiler": "5.0.5",
|
||||
"terser": "5.3.8",
|
||||
"websocket": "1.0.32"
|
||||
},
|
||||
@ -1501,8 +1501,7 @@
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"node_modules/base": {
|
||||
"version": "0.11.2",
|
||||
@ -1592,7 +1591,6 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -1857,8 +1855,7 @@
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "1.7.0",
|
||||
@ -1887,7 +1884,6 @@
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
@ -2116,9 +2112,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/elm": {
|
||||
"version": "0.19.1-3",
|
||||
"resolved": "https://registry.npmjs.org/elm/-/elm-0.19.1-3.tgz",
|
||||
"integrity": "sha512-6y36ewCcVmTOx8lj7cKJs3bhI5qMfoVEigePZ9PhEUNKpwjjML/pU2u2YSpHVAznuCcojoF6KIsrS1Ci7GtVaQ==",
|
||||
"version": "0.19.1-5",
|
||||
"resolved": "https://registry.npmjs.org/elm/-/elm-0.19.1-5.tgz",
|
||||
"integrity": "sha512-dyBoPvFiNLvxOStQJdyq28gZEjS/enZXdZ5yyCtNtDEMbFJJVQq4pYNRKvhrKKdlxNot6d96iQe1uczoqO5yvA==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"request": "^2.88.0"
|
||||
},
|
||||
@ -2573,6 +2570,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/find-elm-dependencies": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.4.tgz",
|
||||
"integrity": "sha512-x/4w4fVmlD2X4PD9oQ+yh9EyaQef6OtEULdMGBTuWx0Nkppvo2Z/bAiQioW2n+GdRYKypME2b9OmYTw5tw5qDg==",
|
||||
"dependencies": {
|
||||
"firstline": "^1.2.0",
|
||||
"lodash": "^4.17.19"
|
||||
},
|
||||
"bin": {
|
||||
"find-elm-dependencies": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
@ -2586,6 +2598,14 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/firstline": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/firstline/-/firstline-1.3.1.tgz",
|
||||
"integrity": "sha512-ycwgqtoxujz1dm0kjkBFOPQMESxB9uKc/PlD951dQDIG+tBXRpYZC2UmJb0gDxopQ1ZX6oyRQN3goRczYu7Deg==",
|
||||
"engines": {
|
||||
"node": ">=6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
@ -2631,8 +2651,7 @@
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.1.3",
|
||||
@ -2706,7 +2725,6 @@
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@ -2932,7 +2950,6 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@ -2941,8 +2958,7 @@
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/ip-regex": {
|
||||
"version": "2.1.0",
|
||||
@ -3189,8 +3205,7 @@
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
|
||||
"dev": true
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"node_modules/isobject": {
|
||||
"version": "3.0.1",
|
||||
@ -5082,8 +5097,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
@ -5216,7 +5230,6 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
@ -5227,8 +5240,7 @@
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"node_modules/mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
@ -5309,8 +5321,21 @@
|
||||
"node_modules/nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"node_modules/node-elm-compiler": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.5.tgz",
|
||||
"integrity": "sha512-vapB+VkmKMY1NRy7jjpGjzwWbKmtiRfzbgVoV/eROz5Kx30QvY0Nd5Ua7iST+9utrn1aG8cVToXC6UWdEO5BKQ==",
|
||||
"dependencies": {
|
||||
"cross-spawn": "6.0.5",
|
||||
"find-elm-dependencies": "^2.0.4",
|
||||
"lodash": "^4.17.19",
|
||||
"temp": "^0.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.2.3",
|
||||
@ -5493,7 +5518,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -5621,7 +5645,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -5630,7 +5653,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@ -6218,7 +6240,6 @@
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@ -6260,7 +6281,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^1.0.0"
|
||||
},
|
||||
@ -6272,7 +6292,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -6722,6 +6741,40 @@
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/temp": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"dependencies": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/temp/node_modules/mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/temp/node_modules/rimraf": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/terminal-link": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
|
||||
@ -7244,7 +7297,6 @@
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@ -7284,8 +7336,7 @@
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "3.0.3",
|
||||
@ -8678,8 +8729,7 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"base": {
|
||||
"version": "0.11.2",
|
||||
@ -8753,7 +8803,6 @@
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -8972,8 +9021,7 @@
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"convert-source-map": {
|
||||
"version": "1.7.0",
|
||||
@ -8999,7 +9047,6 @@
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
@ -9183,9 +9230,8 @@
|
||||
}
|
||||
},
|
||||
"elm": {
|
||||
"version": "0.19.1-3",
|
||||
"resolved": "https://registry.npmjs.org/elm/-/elm-0.19.1-3.tgz",
|
||||
"integrity": "sha512-6y36ewCcVmTOx8lj7cKJs3bhI5qMfoVEigePZ9PhEUNKpwjjML/pU2u2YSpHVAznuCcojoF6KIsrS1Ci7GtVaQ==",
|
||||
"version": "https://registry.npmjs.org/elm/-/elm-0.19.1-5.tgz",
|
||||
"integrity": "sha512-dyBoPvFiNLvxOStQJdyq28gZEjS/enZXdZ5yyCtNtDEMbFJJVQq4pYNRKvhrKKdlxNot6d96iQe1uczoqO5yvA==",
|
||||
"requires": {
|
||||
"request": "^2.88.0"
|
||||
}
|
||||
@ -9561,6 +9607,15 @@
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"find-elm-dependencies": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.4.tgz",
|
||||
"integrity": "sha512-x/4w4fVmlD2X4PD9oQ+yh9EyaQef6OtEULdMGBTuWx0Nkppvo2Z/bAiQioW2n+GdRYKypME2b9OmYTw5tw5qDg==",
|
||||
"requires": {
|
||||
"firstline": "^1.2.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
@ -9571,6 +9626,11 @@
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"firstline": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/firstline/-/firstline-1.3.1.tgz",
|
||||
"integrity": "sha512-ycwgqtoxujz1dm0kjkBFOPQMESxB9uKc/PlD951dQDIG+tBXRpYZC2UmJb0gDxopQ1ZX6oyRQN3goRczYu7Deg=="
|
||||
},
|
||||
"for-in": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
|
||||
@ -9604,8 +9664,7 @@
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.1.3",
|
||||
@ -9658,7 +9717,6 @@
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@ -9833,7 +9891,6 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@ -9842,8 +9899,7 @@
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ip-regex": {
|
||||
"version": "2.1.0",
|
||||
@ -10033,8 +10089,7 @@
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
|
||||
"dev": true
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"isobject": {
|
||||
"version": "3.0.1",
|
||||
@ -11606,8 +11661,7 @@
|
||||
"lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
@ -11712,7 +11766,6 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -11720,8 +11773,7 @@
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"mixin-deep": {
|
||||
"version": "1.3.2",
|
||||
@ -11789,8 +11841,18 @@
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"node-elm-compiler": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.5.tgz",
|
||||
"integrity": "sha512-vapB+VkmKMY1NRy7jjpGjzwWbKmtiRfzbgVoV/eROz5Kx30QvY0Nd5Ua7iST+9utrn1aG8cVToXC6UWdEO5BKQ==",
|
||||
"requires": {
|
||||
"cross-spawn": "6.0.5",
|
||||
"find-elm-dependencies": "^2.0.4",
|
||||
"lodash": "^4.17.19",
|
||||
"temp": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node-gyp-build": {
|
||||
"version": "4.2.3",
|
||||
@ -11933,7 +11995,6 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -12030,14 +12091,12 @@
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
},
|
||||
"path-key": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
|
||||
"dev": true
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
@ -12515,8 +12574,7 @@
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
@ -12551,7 +12609,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"shebang-regex": "^1.0.0"
|
||||
}
|
||||
@ -12559,8 +12616,7 @@
|
||||
"shebang-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
|
||||
"dev": true
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
|
||||
},
|
||||
"shellwords": {
|
||||
"version": "0.1.1",
|
||||
@ -12928,6 +12984,33 @@
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true
|
||||
},
|
||||
"temp": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
||||
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||
"requires": {
|
||||
"mkdirp": "^0.5.1",
|
||||
"rimraf": "~2.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
|
||||
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"terminal-link": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
|
||||
@ -13352,7 +13435,6 @@
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
@ -13383,8 +13465,7 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
},
|
||||
"write-file-atomic": {
|
||||
"version": "3.0.3",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "elm-spa",
|
||||
"version": "6.0.7--beta",
|
||||
"version": "6.0.15--beta",
|
||||
"description": "single page apps made easy",
|
||||
"bin": "dist/src/index.js",
|
||||
"scripts": {
|
||||
@ -40,8 +40,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "3.4.2",
|
||||
"elm": "0.19.1-3",
|
||||
"mime": "2.4.6",
|
||||
"node-elm-compiler": "5.0.5",
|
||||
"terser": "5.3.8",
|
||||
"websocket": "1.0.32"
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export default {
|
||||
|
||||
await File.create(outputFilepath, contents)
|
||||
|
||||
return ` ${bold('New page created at:')}\n ${outputFilepath}`
|
||||
return ` ${bold('New page created at:')}\n ${outputFilepath}\n`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,15 +2,20 @@ import path from "path"
|
||||
import { exists } from "../file"
|
||||
import config from '../config'
|
||||
import * as File from '../file'
|
||||
import { createInterface } from 'readline'
|
||||
import RouteTemplate from '../templates/routes'
|
||||
import PagesTemplate from '../templates/pages'
|
||||
import PageTemplate from '../templates/page'
|
||||
import RequestTemplate from '../templates/request'
|
||||
import ModelTemplate from '../templates/model'
|
||||
import MsgTemplate from '../templates/msg'
|
||||
import ChildProcess from 'child_process'
|
||||
import ParamsTemplate from '../templates/params'
|
||||
import * as Process from '../process'
|
||||
import { bold, underline, colors, reset, check, dim, dot } from "../terminal"
|
||||
import terser from 'terser'
|
||||
import { bold, underline, colors, reset, check, dim, dot, warn, error } from "../terminal"
|
||||
import { isStandardPage, isStaticPage, isStaticView, options, PageKind } from "../templates/utils"
|
||||
import { createMissingAddTemplates } from "./_common"
|
||||
const elm = require('node-elm-compiler')
|
||||
|
||||
export const build = ({ env, runElmMake } : { env : Environment, runElmMake: boolean }) => () =>
|
||||
Promise.all([
|
||||
@ -22,39 +27,39 @@ export const build = ({ env, runElmMake } : { env : Environment, runElmMake: boo
|
||||
|
||||
const createMissingDefaultFiles = async () => {
|
||||
type Action
|
||||
= [ 'DELETE_FROM_DEFAULTS', string[] ]
|
||||
| [ 'CREATE_IN_DEFAULTS', string[] ]
|
||||
| [ 'DO_NOTHING', string[] ]
|
||||
= ['DELETE_FROM_DEFAULTS', string[]]
|
||||
| ['CREATE_IN_DEFAULTS', string[]]
|
||||
| ['DO_NOTHING', string[]]
|
||||
|
||||
const toAction = async (filepath : string[]) : Promise<Action> => {
|
||||
const [ inDefaults, inSrc ] = await Promise.all([
|
||||
const toAction = async (filepath: string[]): Promise<Action> => {
|
||||
const [inDefaults, inSrc] = await Promise.all([
|
||||
exists(path.join(config.folders.defaults.dest, ...filepath)),
|
||||
exists(path.join(config.folders.src, ...filepath))
|
||||
])
|
||||
|
||||
if (inSrc && inDefaults) {
|
||||
return [ 'DELETE_FROM_DEFAULTS', filepath ]
|
||||
return ['DELETE_FROM_DEFAULTS', filepath]
|
||||
} else if (!inSrc) {
|
||||
return [ 'CREATE_IN_DEFAULTS', filepath ]
|
||||
return ['CREATE_IN_DEFAULTS', filepath]
|
||||
} else {
|
||||
return [ 'DO_NOTHING', filepath ]
|
||||
return ['DO_NOTHING', filepath]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const actions = await Promise.all(config.defaults.map(toAction))
|
||||
|
||||
const performDefaultFileAction = ([ action, relative ] : Action) : Promise<any> =>
|
||||
const performDefaultFileAction = ([action, relative]: Action): Promise<any> =>
|
||||
action === 'CREATE_IN_DEFAULTS' ? createDefaultFile(relative)
|
||||
: action === 'DELETE_FROM_DEFAULTS' ? deleteFromDefaults(relative)
|
||||
: Promise.resolve()
|
||||
: action === 'DELETE_FROM_DEFAULTS' ? deleteFromDefaults(relative)
|
||||
: Promise.resolve()
|
||||
|
||||
const createDefaultFile = async (relative : string[]) =>
|
||||
const createDefaultFile = async (relative: string[]) =>
|
||||
File.copyFile(
|
||||
path.join(config.folders.defaults.src, ...relative),
|
||||
path.join(config.folders.defaults.dest, ...relative)
|
||||
)
|
||||
|
||||
const deleteFromDefaults = async (relative : string[]) =>
|
||||
const deleteFromDefaults = async (relative: string[]) =>
|
||||
File.remove(path.join(config.folders.defaults.dest, ...relative))
|
||||
|
||||
return Promise.all(actions.map(performDefaultFileAction))
|
||||
@ -67,22 +72,22 @@ type FilepathSegments = {
|
||||
entry: PageEntry
|
||||
}
|
||||
|
||||
const getFilepathSegments = async (entries: PageEntry[]) : Promise<FilepathSegments[]> => {
|
||||
const getFilepathSegments = async (entries: PageEntry[]): Promise<FilepathSegments[]> => {
|
||||
const contents = await Promise.all(entries.map(e => File.read(e.filepath)))
|
||||
|
||||
return Promise.all(entries.map(async (entry, i) => {
|
||||
const c = contents[i]
|
||||
const kind : PageKind = await (
|
||||
const kind: PageKind = await (
|
||||
isStandardPage(c) ? Promise.resolve('page')
|
||||
: isStaticPage(c) ? Promise.resolve('static-page')
|
||||
: isStaticView(c) ? Promise.resolve('view')
|
||||
: Promise.reject(invalidExportsMessage(entry))
|
||||
: isStaticPage(c) ? Promise.resolve('static-page')
|
||||
: isStaticView(c) ? Promise.resolve('view')
|
||||
: Promise.reject(invalidExportsMessage(entry))
|
||||
)
|
||||
return { kind, entry }
|
||||
}))
|
||||
}
|
||||
|
||||
const invalidExportsMessage = (entry : PageEntry) => {
|
||||
const invalidExportsMessage = (entry: PageEntry) => {
|
||||
const moduleName = `${bold}Pages.${entry.segments.join('.')}${reset}`
|
||||
const cyan = (str: string) => `${colors.cyan}${str}${reset}`
|
||||
|
||||
@ -104,22 +109,25 @@ const createGeneratedFiles = async () => {
|
||||
const segments = entries.map(e => e.segments)
|
||||
|
||||
const filepathSegments = await getFilepathSegments(entries)
|
||||
const kindForPage = (p : string[]) : PageKind =>
|
||||
const kindForPage = (p: string[]): PageKind =>
|
||||
filepathSegments
|
||||
.filter(item => item.entry.segments.join('.') == p.join('.'))
|
||||
.map(fps => fps.kind)[0] || 'page'
|
||||
|
||||
const paramFiles = segments.map(filepath => ({
|
||||
filepath: [ 'Gen', 'Params', ...filepath ],
|
||||
filepath: ['Gen', 'Params', ...filepath],
|
||||
contents: ParamsTemplate(filepath, options(kindForPage))
|
||||
}))
|
||||
|
||||
const filesToCreate = [
|
||||
...paramFiles,
|
||||
{ filepath: [ 'Gen', 'Route' ], contents: RouteTemplate(segments, options(kindForPage)) },
|
||||
{ filepath: [ 'Gen', 'Pages' ], contents: PagesTemplate(segments, options(kindForPage)) },
|
||||
{ filepath: [ 'Gen', 'Model' ], contents: ModelTemplate(segments, options(kindForPage)) },
|
||||
{ filepath: [ 'Gen', 'Msg' ], contents: MsgTemplate(segments, options(kindForPage)) }
|
||||
{ filepath: ['Page'], contents: PageTemplate() },
|
||||
{ filepath: ['Request'], contents: RequestTemplate() },
|
||||
{ filepath: ['Gen', 'Route'], contents: RouteTemplate(segments, options(kindForPage)) },
|
||||
{ filepath: ['Gen', 'Route'], contents: RouteTemplate(segments, options(kindForPage)) },
|
||||
{ filepath: ['Gen', 'Pages'], contents: PagesTemplate(segments, options(kindForPage)) },
|
||||
{ filepath: ['Gen', 'Model'], contents: ModelTemplate(segments, options(kindForPage)) },
|
||||
{ filepath: ['Gen', 'Msg'], contents: MsgTemplate(segments, options(kindForPage)) }
|
||||
]
|
||||
|
||||
return Promise.all(filesToCreate.map(({ filepath, contents }) =>
|
||||
@ -132,120 +140,173 @@ type PageEntry = {
|
||||
segments: string[];
|
||||
}
|
||||
|
||||
const getAllPageEntries = async () : Promise<PageEntry[]> => {
|
||||
const scanPageFilesIn = async (folder : string) => {
|
||||
const getAllPageEntries = async (): Promise<PageEntry[]> => {
|
||||
const scanPageFilesIn = async (folder: string) => {
|
||||
const items = await File.scan(folder)
|
||||
return items.map(s =>({
|
||||
return items.map(s => ({
|
||||
filepath: s,
|
||||
segments: s.substring(folder.length + 1, s.length - '.elm'.length).split(path.sep)
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
return Promise.all([
|
||||
scanPageFilesIn(config.folders.pages.src),
|
||||
scanPageFilesIn(config.folders.pages.defaults)
|
||||
]).then(([ left, right ]) => left.concat(right))
|
||||
]).then(([left, right]) => left.concat(right))
|
||||
}
|
||||
|
||||
type Environment = 'production' | 'development'
|
||||
|
||||
const output = path.join(config.folders.dist, 'elm.js')
|
||||
const outputFilepath = path.join(config.folders.dist, 'elm.js')
|
||||
|
||||
const compileMainElm = (env: Environment) => async () => {
|
||||
await ensureElmIsInstalled(env)
|
||||
|
||||
const compileMainElm = (env : Environment) => async () => {
|
||||
const start = Date.now()
|
||||
|
||||
const elmMake = async () => {
|
||||
const flags = env === 'development' ? '--debug' : '--optimize'
|
||||
const inDevelopment = env === 'development'
|
||||
const inProduction = env === 'production'
|
||||
|
||||
const isSrcMainElmDefined = await File.exists(path.join(config.folders.src, 'Main.elm'))
|
||||
const input = isSrcMainElmDefined
|
||||
const isSrcMainElmDefined = await File.exists(path.join(config.folders.src, 'Main.elm'))
|
||||
const inputFilepath = isSrcMainElmDefined
|
||||
? path.join(config.folders.src, 'Main.elm')
|
||||
: path.join(config.folders.defaults.dest, 'Main.elm')
|
||||
|
||||
|
||||
if (await File.exists(config.folders.dist) === false) {
|
||||
await File.mkdir(config.folders.dist)
|
||||
}
|
||||
|
||||
return Process.run(`${config.binaries.elm} make ${input} --output=${output} --report=json ${flags}`)
|
||||
.catch(colorElmError)
|
||||
return elm.compileToString(inputFilepath, {
|
||||
output: outputFilepath,
|
||||
report: 'json',
|
||||
debug: inDevelopment,
|
||||
optimize: inProduction,
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
try { return colorElmError(JSON.parse(error.message.split('\n')[1])) }
|
||||
catch {
|
||||
const { RED, green } = colors
|
||||
return Promise.reject([
|
||||
`${RED}!${reset} elm-spa failed to understand an error`,
|
||||
`Please report the output below to ${green}https://github.com/ryannhg/elm-spa/issues${reset}`,
|
||||
`-----`,
|
||||
error,
|
||||
`-----`,
|
||||
`${RED}!${reset} elm-spa failed to understand an error`,
|
||||
`Please send the output above to ${green}https://github.com/ryannhg/elm-spa/issues${reset}`,
|
||||
``
|
||||
].join('\n\n'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const red = colors.RED
|
||||
const green = colors.green
|
||||
type ElmError = {
|
||||
path: string
|
||||
problems: Problem[]
|
||||
}
|
||||
|
||||
const colorElmError = (err : string) => {
|
||||
let errors = []
|
||||
type Problem = {
|
||||
title: string
|
||||
message: (Message | string)[]
|
||||
}
|
||||
|
||||
try {
|
||||
errors = JSON.parse(err).errors as Error[] || []
|
||||
} catch (e) {
|
||||
return Promise.reject([
|
||||
`${red}Something went wrong with elm-spa.${reset}`,
|
||||
`Please report this entire error to ${green}https://github.com/ryannhg/elm-spa/issues${reset}`,
|
||||
`-----`,
|
||||
err,
|
||||
`-----`
|
||||
].join('\n\n'))
|
||||
}
|
||||
type Message = {
|
||||
bold: boolean
|
||||
underline: boolean
|
||||
color: keyof typeof colors
|
||||
string: string
|
||||
}
|
||||
|
||||
const strIf = (str : string) => (cond : boolean) : string => cond ? str : ''
|
||||
const colorElmError = (output : { errors: ElmError[] }) => {
|
||||
|
||||
const { errors } = output
|
||||
|
||||
const strIf = (str: string) => (cond: boolean): string => cond ? str : ''
|
||||
const boldIf = strIf(bold)
|
||||
const underlineIf = strIf(underline)
|
||||
|
||||
type Error = {
|
||||
path : string
|
||||
problems: Problem[]
|
||||
}
|
||||
const repeat = (str: string, num: number, min = 3) => [...Array(num < 0 ? min : num)].map(_ => str).join('')
|
||||
|
||||
type Problem = {
|
||||
title : string
|
||||
message : (Message | string)[]
|
||||
}
|
||||
|
||||
type Message = {
|
||||
bold : boolean
|
||||
underline : boolean
|
||||
color : keyof typeof colors
|
||||
string : string
|
||||
}
|
||||
|
||||
const repeat = (str : string, num : number, min = 3) => [...Array(num < 0 ? min : num)].map(_ => str).join('')
|
||||
|
||||
const errorToString = (error : Error) : string => {
|
||||
const problemToString = (problem : Problem) : string => {
|
||||
const errorToString = (error: ElmError): string => {
|
||||
const problemToString = (problem: Problem): string => {
|
||||
const path = error.path.substr(process.cwd().length + 1)
|
||||
return [
|
||||
`${colors.cyan}-- ${problem.title} ${repeat('-', 63 - problem.title.length - path.length)} ${path}${reset}`,
|
||||
problem.message.map(messageToString).join('')
|
||||
].join('\n\n')
|
||||
}
|
||||
|
||||
const messageToString = (line : Message | string) =>
|
||||
|
||||
const messageToString = (line: Message | string) =>
|
||||
typeof line === 'string'
|
||||
? line
|
||||
: [ boldIf(line.bold), underlineIf(line.underline), colors[line.color] || '', line.string, reset ].join('')
|
||||
: [boldIf(line.bold), underlineIf(line.underline), colors[line.color] || '', line.string, reset].join('')
|
||||
|
||||
return error.problems.map(problemToString).join('\n\n')
|
||||
}
|
||||
|
||||
return errors.length
|
||||
? Promise.reject(errors.map(errorToString).join('\n\n\n'))
|
||||
: err
|
||||
return Promise.reject(errors.map(err => errorToString(err)).join('\n\n\n'))
|
||||
}
|
||||
|
||||
const success = () => `${check} Build successful! ${dim}(${Date.now() - start}ms)${reset}`
|
||||
|
||||
const minify = () =>
|
||||
Process.run(`${config.binaries.terser} ${output} --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | ${config.binaries.terser} --mangle --output=${output}`)
|
||||
const minify = (rawCode: string) =>
|
||||
terser.minify(rawCode, { compress: { pure_funcs: `F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9`.split(','), pure_getters: true, keep_fargs: false, unsafe_comps: true, unsafe: true } })
|
||||
.then(intermediate => terser.minify(intermediate.code || '', { mangle: true }))
|
||||
.then(minified => File.create(outputFilepath, minified.code || ''))
|
||||
|
||||
return (env === 'development')
|
||||
? elmMake()
|
||||
.then(_ => success()).catch(error => error)
|
||||
: elmMake().then(minify)
|
||||
.then(_ => [ success() + '\n' ])
|
||||
}
|
||||
|
||||
.then(rawJsCode => File.create(outputFilepath, rawJsCode))
|
||||
.then(_ => success())
|
||||
.catch(error => error)
|
||||
: elmMake()
|
||||
.then(minify)
|
||||
.then(_ => [success() + '\n'])
|
||||
}
|
||||
|
||||
const ensureElmIsInstalled = async (environment : Environment) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
ChildProcess.exec('elm', (err) => {
|
||||
if (err) {
|
||||
if (environment === 'production') {
|
||||
attemptToInstallViaNpm(resolve, reject)
|
||||
} else {
|
||||
offerToInstallForDeveloper(resolve, reject)
|
||||
}
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attemptToInstallViaNpm = (resolve: (value: unknown) => void, reject: (reason: unknown) => void) => {
|
||||
process.stdout.write(`\n ${bold}Awesome!${reset} Installing Elm via NPM... `)
|
||||
ChildProcess.exec(`npm install --global elm@latest-0.19.1`, (err) => {
|
||||
if (err) {
|
||||
console.info(error)
|
||||
reject(` The automatic install didn't work...\n Please visit ${colors.green}https://guide.elm-lang.org/install/elm${reset} to install Elm.\n`)
|
||||
} else {
|
||||
console.info(check)
|
||||
console.info(` Elm is now installed!`)
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const offerToInstallForDeveloper = (resolve: (value: unknown) => void, reject: (reason: unknown) => void) => {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
rl.question(`\n${warn} Elm hasn't been installed yet.\n\n May I ${colors.cyan}install${reset} it for you? ${dim}[y/n]${reset} `, answer => {
|
||||
if (answer.toLowerCase() === 'n') {
|
||||
reject(` ${bold}No changes made!${reset}\n Please visit ${colors.green}https://guide.elm-lang.org/install/elm${reset} to install Elm.`)
|
||||
} else {
|
||||
attemptToInstallViaNpm(resolve, reject)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
build: build({ env: 'production', runElmMake: true }),
|
||||
gen: build({ env: 'production', runElmMake: false })
|
||||
|
@ -22,5 +22,5 @@ Other commands:
|
||||
${bold(`elm-spa ${cyan(`gen`)}`)} . . . . generates code without elm make
|
||||
${bold(`elm-spa ${cyan(`watch`)}`)} . . . . runs elm-spa gen as you code
|
||||
|
||||
Visit ${green(`https://next.elm-spa.dev`)} for more!
|
||||
Visit ${green(`https://elm-spa.dev`)} for more!
|
||||
`
|
@ -10,6 +10,6 @@ export default {
|
||||
const dest = process.cwd()
|
||||
File.copy(config.folders.init, dest)
|
||||
try { fs.renameSync(path.join(dest, '_gitignore'), path.join(dest, '.gitignore')) } catch (_) {}
|
||||
return ` ${bold}New project created in:${reset}\n ${process.cwd()}`
|
||||
return ` ${bold}New project created in:${reset}\n ${process.cwd()}\n`
|
||||
}
|
||||
}
|
@ -31,17 +31,12 @@ const config = {
|
||||
public: path.join(cwd, 'public'),
|
||||
dist: path.join(cwd, 'public', 'dist'),
|
||||
},
|
||||
binaries: {
|
||||
elm: `npx elm`,
|
||||
terser: `npx terser`
|
||||
},
|
||||
defaults: [
|
||||
[ 'Auth.elm' ],
|
||||
[ 'Effect.elm' ],
|
||||
[ 'Main.elm' ],
|
||||
[ 'Shared.elm' ],
|
||||
[ `Pages`, `${reserved.notFound}.elm` ],
|
||||
[ 'Page.elm' ],
|
||||
[ 'Request.elm' ],
|
||||
[ 'View.elm' ]
|
||||
]
|
||||
}
|
||||
|
39
src/cli/src/defaults/Auth.elm
Normal file
39
src/cli/src/defaults/Auth.elm
Normal file
@ -0,0 +1,39 @@
|
||||
module Auth exposing
|
||||
( User
|
||||
, beforeProtectedInit
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs User
|
||||
@docs beforeProtectedInit
|
||||
|
||||
-}
|
||||
|
||||
import ElmSpa.Internals.Page as ElmSpa
|
||||
import Gen.Route exposing (Route)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
|
||||
|
||||
{-| Replace the "()" with your actual User type
|
||||
-}
|
||||
type alias User =
|
||||
()
|
||||
|
||||
|
||||
{-| This function will run before any `protected` pages.
|
||||
|
||||
Here, you can provide logic on where to redirect if a user is not signed in. Here's an example:
|
||||
|
||||
case shared.user of
|
||||
Just user ->
|
||||
ElmSpa.Provide user
|
||||
|
||||
Nothing ->
|
||||
ElmSpa.RedirectTo Gen.Route.SignIn
|
||||
|
||||
-}
|
||||
beforeProtectedInit : Shared.Model -> Request -> ElmSpa.Protected User Route
|
||||
beforeProtectedInit shared req =
|
||||
ElmSpa.RedirectTo Gen.Route.NotFound
|
@ -3,6 +3,7 @@ module Main exposing (main)
|
||||
import Browser
|
||||
import Browser.Navigation as Nav exposing (Key)
|
||||
import Effect
|
||||
import Gen.Model
|
||||
import Gen.Pages as Pages
|
||||
import Gen.Route as Route
|
||||
import Request
|
||||
@ -93,10 +94,22 @@ update msg model =
|
||||
let
|
||||
( shared, sharedCmd ) =
|
||||
Shared.update (Request.create () model.url model.key) sharedMsg model.shared
|
||||
|
||||
( page, effect ) =
|
||||
Pages.init (Route.fromUrl model.url) shared model.url model.key
|
||||
in
|
||||
( { model | shared = shared }
|
||||
, Cmd.map Shared sharedCmd
|
||||
)
|
||||
if page == Gen.Model.Redirecting_ then
|
||||
( { model | shared = shared, page = page }
|
||||
, Cmd.batch
|
||||
[ Cmd.map Shared sharedCmd
|
||||
, Effect.toCmd ( Shared, Page ) effect
|
||||
]
|
||||
)
|
||||
|
||||
else
|
||||
( { model | shared = shared }
|
||||
, Cmd.map Shared sharedCmd
|
||||
)
|
||||
|
||||
Page pageMsg ->
|
||||
let
|
||||
|
@ -1,128 +0,0 @@
|
||||
module Page exposing
|
||||
( Page
|
||||
, static, sandbox, element, advanced
|
||||
, protected
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs Page
|
||||
@docs static, sandbox, element, advanced
|
||||
|
||||
-}
|
||||
|
||||
import Effect exposing (Effect)
|
||||
import ElmSpa.Internals.Page as ElmSpa
|
||||
import Gen.Route exposing (Route)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
|
||||
-- PROTECTED PAGES
|
||||
|
||||
|
||||
{-| Replace "()" with your actual User type
|
||||
-}
|
||||
type alias User =
|
||||
()
|
||||
|
||||
|
||||
{-| This function will run before any `protected` pages.
|
||||
|
||||
Here, you can provide logic on where to redirect if a user is not signed in. Here's an example:
|
||||
|
||||
case shared.user of
|
||||
Just user ->
|
||||
ElmSpa.Provide user
|
||||
|
||||
Nothing ->
|
||||
ElmSpa.RedirectTo Gen.Route.SignIn
|
||||
|
||||
-}
|
||||
beforeProtectedInit : Shared.Model -> Request () -> ElmSpa.Protected User Route
|
||||
beforeProtectedInit shared req =
|
||||
ElmSpa.RedirectTo Gen.Route.NotFound
|
||||
|
||||
|
||||
|
||||
-- PAGES
|
||||
|
||||
|
||||
type alias Page model msg =
|
||||
ElmSpa.Page Shared.Model Route (Effect msg) (View msg) model msg
|
||||
|
||||
|
||||
static :
|
||||
{ view : View Never
|
||||
}
|
||||
-> Page () Never
|
||||
static =
|
||||
ElmSpa.static Effect.none
|
||||
|
||||
|
||||
sandbox :
|
||||
{ init : model
|
||||
, update : msg -> model -> model
|
||||
, view : model -> View msg
|
||||
}
|
||||
-> Page model msg
|
||||
sandbox =
|
||||
ElmSpa.sandbox Effect.none
|
||||
|
||||
|
||||
element :
|
||||
{ init : ( model, Cmd msg )
|
||||
, update : msg -> model -> ( model, Cmd msg )
|
||||
, view : model -> View msg
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
-> Page model msg
|
||||
element =
|
||||
ElmSpa.element Effect.fromCmd
|
||||
|
||||
|
||||
advanced :
|
||||
{ init : ( model, Effect msg )
|
||||
, update : msg -> model -> ( model, Effect msg )
|
||||
, view : model -> View msg
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
-> Page model msg
|
||||
advanced =
|
||||
ElmSpa.advanced
|
||||
|
||||
|
||||
protected :
|
||||
{ static :
|
||||
{ view : User -> View msg
|
||||
}
|
||||
-> Page () msg
|
||||
, sandbox :
|
||||
{ init : User -> model
|
||||
, update : User -> msg -> model -> model
|
||||
, view : User -> model -> View msg
|
||||
}
|
||||
-> Page model msg
|
||||
, element :
|
||||
{ init : User -> ( model, Cmd msg )
|
||||
, update : User -> msg -> model -> ( model, Cmd msg )
|
||||
, view : User -> model -> View msg
|
||||
, subscriptions : User -> model -> Sub msg
|
||||
}
|
||||
-> Page model msg
|
||||
, advanced :
|
||||
{ init : User -> ( model, Effect msg )
|
||||
, update : User -> msg -> model -> ( model, Effect msg )
|
||||
, view : User -> model -> View msg
|
||||
, subscriptions : User -> model -> Sub msg
|
||||
}
|
||||
-> Page model msg
|
||||
}
|
||||
protected =
|
||||
ElmSpa.protected2
|
||||
{ effectNone = Effect.none
|
||||
, fromCmd = Effect.fromCmd
|
||||
, beforeInit = beforeProtectedInit
|
||||
}
|
@ -3,6 +3,6 @@ module Pages.NotFound exposing (view)
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
view : View Never
|
||||
view : View msg
|
||||
view =
|
||||
View.placeholder "Page not found."
|
||||
|
@ -23,18 +23,18 @@ type Msg
|
||||
= NoOp
|
||||
|
||||
|
||||
init : Request () -> Flags -> ( Model, Cmd Msg )
|
||||
init : Request -> Flags -> ( Model, Cmd Msg )
|
||||
init _ _ =
|
||||
( {}, Cmd.none )
|
||||
|
||||
|
||||
update : Request () -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update : Request -> Msg -> Model -> ( Model, Cmd Msg )
|
||||
update _ msg model =
|
||||
case msg of
|
||||
NoOp ->
|
||||
( model, Cmd.none )
|
||||
|
||||
|
||||
subscriptions : Request () -> Model -> Sub Msg
|
||||
subscriptions : Request -> Model -> Sub Msg
|
||||
subscriptions _ _ =
|
||||
Sub.none
|
||||
|
@ -13,7 +13,7 @@
|
||||
"elm/html": "1.0.0",
|
||||
"elm/json": "1.1.3",
|
||||
"elm/url": "1.0.0",
|
||||
"ryannhg/elm-spa": "5.2.0"
|
||||
"ryannhg/elm-spa": "5.3.0"
|
||||
},
|
||||
"indirect": {
|
||||
"elm/time": "1.0.0",
|
||||
|
@ -4,7 +4,7 @@ import Html
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
view : View Never
|
||||
view : View msg
|
||||
view =
|
||||
{ title = "Homepage"
|
||||
, body = [ Html.text "Hello, world!" ]
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { exec } from 'child_process'
|
||||
|
||||
export const run = (cmd : string) : Promise<unknown> =>
|
||||
new Promise((resolve, reject) =>
|
||||
exec(cmd, (err, stdout, stderr) => err
|
||||
? reject(stderr.split('npm ERR!')[0] || stderr)
|
||||
: resolve(stdout)
|
||||
)
|
||||
)
|
@ -1,10 +1,10 @@
|
||||
export default (page : string[]) : string => `
|
||||
export default (page: string[]): string => `
|
||||
module Pages.${page.join('.')} exposing (view)
|
||||
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
view : View Never
|
||||
view : View msg
|
||||
view =
|
||||
View.placeholder "${page.join('.')}"
|
||||
|
||||
|
@ -2,13 +2,14 @@ module Pages.{{module}} exposing (Model, Msg, page)
|
||||
|
||||
import Effect exposing (Effect)
|
||||
import Gen.Params.{{module}} exposing (Params)
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
import Page
|
||||
|
||||
|
||||
page : Shared.Model -> Request Params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.advanced
|
||||
{ init = init
|
||||
|
@ -1,13 +1,13 @@
|
||||
module Pages.{{module}} exposing (Model, Msg, page)
|
||||
|
||||
import Gen.Params.{{module}} exposing (Params)
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request Params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.element
|
||||
{ init = init
|
||||
|
@ -1,13 +1,13 @@
|
||||
module Pages.{{module}} exposing (Model, Msg, page)
|
||||
|
||||
import Gen.Params.{{module}} exposing (Params)
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Page
|
||||
import Request
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request Params -> Page Model Msg
|
||||
page : Shared.Model -> Request.With Params -> Page.With Model Msg
|
||||
page shared req =
|
||||
Page.sandbox
|
||||
{ init = init
|
||||
|
@ -2,18 +2,18 @@ module Pages.{{module}} exposing (page)
|
||||
|
||||
import Gen.Params.{{module}} exposing (Params)
|
||||
import Page exposing (Page)
|
||||
import Request exposing (Request)
|
||||
import Request
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
page : Shared.Model -> Request Params -> Page () Never
|
||||
page : Shared.Model -> Request.With Params -> Page
|
||||
page shared req =
|
||||
Page.static
|
||||
{ view = view
|
||||
}
|
||||
|
||||
|
||||
view : View Never
|
||||
view : View msg
|
||||
view =
|
||||
View.placeholder "{{module}}"
|
||||
|
126
src/cli/src/templates/page.ts
Normal file
126
src/cli/src/templates/page.ts
Normal file
@ -0,0 +1,126 @@
|
||||
export default (): string => `
|
||||
module Page exposing
|
||||
( Page, With
|
||||
, static, sandbox, element, advanced
|
||||
, protected
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs Page, With
|
||||
@docs static, sandbox, element, advanced
|
||||
@docs protected
|
||||
|
||||
-}
|
||||
|
||||
import Auth exposing (User)
|
||||
import Effect exposing (Effect)
|
||||
import ElmSpa.Internals.Page as ElmSpa
|
||||
import Gen.Route exposing (Route)
|
||||
import Request exposing (Request)
|
||||
import Shared
|
||||
import View exposing (View)
|
||||
|
||||
|
||||
|
||||
-- PAGES
|
||||
|
||||
|
||||
type alias Page =
|
||||
With () Never
|
||||
|
||||
|
||||
type alias With model msg =
|
||||
ElmSpa.Page Shared.Model Route (Effect msg) (View msg) model msg
|
||||
|
||||
|
||||
static :
|
||||
{ view : View Never
|
||||
}
|
||||
-> Page
|
||||
static =
|
||||
ElmSpa.static Effect.none
|
||||
|
||||
|
||||
sandbox :
|
||||
{ init : model
|
||||
, update : msg -> model -> model
|
||||
, view : model -> View msg
|
||||
}
|
||||
-> With model msg
|
||||
sandbox =
|
||||
ElmSpa.sandbox Effect.none
|
||||
|
||||
|
||||
element :
|
||||
{ init : ( model, Cmd msg )
|
||||
, update : msg -> model -> ( model, Cmd msg )
|
||||
, view : model -> View msg
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
-> With model msg
|
||||
element =
|
||||
ElmSpa.element Effect.fromCmd
|
||||
|
||||
|
||||
advanced :
|
||||
{ init : ( model, Effect msg )
|
||||
, update : msg -> model -> ( model, Effect msg )
|
||||
, view : model -> View msg
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
-> With model msg
|
||||
advanced =
|
||||
ElmSpa.advanced
|
||||
|
||||
|
||||
|
||||
-- PROTECTED PAGES
|
||||
|
||||
|
||||
protected :
|
||||
{ static :
|
||||
(User
|
||||
->
|
||||
{ view : View msg
|
||||
}
|
||||
)
|
||||
-> With () msg
|
||||
, sandbox :
|
||||
(User
|
||||
->
|
||||
{ init : model
|
||||
, update : msg -> model -> model
|
||||
, view : model -> View msg
|
||||
}
|
||||
)
|
||||
-> With model msg
|
||||
, element :
|
||||
(User
|
||||
->
|
||||
{ init : ( model, Cmd msg )
|
||||
, update : msg -> model -> ( model, Cmd msg )
|
||||
, view : model -> View msg
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
)
|
||||
-> With model msg
|
||||
, advanced :
|
||||
(User
|
||||
->
|
||||
{ init : ( model, Effect msg )
|
||||
, update : msg -> model -> ( model, Effect msg )
|
||||
, view : model -> View msg
|
||||
, subscriptions : model -> Sub msg
|
||||
}
|
||||
)
|
||||
-> With model msg
|
||||
}
|
||||
protected =
|
||||
ElmSpa.protected3
|
||||
{ effectNone = Effect.none
|
||||
, fromCmd = Effect.fromCmd
|
||||
, beforeInit = Auth.beforeProtectedInit
|
||||
}
|
||||
|
||||
`.trimLeft()
|
@ -1,4 +1,17 @@
|
||||
module Request exposing (Request, create, pushRoute, replaceRoute)
|
||||
export default (): string => `
|
||||
module Request exposing
|
||||
( Request, With
|
||||
, create
|
||||
, pushRoute, replaceRoute
|
||||
)
|
||||
|
||||
{-|
|
||||
|
||||
@docs Request, With
|
||||
@docs create
|
||||
@docs pushRoute, replaceRoute
|
||||
|
||||
-}
|
||||
|
||||
import Browser.Navigation exposing (Key)
|
||||
import ElmSpa.Request as ElmSpa
|
||||
@ -6,20 +19,26 @@ import Gen.Route as Route exposing (Route)
|
||||
import Url exposing (Url)
|
||||
|
||||
|
||||
type alias Request params =
|
||||
type alias Request =
|
||||
With ()
|
||||
|
||||
|
||||
type alias With params =
|
||||
ElmSpa.Request Route params
|
||||
|
||||
|
||||
create : params -> Url -> Key -> Request params
|
||||
create : params -> Url -> Key -> With params
|
||||
create params url key =
|
||||
ElmSpa.create (Route.fromUrl url) params url key
|
||||
|
||||
|
||||
pushRoute : Route -> Request params -> Cmd msg
|
||||
pushRoute : Route -> With params -> Cmd msg
|
||||
pushRoute route req =
|
||||
Browser.Navigation.pushUrl req.key (Route.toHref route)
|
||||
|
||||
|
||||
replaceRoute : Route -> Request params -> Cmd msg
|
||||
replaceRoute : Route -> With params -> Cmd msg
|
||||
replaceRoute route req =
|
||||
Browser.Navigation.replaceUrl req.key (Route.toHref route)
|
||||
|
||||
`.trimLeft()
|
@ -1,6 +1,31 @@
|
||||
import config from "../config"
|
||||
import { routeTypeDefinition, indent, routeParserList, paramsImports, Options, routeToHref } from "./utils"
|
||||
|
||||
export default (pages : string[][], _options : Options) : string => `
|
||||
const routeParserOrder = (pages: string[][]) =>
|
||||
[...pages].sort(sorter)
|
||||
|
||||
const isHomepage = (list: string[]) => list.join('.') === config.reserved.homepage
|
||||
const isDynamic = (piece: string) => piece.endsWith('_')
|
||||
const alphaSorter = (a: string, b: string) => a < b ? -1 : b < a ? 1 : 0
|
||||
|
||||
const sorter = (a: string[], b: string[]): (-1 | 1 | 0) => {
|
||||
if (isHomepage(a)) return -1
|
||||
if (isHomepage(b)) return 1
|
||||
|
||||
if (a.length < b.length) return -1
|
||||
if (a.length > b.length) return 1
|
||||
|
||||
for (let i in a) {
|
||||
const [isA, isB] = [isDynamic(a[i]), isDynamic(b[i])]
|
||||
if (isA && isB) return alphaSorter(a[i], b[i])
|
||||
if (isA) return 1
|
||||
if (isB) return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export default (pages: string[][], _options: Options): string => `
|
||||
module Gen.Route exposing
|
||||
( Route(..)
|
||||
, fromUrl
|
||||
@ -22,7 +47,7 @@ fromUrl =
|
||||
|
||||
routes : List (Parser (Route -> a) a)
|
||||
routes =
|
||||
${indent(routeParserList(pages), 1)}
|
||||
${indent(routeParserList(routeParserOrder(pages)), 1)}
|
||||
|
||||
|
||||
toHref : Route -> String
|
||||
|
@ -316,9 +316,15 @@ const pageModelArguments = (path: string[], options : Options) : string => {
|
||||
}
|
||||
}
|
||||
|
||||
// Used in place of sophisticated AST parsing
|
||||
const exposes = (keyword: string) => (elmSourceCode: string): boolean =>
|
||||
new RegExp(`module\\s(\\S)+\\sexposing(\\s)+\\([^\\)]*${keyword}[^\\)]*\\)`, 'm').test(elmSourceCode)
|
||||
const exposes = (value : string) => (str : string) : boolean => {
|
||||
const regex = new RegExp('^module\\s+[^\\s]+\\s+exposing\\s+\\(([^)]+)\\)')
|
||||
const match = (str.match(regex) || [])[1]
|
||||
if (match) {
|
||||
return match.split(',').filter(a => a).map(a => a.trim()).includes(value)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const exposesModel = exposes('Model')
|
||||
export const exposesMsg = exposes('Msg')
|
||||
@ -334,4 +340,4 @@ export const isStaticPage = (src : string) =>
|
||||
exposesPageFunction(src)
|
||||
|
||||
export const isStaticView = (src : string) =>
|
||||
exposesViewFunction(src)
|
||||
exposesViewFunction(src)
|
||||
|
@ -184,18 +184,18 @@ describe.each([['Model'], ['Msg']])
|
||||
module Layout exposing
|
||||
( ${name}
|
||||
)
|
||||
`)).toBe(true)
|
||||
`.trim())).toBe(true)
|
||||
expect(fn(`
|
||||
module Layout exposing
|
||||
( OtherImport
|
||||
, ${name}
|
||||
)
|
||||
`)).toBe(true)
|
||||
`.trim())).toBe(true)
|
||||
expect(fn(`
|
||||
module Layout exposing
|
||||
( ${name}
|
||||
, OtherImport
|
||||
)
|
||||
`)).toBe(true)
|
||||
`.trim())).toBe(true)
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user