-
-
Notifications
You must be signed in to change notification settings - Fork 98
/
Copy pathRouteBuilder.elm
385 lines (325 loc) · 13.9 KB
/
RouteBuilder.elm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
module RouteBuilder exposing
( StatelessRoute, buildNoState
, App
, withOnAction
, buildWithLocalState, buildWithSharedState
, preRender, single
, preRenderWithFallback, serverRender
, Builder(..)
, StatefulRoute
)
{-|
## Stateless Route Modules
The simplest Route Module you can build is one with no state. It still can use `BackendTask`'s, but it has no `init`, `update`, or `subscriptions`.
It can read the `Shared.Model`, but it cannot send `Shared.Msg`'s to update the `Shared.Model`. If you need a `Model`, use `buildWithLocalState`.
If you need to _change_ Shared state, use `buildWithSharedState`.
@docs StatelessRoute, buildNoState
## Accessing Static Data
With `elm-pages`, you can have HTTP data available before a page is loaded, or read in a file, etc, using the BackendTask API. Since the data
is available when the page is pre-rendered (as well as in the hydrated page), this is called Static Data.
An example of dynamic data would be keyboard input from the user, query params, or any other data that comes from the app running in the browser.
We have the following data during pre-render:
- `path` - the current path is static. In other words, we know the current path when we build an elm-pages site. Note that we **do not** know query parameters, fragments, etc. That is dynamic data. Pre-rendering occurs for paths in our app, but we don't know what possible query paremters might be used when those paths are hit.
- `data` - this will be the resolved `BackendTask` for our page.
- `sharedData` - we can access any shared data between pages. For example, you may have fetched the name of a blog ("Jane's Blog") from the API for a Content Management System (CMS).
- `routeParams` - this is the record that includes any Dynamic Route Segments for the given page (or an empty record if there are none)
@docs App
@docs withOnAction
## Stateful Route Modules
@docs buildWithLocalState, buildWithSharedState
## Pre-Rendered Routes
A `single` page is just a Route that has no Dynamic Route Segments. For example, `Route.About` will have `type alias RouteParams = {}`, whereas `Route.Blog.Slug_` has a Dynamic Segment slug, and `type alias RouteParams = { slug : String }`.
When you run `elm-pages add About`, it will use `RouteBuilder.single { ... }` because it has empty `RouteParams`. When you run `elm-pages add Blog.Slug_`, will will use `RouteBuilder.preRender` because it has a Dynamic Route Segment.
So `RouteBuilder.single` is just a simplified version of `RouteBuilder.preRender`. If there are no Dynamic Route Segments, then you don't need to define which pages to render so `RouteBuilder.single` doesn't need a `pages` field.
When there are Dynamic Route Segments, you need to tell `elm-pages` which pages to render. For example:
page =
RouteBuilder.preRender
{ data = data
, pages = pages
, head = head
}
pages =
BackendTask.succeed
[ { slug = "blog-post1" }
, { slug = "blog-post2" }
]
@docs preRender, single
## Rendering on the Server
@docs preRenderWithFallback, serverRender
## Internals
@docs Builder
@docs StatefulRoute
-}
import BackendTask exposing (BackendTask)
import Dict exposing (Dict)
import Effect exposing (Effect)
import ErrorPage exposing (ErrorPage)
import FatalError exposing (FatalError)
import Form
import Head
import Http
import Json.Decode
import Pages.ConcurrentSubmission
import Pages.Fetcher
import Pages.Internal.NotFoundReason exposing (NotFoundReason)
import Pages.Internal.RoutePattern exposing (RoutePattern)
import Pages.Navigation
import Pages.PageUrl exposing (PageUrl)
import PagesMsg exposing (PagesMsg)
import Server.Request
import Server.Response
import Shared
import UrlPath exposing (UrlPath)
import View exposing (View)
{-| -}
type alias StatefulRoute routeParams data action model msg =
{ data : Server.Request.Request -> routeParams -> BackendTask FatalError (Server.Response.Response data ErrorPage)
, action : Server.Request.Request -> routeParams -> BackendTask FatalError (Server.Response.Response action ErrorPage)
, staticRoutes : BackendTask FatalError (List routeParams)
, view :
Shared.Model
-> model
-> App data action routeParams
-> View (PagesMsg msg)
, head :
App data action routeParams
-> List Head.Tag
, init : Shared.Model -> App data action routeParams -> ( model, Effect msg )
, update : App data action routeParams -> msg -> model -> Shared.Model -> ( model, Effect msg, Maybe Shared.Msg )
, subscriptions : routeParams -> UrlPath -> model -> Shared.Model -> Sub msg
, handleRoute : { moduleName : List String, routePattern : RoutePattern } -> (routeParams -> List ( String, String )) -> routeParams -> BackendTask FatalError (Maybe NotFoundReason)
, kind : String
, onAction : Maybe (action -> msg)
}
{-| -}
type alias StatelessRoute routeParams data action =
StatefulRoute routeParams data action {} ()
{-| -}
type alias App data action routeParams =
{ data : data
, sharedData : Shared.Data
, routeParams : routeParams
, path : UrlPath
, url : Maybe PageUrl
, action : Maybe action
, submit :
{ fields : List ( String, String ), headers : List ( String, String ) }
-> Pages.Fetcher.Fetcher (Result Http.Error action)
, navigation : Maybe Pages.Navigation.Navigation
, concurrentSubmissions : Dict String (Pages.ConcurrentSubmission.ConcurrentSubmission (Maybe action))
, pageFormState : Form.Model
}
{-| -}
type Builder routeParams data action
= WithData
{ data : Server.Request.Request -> routeParams -> BackendTask FatalError (Server.Response.Response data ErrorPage)
, action : Server.Request.Request -> routeParams -> BackendTask FatalError (Server.Response.Response action ErrorPage)
, staticRoutes : BackendTask FatalError (List routeParams)
, head :
App data action routeParams
-> List Head.Tag
, serverless : Bool
, handleRoute :
{ moduleName : List String, routePattern : RoutePattern }
-> (routeParams -> List ( String, String ))
-> routeParams
-> BackendTask FatalError (Maybe NotFoundReason)
, kind : String
}
{-| -}
buildNoState :
{ view :
App data action routeParams
-> Shared.Model
-> View (PagesMsg ())
}
-> Builder routeParams data action
-> StatefulRoute routeParams data action {} ()
buildNoState { view } builderState =
case builderState of
WithData record ->
{ view = \shared model app -> view app shared
, head = record.head
, data = record.data
, action = record.action
, staticRoutes = record.staticRoutes
, init = \_ _ -> ( {}, Effect.none )
, update = \_ _ _ _ -> ( {}, Effect.none, Nothing )
, subscriptions = \_ _ _ _ -> Sub.none
, handleRoute = record.handleRoute
, kind = record.kind
, onAction = Nothing
}
{-| -}
withOnAction : (action -> msg) -> StatefulRoute routeParams data action model msg -> StatefulRoute routeParams data action model msg
withOnAction toMsg config =
{ config
| onAction = Just toMsg
}
{-| -}
buildWithLocalState :
{ view :
App data action routeParams
-> Shared.Model
-> model
-> View (PagesMsg msg)
, init : App data action routeParams -> Shared.Model -> ( model, Effect msg )
, update : App data action routeParams -> Shared.Model -> msg -> model -> ( model, Effect msg )
, subscriptions : routeParams -> UrlPath -> Shared.Model -> model -> Sub msg
}
-> Builder routeParams data action
-> StatefulRoute routeParams data action model msg
buildWithLocalState config builderState =
case builderState of
WithData record ->
{ view =
\model sharedModel app ->
config.view app model sharedModel
, head = record.head
, data = record.data
, action = record.action
, staticRoutes = record.staticRoutes
, init = \shared app -> config.init app shared
, update =
\app msg model sharedModel ->
let
( updatedModel, cmd ) =
config.update app sharedModel msg model
in
( updatedModel, cmd, Nothing )
, subscriptions =
\routeParams path model sharedModel ->
config.subscriptions routeParams path sharedModel model
, handleRoute = record.handleRoute
, kind = record.kind
, onAction = Nothing
}
{-| -}
buildWithSharedState :
{ view :
App data action routeParams
-> Shared.Model
-> model
-> View (PagesMsg msg)
, init : App data action routeParams -> Shared.Model -> ( model, Effect msg )
, update : App data action routeParams -> Shared.Model -> msg -> model -> ( model, Effect msg, Maybe Shared.Msg )
, subscriptions : routeParams -> UrlPath -> Shared.Model -> model -> Sub msg
}
-> Builder routeParams data action
-> StatefulRoute routeParams data action model msg
buildWithSharedState config builderState =
case builderState of
WithData record ->
{ view = \shared model app -> config.view app shared model
, head = record.head
, data = record.data
, action = record.action
, staticRoutes = record.staticRoutes
, init = \shared app -> config.init app shared
, update =
\app msg model sharedModel ->
config.update
app
sharedModel
msg
model
, subscriptions =
\routeParams path model sharedModel ->
config.subscriptions routeParams path sharedModel model
, handleRoute = record.handleRoute
, kind = record.kind
, onAction = Nothing
}
{-| -}
single :
{ data : BackendTask FatalError data
, head : App data action {} -> List Head.Tag
}
-> Builder {} data action
single { data, head } =
WithData
{ data = \_ _ -> data |> BackendTask.map Server.Response.render
, action = \_ _ -> BackendTask.fail (FatalError.fromString "Internal Error - actions should never be called for statically generated pages.")
, staticRoutes = BackendTask.succeed [ {} ]
, head = head
, serverless = False
, handleRoute = \_ _ _ -> BackendTask.succeed Nothing
, kind = "static"
}
{-| -}
preRender :
{ data : routeParams -> BackendTask FatalError data
, pages : BackendTask FatalError (List routeParams)
, head : App data action routeParams -> List Head.Tag
}
-> Builder routeParams data action
preRender { data, head, pages } =
WithData
{ data = \_ -> data >> BackendTask.map Server.Response.render
, action = \_ _ -> BackendTask.fail (FatalError.fromString "Internal Error - actions should never be called for statically generated pages.")
, staticRoutes = pages
, head = head
, serverless = False
, handleRoute =
\moduleContext toRecord routeParams ->
pages
|> BackendTask.map
(\allRoutes ->
if allRoutes |> List.member routeParams then
Nothing
else
-- TODO pass in toString function, and use a custom one to avoid Debug.toString
Just <|
Pages.Internal.NotFoundReason.NotPrerendered
{ moduleName = moduleContext.moduleName
, routePattern = moduleContext.routePattern
, matchedRouteParams = toRecord routeParams
}
(allRoutes
|> List.map toRecord
)
)
, kind = "prerender"
}
{-| -}
preRenderWithFallback :
{ data : routeParams -> BackendTask FatalError (Server.Response.Response data ErrorPage)
, pages : BackendTask FatalError (List routeParams)
, head : App data action routeParams -> List Head.Tag
}
-> Builder routeParams data action
preRenderWithFallback { data, head, pages } =
WithData
{ data = \_ -> data
, action = \_ _ -> BackendTask.fail (FatalError.fromString "Internal Error - actions should never be called for statically generated pages.")
, staticRoutes = pages
, head = head
, serverless = False
, handleRoute =
\moduleContext toRecord routeParams ->
BackendTask.succeed Nothing
, kind = "prerender-with-fallback"
}
{-| -}
serverRender :
{ data : routeParams -> Server.Request.Request -> BackendTask FatalError (Server.Response.Response data ErrorPage)
, action : routeParams -> Server.Request.Request -> BackendTask FatalError (Server.Response.Response action ErrorPage)
, head : App data action routeParams -> List Head.Tag
}
-> Builder routeParams data action
serverRender { data, action, head } =
WithData
{ data =
\request routeParams ->
data routeParams request
, action =
\request routeParams ->
action routeParams request
, staticRoutes = BackendTask.succeed []
, head = head
, serverless = True
, handleRoute =
\moduleContext toRecord routeParams ->
BackendTask.succeed Nothing
, kind = "serverless"
}