elm-pages 2.1.11 → 3.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Internal-RoutePattern.elmi +0 -0
  2. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Internal-RoutePattern.elmo +0 -0
  3. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolations.elmi +0 -0
  4. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolations.elmo +0 -0
  5. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolationsTest.elmi +0 -0
  6. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolationsTest.elmo +0 -0
  7. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Reporter.elmi +0 -0
  8. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Reporter.elmo +0 -0
  9. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Runner.elmi +0 -0
  10. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Runner.elmo +0 -0
  11. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
  12. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
  13. package/generator/{template/public/style.css → review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/lock} +0 -0
  14. package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
  15. package/generator/review/elm-stuff/tests-0.19.1/elm.json +1 -0
  16. package/generator/review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +6795 -0
  17. package/generator/review/elm-stuff/tests-0.19.1/js/Runner.elm.js +27617 -0
  18. package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +110 -0
  19. package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +187 -0
  20. package/generator/review/elm-stuff/tests-0.19.1/js/package.json +1 -0
  21. package/generator/review/elm-stuff/tests-0.19.1/src/Reporter.elm +26 -0
  22. package/generator/review/elm-stuff/tests-0.19.1/src/Runner.elm +62 -0
  23. package/generator/review/elm.json +13 -4
  24. package/{src → generator/review/src}/Pages/Review/NoContractViolations.elm +148 -148
  25. package/generator/review/tests/Pages/Review/NoContractViolationsTest.elm +331 -0
  26. package/generator/src/RouteBuilder.elm +420 -0
  27. package/generator/src/SharedTemplate.elm +4 -5
  28. package/generator/src/SiteConfig.elm +3 -9
  29. package/generator/src/build.js +308 -95
  30. package/generator/src/cli.js +103 -8
  31. package/generator/src/codegen.js +192 -35
  32. package/generator/src/compile-elm.js +183 -31
  33. package/generator/src/dev-server.js +353 -96
  34. package/generator/src/elm-application.json +3 -1
  35. package/generator/src/elm-codegen.js +34 -0
  36. package/generator/src/elm-file-constants.js +2 -0
  37. package/generator/src/error-formatter.js +20 -1
  38. package/generator/src/generate-template-module-connector.js +120 -924
  39. package/generator/src/hello.ts +5 -0
  40. package/generator/src/pre-render-html.js +58 -104
  41. package/generator/src/render-worker.js +27 -13
  42. package/generator/src/render.js +252 -197
  43. package/generator/src/request-cache-fs.js +18 -0
  44. package/generator/src/request-cache.js +128 -56
  45. package/generator/src/rewrite-client-elm-json.js +49 -0
  46. package/generator/src/route-codegen-helpers.js +62 -1
  47. package/generator/static-code/dev-style.css +22 -0
  48. package/generator/static-code/elm-pages.js +43 -39
  49. package/generator/static-code/hmr.js +98 -88
  50. package/generator/template/app/Api.elm +25 -0
  51. package/generator/template/app/ErrorPage.elm +38 -0
  52. package/generator/template/app/Route/Index.elm +87 -0
  53. package/generator/template/{src → app}/Shared.elm +34 -13
  54. package/generator/template/app/Site.elm +19 -0
  55. package/generator/template/{src → app}/View.elm +0 -0
  56. package/generator/template/elm-pages.config.mjs +5 -0
  57. package/generator/template/elm.json +1 -0
  58. package/generator/template/{public/index.js → index.ts} +7 -3
  59. package/generator/template/package.json +4 -4
  60. package/generator/template/public/favicon.ico +0 -0
  61. package/generator/template/public/images/icon-png.png +0 -0
  62. package/generator/template/src/.gitkeep +0 -0
  63. package/generator/template/style.css +4 -0
  64. package/package.json +30 -23
  65. package/src/ApiRoute.elm +176 -43
  66. package/src/BuildError.elm +10 -1
  67. package/src/CookieParser.elm +84 -0
  68. package/src/DataSource/Env.elm +38 -0
  69. package/src/DataSource/File.elm +27 -16
  70. package/src/DataSource/Glob.elm +126 -80
  71. package/src/DataSource/Http.elm +283 -304
  72. package/src/DataSource/Internal/Glob.elm +5 -21
  73. package/src/DataSource/Internal/Request.elm +25 -0
  74. package/src/DataSource/Port.elm +17 -14
  75. package/src/DataSource.elm +55 -318
  76. package/src/Form/Field.elm +717 -0
  77. package/src/Form/FieldStatus.elm +36 -0
  78. package/src/Form/FieldView.elm +417 -0
  79. package/src/Form/FormData.elm +22 -0
  80. package/src/Form/Validation.elm +391 -0
  81. package/src/Form/Value.elm +118 -0
  82. package/src/Form.elm +1683 -0
  83. package/src/FormData.elm +58 -0
  84. package/src/FormDecoder.elm +102 -0
  85. package/src/Head/Seo.elm +12 -4
  86. package/src/Head.elm +12 -2
  87. package/src/HtmlPrinter.elm +1 -1
  88. package/src/Internal/ApiRoute.elm +17 -4
  89. package/src/Internal/Request.elm +7 -0
  90. package/src/PageServerResponse.elm +68 -0
  91. package/src/Pages/ContentCache.elm +1 -229
  92. package/src/Pages/Fetcher.elm +58 -0
  93. package/src/Pages/FormState.elm +256 -0
  94. package/src/Pages/Generate.elm +800 -0
  95. package/src/Pages/Internal/Form.elm +17 -0
  96. package/src/Pages/Internal/NotFoundReason.elm +3 -55
  97. package/src/Pages/Internal/Platform/Cli.elm +777 -579
  98. package/src/Pages/Internal/Platform/Effect.elm +5 -5
  99. package/src/Pages/Internal/Platform/StaticResponses.elm +178 -394
  100. package/src/Pages/Internal/Platform/ToJsPayload.elm +24 -23
  101. package/src/Pages/Internal/Platform.elm +1244 -504
  102. package/src/Pages/Internal/ResponseSketch.elm +19 -0
  103. package/src/Pages/Internal/RoutePattern.elm +596 -45
  104. package/src/Pages/Manifest.elm +26 -0
  105. package/src/Pages/Msg.elm +79 -0
  106. package/src/Pages/ProgramConfig.elm +67 -14
  107. package/src/Pages/SiteConfig.elm +3 -6
  108. package/src/Pages/StaticHttp/Request.elm +4 -2
  109. package/src/Pages/StaticHttpRequest.elm +50 -215
  110. package/src/Pages/Transition.elm +70 -0
  111. package/src/Path.elm +1 -0
  112. package/src/Pattern.elm +98 -0
  113. package/src/RenderRequest.elm +2 -2
  114. package/src/RequestsAndPending.elm +111 -9
  115. package/src/Server/Request.elm +1253 -0
  116. package/src/Server/Response.elm +292 -0
  117. package/src/Server/Session.elm +316 -0
  118. package/src/Server/SetCookie.elm +169 -0
  119. package/src/TerminalText.elm +1 -1
  120. package/src/Test/Html/Internal/ElmHtml/Markdown.elm +0 -1
  121. package/src/Test/Html/Internal/ElmHtml/ToString.elm +1 -1
  122. package/generator/src/Page.elm +0 -359
  123. package/generator/src/codegen-template-module.js +0 -183
  124. package/generator/src/elm-pages-js-minified.js +0 -1
  125. package/generator/template/src/Api.elm +0 -14
  126. package/generator/template/src/Page/Index.elm +0 -69
  127. package/generator/template/src/Site.elm +0 -41
  128. package/src/DataSource/ServerRequest.elm +0 -60
  129. package/src/Internal/OptimizedDecoder.elm +0 -18
  130. package/src/KeepOrDiscard.elm +0 -6
  131. package/src/OptimizedDecoder/Pipeline.elm +0 -335
  132. package/src/OptimizedDecoder.elm +0 -818
  133. package/src/Pages/Internal/ApplicationType.elm +0 -6
  134. package/src/Pages/Secrets.elm +0 -83
  135. package/src/Secrets.elm +0 -111
  136. package/src/SecretsDict.elm +0 -45
@@ -1,65 +1,93 @@
1
- module Pages.Internal.Platform exposing (Flags, Model, Msg, Program, application)
1
+ module Pages.Internal.Platform exposing
2
+ ( Flags, Model, Msg(..), Program, application, init, update
3
+ , Effect(..), RequestInfo, view
4
+ )
2
5
 
3
6
  {-| Exposed for internal use only (used in generated code).
4
7
 
5
- @docs Flags, Model, Msg, Program, application
8
+ @docs Flags, Model, Msg, Program, application, init, update
9
+
10
+ @docs Effect, RequestInfo, view
6
11
 
7
12
  -}
8
13
 
9
14
  import AriaLiveAnnouncer
15
+ import Base64
10
16
  import Browser
11
17
  import Browser.Dom as Dom
12
18
  import Browser.Navigation
13
19
  import BuildError exposing (BuildError)
20
+ import Bytes exposing (Bytes)
21
+ import Bytes.Decode
22
+ import Dict exposing (Dict)
23
+ import Form.FormData exposing (FormData, Method(..))
24
+ import FormDecoder
14
25
  import Html exposing (Html)
15
26
  import Html.Attributes as Attr
16
27
  import Http
17
28
  import Json.Decode as Decode
18
29
  import Json.Encode
19
- import Pages.ContentCache as ContentCache exposing (ContentCache, ContentJson, contentJsonDecoder)
30
+ import Pages.ContentCache as ContentCache
31
+ import Pages.Fetcher
20
32
  import Pages.Flags
21
- import Pages.Internal.ApplicationType as ApplicationType
22
- import Pages.Internal.NotFoundReason
33
+ import Pages.FormState
34
+ import Pages.Internal.NotFoundReason exposing (NotFoundReason)
35
+ import Pages.Internal.ResponseSketch as ResponseSketch exposing (ResponseSketch)
23
36
  import Pages.Internal.String as String
37
+ import Pages.Msg
24
38
  import Pages.ProgramConfig exposing (ProgramConfig)
25
39
  import Pages.StaticHttpRequest as StaticHttpRequest
40
+ import Pages.Transition
26
41
  import Path exposing (Path)
27
42
  import QueryParams
28
43
  import Task
44
+ import Time
29
45
  import Url exposing (Url)
30
46
 
31
47
 
48
+ type Transition
49
+ = Loading Int Path
50
+ | Submitting FormData
51
+
52
+
32
53
  {-| -}
33
- type alias Program userModel userMsg pageData sharedData =
34
- Platform.Program Flags (Model userModel pageData sharedData) (Msg userMsg)
54
+ type alias Program userModel userMsg pageData actionData sharedData errorPage =
55
+ Platform.Program Flags (Model userModel pageData actionData sharedData) (Msg userMsg pageData actionData sharedData errorPage)
35
56
 
36
57
 
37
58
  mainView :
38
- ProgramConfig userMsg userModel route siteData pageData sharedData
39
- -> Model userModel pageData sharedData
40
- -> { title : String, body : Html userMsg }
59
+ ProgramConfig userMsg userModel route pageData actionData sharedData effect (Msg userMsg pageData actionData sharedData errorPage) errorPage
60
+ -> Model userModel pageData actionData sharedData
61
+ -> { title : String, body : Html (Pages.Msg.Msg userMsg) }
41
62
  mainView config model =
42
- let
43
- urls : { currentUrl : Url, basePath : List String }
44
- urls =
45
- { currentUrl = model.url
46
- , basePath = config.basePath
47
- }
48
- in
49
- case ContentCache.notFoundReason model.contentCache urls of
50
- Just notFoundReason ->
51
- Pages.Internal.NotFoundReason.document config.pathPatterns notFoundReason
63
+ case model.notFound of
64
+ Just info ->
65
+ Pages.Internal.NotFoundReason.document config.pathPatterns info
52
66
 
53
67
  Nothing ->
54
68
  case model.pageData of
55
69
  Ok pageData ->
56
- (config.view
70
+ let
71
+ urls : { currentUrl : Url, basePath : List String }
72
+ urls =
73
+ { currentUrl = model.url
74
+ , basePath = config.basePath
75
+ }
76
+
77
+ currentUrl : Url
78
+ currentUrl =
79
+ model.url
80
+ in
81
+ (config.view model.pageFormState
82
+ (model.inFlightFetchers |> toFetcherState)
83
+ (model.transition |> Maybe.map Tuple.second)
57
84
  { path = ContentCache.pathForUrl urls |> Path.join
58
- , route = config.urlToRoute model.url
85
+ , route = config.urlToRoute { currentUrl | path = model.currentPath }
59
86
  }
60
87
  Nothing
61
88
  pageData.sharedData
62
89
  pageData.pageData
90
+ pageData.actionData
63
91
  |> .view
64
92
  )
65
93
  pageData.userModel
@@ -83,10 +111,11 @@ urlsToPagePath urls =
83
111
  |> Path.join
84
112
 
85
113
 
114
+ {-| -}
86
115
  view :
87
- ProgramConfig userMsg userModel route siteData pageData sharedData
88
- -> Model userModel pageData sharedData
89
- -> Browser.Document (Msg userMsg)
116
+ ProgramConfig userMsg userModel route pageData actionData sharedData effect (Msg userMsg pageData actionData sharedData errorPage) errorPage
117
+ -> Model userModel pageData actionData sharedData
118
+ -> Browser.Document (Msg userMsg pageData actionData sharedData errorPage)
90
119
  view config model =
91
120
  let
92
121
  { title, body } =
@@ -119,179 +148,186 @@ type alias Flags =
119
148
  Decode.Value
120
149
 
121
150
 
151
+ type InitKind shared page actionData errorPage
152
+ = OkPage shared page (Maybe actionData)
153
+ | NotFound { reason : NotFoundReason, path : Path }
154
+
155
+
122
156
  {-| -}
123
157
  init :
124
- ProgramConfig userMsg userModel route staticData pageData sharedData
158
+ ProgramConfig userMsg userModel route pageData actionData sharedData userEffect (Msg userMsg pageData actionData sharedData errorPage) errorPage
125
159
  -> Flags
126
160
  -> Url
127
- -> Browser.Navigation.Key
128
- -> ( Model userModel pageData sharedData, Cmd (Msg userMsg) )
161
+ -> Maybe Browser.Navigation.Key
162
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
129
163
  init config flags url key =
130
164
  let
131
- contentCache : ContentCache
132
- contentCache =
133
- ContentCache.init
134
- (Maybe.map
135
- (\cj ->
136
- ( currentPath
137
- , cj
138
- )
139
- )
140
- contentJson
141
- )
142
-
143
- currentPath : List String
144
- currentPath =
165
+ pageDataResult : Result BuildError (InitKind sharedData pageData actionData errorPage)
166
+ pageDataResult =
145
167
  flags
146
- |> Decode.decodeValue
147
- (Decode.at [ "contentJson", "path" ]
148
- (Decode.string
149
- |> Decode.map Path.fromString
150
- |> Decode.map Path.toSegments
151
- )
152
- )
153
- |> Result.mapError Decode.errorToString
168
+ |> Decode.decodeValue (Decode.field "pageDataBase64" Decode.string)
154
169
  |> Result.toMaybe
155
- |> Maybe.withDefault []
170
+ |> Maybe.andThen Base64.toBytes
171
+ |> Maybe.andThen
172
+ (\justBytes ->
173
+ case
174
+ Bytes.Decode.decode
175
+ -- TODO should this use byteDecodePageData, or should it be decoding ResponseSketch data?
176
+ config.decodeResponse
177
+ justBytes
178
+ of
179
+ Just (ResponseSketch.RenderPage _ _) ->
180
+ Nothing
156
181
 
157
- contentJson : Maybe ContentJson
158
- contentJson =
159
- flags
160
- |> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder)
161
- |> Result.toMaybe
182
+ Just (ResponseSketch.HotUpdate pageData shared actionData) ->
183
+ OkPage shared pageData actionData
184
+ |> Just
162
185
 
163
- urls : { currentUrl : Url, basePath : List String }
164
- urls =
165
- { currentUrl = url
166
- , basePath = config.basePath
167
- }
186
+ Just (ResponseSketch.NotFound notFound) ->
187
+ NotFound notFound
188
+ |> Just
189
+
190
+ _ ->
191
+ Nothing
192
+ )
193
+ |> Result.fromMaybe
194
+ (StaticHttpRequest.DecoderError "Bytes decode error"
195
+ |> StaticHttpRequest.toBuildError url.path
196
+ )
168
197
  in
169
- case contentJson |> Maybe.map .staticData of
170
- Just justContentJson ->
198
+ case pageDataResult of
199
+ Ok (OkPage sharedData pageData actionData) ->
171
200
  let
172
- pageDataResult : Result BuildError pageData
173
- pageDataResult =
174
- StaticHttpRequest.resolve ApplicationType.Browser
175
- (config.data (config.urlToRoute url))
176
- justContentJson
177
- |> Result.mapError (StaticHttpRequest.toBuildError url.path)
178
-
179
- sharedDataResult : Result BuildError sharedData
180
- sharedDataResult =
181
- StaticHttpRequest.resolve ApplicationType.Browser
182
- config.sharedData
183
- justContentJson
184
- |> Result.mapError (StaticHttpRequest.toBuildError url.path)
201
+ urls : { currentUrl : Url, basePath : List String }
202
+ urls =
203
+ { currentUrl = url
204
+ , basePath = config.basePath
205
+ }
185
206
 
186
207
  pagePath : Path
187
208
  pagePath =
188
209
  urlsToPagePath urls
189
- in
190
- case Result.map2 Tuple.pair sharedDataResult pageDataResult of
191
- Ok ( sharedData, pageData ) ->
192
- let
193
- userFlags : Pages.Flags.Flags
194
- userFlags =
195
- flags
196
- |> Decode.decodeValue
197
- (Decode.field "userFlags" Decode.value)
198
- |> Result.withDefault Json.Encode.null
199
- |> Pages.Flags.BrowserFlags
200
-
201
- ( userModel, userCmd ) =
210
+
211
+ userFlags : Pages.Flags.Flags
212
+ userFlags =
213
+ flags
214
+ |> Decode.decodeValue
215
+ (Decode.field "userFlags" Decode.value)
216
+ |> Result.withDefault Json.Encode.null
217
+ |> Pages.Flags.BrowserFlags
218
+
219
+ ( userModel, userCmd ) =
220
+ Just
221
+ { path =
222
+ { path = pagePath
223
+ , query = url.query
224
+ , fragment = url.fragment
225
+ }
226
+ , metadata = config.urlToRoute url
227
+ , pageUrl =
202
228
  Just
203
- { path =
204
- { path = pagePath
205
- , query = url.query
206
- , fragment = url.fragment
207
- }
208
- , metadata = config.urlToRoute url
209
- , pageUrl =
210
- Just
211
- { protocol = url.protocol
212
- , host = url.host
213
- , port_ = url.port_
214
- , path = pagePath
215
- , query = url.query |> Maybe.map QueryParams.fromString
216
- , fragment = url.fragment
217
- }
229
+ { protocol = url.protocol
230
+ , host = url.host
231
+ , port_ = url.port_
232
+ , path = pagePath
233
+ , query = url.query |> Maybe.map QueryParams.fromString
234
+ , fragment = url.fragment
218
235
  }
219
- |> config.init userFlags sharedData pageData (Just key)
220
-
221
- cmd : Cmd (Msg userMsg)
222
- cmd =
223
- [ userCmd
224
- |> Cmd.map UserMsg
225
- |> Just
226
- , contentCache
227
- |> ContentCache.lazyLoad urls
228
- |> Task.attempt UpdateCache
229
- |> Just
230
- ]
231
- |> List.filterMap identity
232
- |> Cmd.batch
233
-
234
- initialModel : Model userModel pageData sharedData
235
- initialModel =
236
- { key = key
237
- , url = url
238
- , contentCache = contentCache
239
- , pageData =
240
- Ok
241
- { pageData = pageData
242
- , sharedData = sharedData
243
- , userModel = userModel
244
- }
245
- , ariaNavigationAnnouncement = ""
246
- , userFlags = flags
236
+ }
237
+ |> config.init userFlags sharedData pageData actionData
238
+
239
+ cmd : Effect userMsg pageData actionData sharedData userEffect errorPage
240
+ cmd =
241
+ UserCmd userCmd
242
+
243
+ initialModel : Model userModel pageData actionData sharedData
244
+ initialModel =
245
+ { key = key
246
+ , url = url
247
+ , currentPath = url.path
248
+ , pageData =
249
+ Ok
250
+ { pageData = pageData
251
+ , sharedData = sharedData
252
+ , userModel = userModel
253
+ , actionData = actionData
247
254
  }
248
- in
249
- ( { initialModel
250
- | ariaNavigationAnnouncement = mainView config initialModel |> .title
251
- }
252
- , cmd
253
- )
255
+ , ariaNavigationAnnouncement = ""
256
+ , userFlags = flags
257
+ , notFound = Nothing
258
+ , transition = Nothing
259
+ , nextTransitionKey = 0
260
+ , inFlightFetchers = Dict.empty
261
+ , pageFormState = Dict.empty
262
+ , pendingRedirect = False
263
+ , pendingData = Nothing
264
+ }
265
+ in
266
+ ( { initialModel
267
+ | ariaNavigationAnnouncement = mainView config initialModel |> .title
268
+ }
269
+ , cmd
270
+ )
254
271
 
255
- Err error ->
256
- ( { key = key
257
- , url = url
258
- , contentCache = contentCache
259
- , pageData = BuildError.errorToString error |> Err
260
- , ariaNavigationAnnouncement = "Error"
261
- , userFlags = flags
262
- }
263
- , Cmd.none
264
- )
272
+ Ok (NotFound info) ->
273
+ ( { key = key
274
+ , url = url
275
+ , currentPath = url.path
276
+ , pageData = Err "Not found"
277
+ , ariaNavigationAnnouncement = "Error" -- TODO use error page title for announcement?
278
+ , userFlags = flags
279
+ , notFound = Just info
280
+ , transition = Nothing
281
+ , nextTransitionKey = 0
282
+ , inFlightFetchers = Dict.empty
283
+ , pageFormState = Dict.empty
284
+ , pendingRedirect = False
285
+ , pendingData = Nothing
286
+ }
287
+ , NoEffect
288
+ )
265
289
 
266
- Nothing ->
290
+ Err error ->
267
291
  ( { key = key
268
292
  , url = url
269
- , contentCache = contentCache
270
- , pageData = Err "TODO"
293
+ , currentPath = url.path
294
+ , pageData =
295
+ error
296
+ |> BuildError.errorToString
297
+ |> Err
271
298
  , ariaNavigationAnnouncement = "Error"
272
299
  , userFlags = flags
300
+ , notFound = Nothing
301
+ , transition = Nothing
302
+ , nextTransitionKey = 0
303
+ , inFlightFetchers = Dict.empty
304
+ , pageFormState = Dict.empty
305
+ , pendingRedirect = False
306
+ , pendingData = Nothing
273
307
  }
274
- , Cmd.none
308
+ , NoEffect
275
309
  )
276
310
 
277
311
 
278
312
  {-| -}
279
- type Msg userMsg
313
+ type Msg userMsg pageData actionData sharedData errorPage
280
314
  = LinkClicked Browser.UrlRequest
281
315
  | UrlChanged Url
282
- | UserMsg userMsg
283
- | UpdateCache (Result Http.Error ( Url, ContentJson, ContentCache ))
284
- | UpdateCacheAndUrl Url (Result Http.Error ( Url, ContentJson, ContentCache ))
316
+ | UserMsg (Pages.Msg.Msg userMsg)
317
+ | SetField { formId : String, name : String, value : String }
318
+ | UpdateCacheAndUrlNew Bool Url (Maybe userMsg) (Result Http.Error ( Url, ResponseSketch pageData actionData sharedData ))
319
+ | FetcherComplete Bool String Int (Result Http.Error ( Maybe userMsg, Maybe actionData ))
320
+ | FetcherStarted String Int FormData Time.Posix
285
321
  | PageScrollComplete
286
- | HotReloadComplete ContentJson
287
- | NoOp
322
+ | HotReloadCompleteNew Bytes
323
+ | ProcessFetchResponse Int (Result Http.Error ( Url, ResponseSketch pageData actionData sharedData )) (Result Http.Error ( Url, ResponseSketch pageData actionData sharedData ) -> Msg userMsg pageData actionData sharedData errorPage)
288
324
 
289
325
 
290
326
  {-| -}
291
- type alias Model userModel pageData sharedData =
292
- { key : Browser.Navigation.Key
327
+ type alias Model userModel pageData actionData sharedData =
328
+ { key : Maybe Browser.Navigation.Key
293
329
  , url : Url
294
- , contentCache : ContentCache
330
+ , currentPath : String
295
331
  , ariaNavigationAnnouncement : String
296
332
  , pageData :
297
333
  Result
@@ -299,17 +335,40 @@ type alias Model userModel pageData sharedData =
299
335
  { userModel : userModel
300
336
  , pageData : pageData
301
337
  , sharedData : sharedData
338
+ , actionData : Maybe actionData
302
339
  }
340
+ , notFound : Maybe { reason : NotFoundReason, path : Path }
303
341
  , userFlags : Decode.Value
342
+ , transition : Maybe ( Int, Pages.Transition.Transition )
343
+ , nextTransitionKey : Int
344
+ , inFlightFetchers : Dict String ( Int, Pages.Transition.FetcherState actionData )
345
+ , pageFormState : Pages.FormState.PageFormState
346
+ , pendingRedirect : Bool
347
+ , pendingData : Maybe ( pageData, sharedData, Maybe actionData )
304
348
  }
305
349
 
306
350
 
351
+ {-| -}
352
+ type Effect userMsg pageData actionData sharedData userEffect errorPage
353
+ = ScrollToTop
354
+ | NoEffect
355
+ | BrowserLoadUrl String
356
+ | BrowserPushUrl String
357
+ | BrowserReplaceUrl String
358
+ | FetchPageData Int (Maybe FormData) Url (Result Http.Error ( Url, ResponseSketch pageData actionData sharedData ) -> Msg userMsg pageData actionData sharedData errorPage)
359
+ | Submit FormData
360
+ | SubmitFetcher String Int FormData
361
+ | Batch (List (Effect userMsg pageData actionData sharedData userEffect errorPage))
362
+ | UserCmd userEffect
363
+ | CancelRequest Int
364
+
365
+
307
366
  {-| -}
308
367
  update :
309
- ProgramConfig userMsg userModel route siteData pageData sharedData
310
- -> Msg userMsg
311
- -> Model userModel pageData sharedData
312
- -> ( Model userModel pageData sharedData, Cmd (Msg userMsg) )
368
+ ProgramConfig userMsg userModel route pageData actionData sharedData userEffect (Msg userMsg pageData actionData sharedData errorPage) errorPage
369
+ -> Msg userMsg pageData actionData sharedData errorPage
370
+ -> Model userModel pageData actionData sharedData
371
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
313
372
  update config appMsg model =
314
373
  case appMsg of
315
374
  LinkClicked urlRequest ->
@@ -318,185 +377,308 @@ update config appMsg model =
318
377
  let
319
378
  navigatingToSamePage : Bool
320
379
  navigatingToSamePage =
321
- (url.path == model.url.path) && (url /= model.url)
380
+ url.path == model.url.path
322
381
  in
323
382
  if navigatingToSamePage then
324
383
  -- this is a workaround for an issue with anchor fragment navigation
325
384
  -- see https://github.com/elm/browser/issues/39
326
- ( model, Browser.Navigation.load (Url.toString url) )
385
+ ( model
386
+ , BrowserLoadUrl (Url.toString url)
387
+ )
327
388
 
328
389
  else
329
390
  ( model
330
- , Browser.Navigation.pushUrl model.key (Url.toString url)
391
+ , BrowserPushUrl url.path
331
392
  )
332
393
 
333
394
  Browser.External href ->
334
- ( model, Browser.Navigation.load href )
395
+ ( model
396
+ , BrowserLoadUrl href
397
+ )
398
+
399
+ SetField info ->
400
+ ( { model | pageFormState = Pages.FormState.setField info model.pageFormState }
401
+ , NoEffect
402
+ )
335
403
 
336
404
  UrlChanged url ->
337
- let
338
- navigatingToSamePage : Bool
339
- navigatingToSamePage =
340
- (url.path == model.url.path) && (url /= model.url)
405
+ case model.pendingData of
406
+ Just ( newPageData, newSharedData, newActionData ) ->
407
+ loadDataAndUpdateUrl
408
+ ( newPageData, newSharedData, newActionData )
409
+ Nothing
410
+ url
411
+ url
412
+ False
413
+ config
414
+ model
415
+
416
+ Nothing ->
417
+ if model.url.path == url.path then
418
+ ( { model
419
+ | -- update the URL in case query params or fragment changed
420
+ url = url
421
+ }
422
+ , NoEffect
423
+ )
341
424
 
342
- urls : { currentUrl : Url, basePath : List String }
343
- urls =
344
- { currentUrl = url
345
- , basePath = config.basePath
346
- }
347
- in
348
- if navigatingToSamePage then
349
- -- this saves a few CPU cycles, but also
350
- -- makes sure we don't send an UpdateCacheAndUrl
351
- -- which scrolls to the top after page changes.
352
- -- This is important because we may have just scrolled
353
- -- to a specific page location for an anchor link.
354
- model.pageData
355
- |> Result.map
356
- (\pageData ->
357
- let
358
- updatedPageData : Result String { userModel : userModel, sharedData : sharedData, pageData : pageData }
359
- updatedPageData =
360
- Ok
361
- { userModel = userModel
362
- , sharedData = pageData.sharedData
363
- , pageData = pageData.pageData
364
- }
425
+ else
426
+ ( { model
427
+ | url = url
428
+ }
429
+ , NoEffect
430
+ )
431
+ -- TODO is it reasonable to always re-fetch route data if you re-navigate to the current route? Might be a good
432
+ -- parallel to the browser behavior
433
+ |> startNewGetLoad url (UpdateCacheAndUrlNew True url Nothing)
365
434
 
366
- ( userModel, userCmd ) =
367
- config.update
368
- pageData.sharedData
369
- pageData.pageData
370
- (Just model.key)
371
- (config.onPageChange
372
- { protocol = model.url.protocol
373
- , host = model.url.host
374
- , port_ = model.url.port_
375
- , path = urlPathToPath config urls.currentUrl
376
- , query = url.query
377
- , fragment = url.fragment
378
- , metadata = config.urlToRoute url
379
- }
435
+ FetcherComplete forPageReload fetcherKey transitionId___ userMsgResult ->
436
+ case userMsgResult of
437
+ Ok ( userMsg, maybeFetcherDoneActionData ) ->
438
+ ( { model
439
+ | inFlightFetchers =
440
+ model.inFlightFetchers
441
+ |> Dict.update fetcherKey
442
+ (Maybe.map
443
+ (\( transitionId, fetcherState ) ->
444
+ ( transitionId
445
+ , { fetcherState
446
+ | status =
447
+ maybeFetcherDoneActionData
448
+ |> Maybe.map Pages.Transition.FetcherReloading
449
+ -- TODO remove this bad default, FetcherSubmitting is incorrect
450
+ |> Maybe.withDefault Pages.Transition.FetcherSubmitting
451
+ }
452
+ )
380
453
  )
381
- pageData.userModel
382
- in
383
- ( { model
384
- | url = url
385
- , pageData = updatedPageData
386
- }
387
- , Cmd.none
388
- --Cmd.batch
389
- -- [ userCmd |> Cmd.map UserMsg
390
- -- , Task.perform (\_ -> PageScrollComplete) (Dom.setViewport 0 0)
391
- -- ]
392
- )
454
+ )
455
+ }
456
+ , NoEffect
457
+ )
458
+ |> (case userMsg of
459
+ Just justUserMsg ->
460
+ performUserMsg justUserMsg config
461
+
462
+ Nothing ->
463
+ identity
464
+ )
465
+ |> startNewGetLoad (currentUrlWithPath model.url.path model) (UpdateCacheAndUrlNew False model.url Nothing)
466
+
467
+ Err _ ->
468
+ -- TODO how to handle error?
469
+ ( model, NoEffect )
470
+ |> startNewGetLoad (currentUrlWithPath model.url.path model) (UpdateCacheAndUrlNew False model.url Nothing)
471
+
472
+ ProcessFetchResponse transitionId response toMsg ->
473
+ case response of
474
+ Ok ( _, ResponseSketch.Redirect redirectTo ) ->
475
+ ( model, NoEffect )
476
+ |> startNewGetLoad (currentUrlWithPath redirectTo model) toMsg
477
+
478
+ _ ->
479
+ update config (toMsg response) (clearLoadingFetchersAfterDataLoad transitionId model)
480
+
481
+ UserMsg userMsg_ ->
482
+ case userMsg_ of
483
+ Pages.Msg.UserMsg userMsg ->
484
+ ( model, NoEffect )
485
+ |> performUserMsg userMsg config
486
+
487
+ Pages.Msg.Submit fields ->
488
+ ( { model
489
+ | transition =
490
+ Just
491
+ ( -- TODO remove hardcoded number
492
+ -1
493
+ , Pages.Transition.Submitting fields
494
+ )
495
+ }
496
+ , Submit fields
497
+ )
498
+
499
+ Pages.Msg.SubmitIfValid formId fields isValid ->
500
+ if isValid then
501
+ ( { model
502
+ -- TODO should I setSubmitAttempted here, too?
503
+ | transition =
504
+ Just
505
+ ( -- TODO remove hardcoded number
506
+ -1
507
+ , Pages.Transition.Submitting fields
508
+ )
509
+ }
510
+ , Submit fields
393
511
  )
394
- |> Result.withDefault ( model, Cmd.none )
395
512
 
396
- else
397
- ( model
398
- , model.contentCache
399
- |> ContentCache.lazyLoad urls
400
- |> Task.attempt (UpdateCacheAndUrl url)
401
- )
513
+ else
514
+ ( { model
515
+ | pageFormState =
516
+ model.pageFormState
517
+ |> Pages.FormState.setSubmitAttempted formId
518
+ }
519
+ , NoEffect
520
+ )
402
521
 
403
- UserMsg userMsg ->
404
- case model.pageData of
405
- Ok pageData ->
406
- let
407
- ( userModel, userCmd ) =
408
- config.update pageData.sharedData pageData.pageData (Just model.key) userMsg pageData.userModel
522
+ Pages.Msg.SubmitFetcher fetcherKey fields isValid maybeUserMsg ->
523
+ if isValid then
524
+ -- TODO should I setSubmitAttempted here, too?
525
+ ( { model | nextTransitionKey = model.nextTransitionKey + 1 }
526
+ , SubmitFetcher fetcherKey model.nextTransitionKey fields
527
+ )
528
+ |> (case maybeUserMsg of
529
+ Just justUserMsg ->
530
+ performUserMsg justUserMsg config
409
531
 
410
- updatedPageData : Result error { userModel : userModel, pageData : pageData, sharedData : sharedData }
411
- updatedPageData =
412
- Ok { pageData | userModel = userModel }
413
- in
414
- ( { model | pageData = updatedPageData }, userCmd |> Cmd.map UserMsg )
532
+ Nothing ->
533
+ identity
534
+ )
415
535
 
416
- Err error ->
417
- ( model, Cmd.none )
418
-
419
- UpdateCache cacheUpdateResult ->
420
- case cacheUpdateResult of
421
- -- TODO can there be race conditions here? Might need to set something in the model
422
- -- to keep track of the last url change
423
- Ok ( url, contentJson, updatedCache ) ->
424
- ( { model | contentCache = updatedCache }
425
- , Cmd.none
426
- )
536
+ else
537
+ ( { model
538
+ | pageFormState =
539
+ model.pageFormState
540
+ |> Pages.FormState.setSubmitAttempted fetcherKey
541
+ }
542
+ , NoEffect
543
+ )
427
544
 
428
- Err _ ->
429
- -- TODO handle error
430
- ( model, Cmd.none )
545
+ Pages.Msg.FormFieldEvent value ->
546
+ -- TODO when init is called for a new page, also need to clear out client-side `pageFormState`
547
+ ( { model | pageFormState = Pages.FormState.update value model.pageFormState }
548
+ , NoEffect
549
+ )
431
550
 
432
- UpdateCacheAndUrl url cacheUpdateResult ->
551
+ UpdateCacheAndUrlNew scrollToTopWhenDone urlWithoutRedirectResolution maybeUserMsg updateResult ->
552
+ -- TODO remove all fetchers that are in the state `FetcherReloading` here -- I think that's the right logic?
433
553
  case
434
- Result.map2 Tuple.pair (cacheUpdateResult |> Result.mapError (\error -> "Http error")) model.pageData
554
+ Result.map2 Tuple.pair
555
+ (updateResult
556
+ |> Result.mapError (\_ -> "Http error")
557
+ )
558
+ model.pageData
435
559
  of
436
- -- TODO can there be race conditions here? Might need to set something in the model
437
- -- to keep track of the last url change
438
- Ok ( ( _, contentJson, updatedCache ), pageData ) ->
560
+ Ok ( ( newUrl, newData ), previousPageData ) ->
439
561
  let
440
- updatedPageData : Result String { userModel : userModel, sharedData : sharedData, pageData : pageData }
441
- updatedPageData =
442
- updatedPageStaticData
443
- |> Result.map
444
- (\pageStaticData ->
445
- { userModel = userModel
446
- , sharedData = pageData.sharedData
447
- , pageData = pageStaticData
448
- }
449
- )
562
+ redirectPending : Bool
563
+ redirectPending =
564
+ newUrl /= urlWithoutRedirectResolution
565
+ in
566
+ if redirectPending then
567
+ ( { model
568
+ | pendingRedirect = True
569
+ , pendingData =
570
+ case newData of
571
+ ResponseSketch.RenderPage pageData actionData ->
572
+ Just ( pageData, previousPageData.sharedData, actionData )
573
+
574
+ ResponseSketch.HotUpdate pageData sharedData actionData ->
575
+ Just ( pageData, sharedData, actionData )
576
+
577
+ _ ->
578
+ Nothing
579
+ }
580
+ , BrowserReplaceUrl newUrl.path
581
+ )
582
+
583
+ else
584
+ let
585
+ stayingOnSamePath : Bool
586
+ stayingOnSamePath =
587
+ newUrl.path == model.url.path
588
+
589
+ ( newPageData, newSharedData, newActionData ) =
590
+ case newData of
591
+ ResponseSketch.RenderPage pageData actionData ->
592
+ ( pageData, previousPageData.sharedData, actionData )
593
+
594
+ ResponseSketch.HotUpdate pageData sharedData actionData ->
595
+ ( pageData, sharedData, actionData )
596
+
597
+ _ ->
598
+ ( previousPageData.pageData, previousPageData.sharedData, previousPageData.actionData )
599
+
600
+ updatedPageData : { userModel : userModel, sharedData : sharedData, actionData : Maybe actionData, pageData : pageData }
601
+ updatedPageData =
602
+ { userModel = userModel
603
+ , sharedData = newSharedData
604
+ , pageData = newPageData
605
+ , actionData = newActionData
606
+ }
450
607
 
451
- updatedPageStaticData : Result String pageData
452
- updatedPageStaticData =
453
- StaticHttpRequest.resolve ApplicationType.Browser
454
- (config.data (config.urlToRoute url))
455
- contentJson.staticData
456
- |> Result.mapError
457
- (\error ->
458
- error
459
- |> StaticHttpRequest.toBuildError ""
460
- |> BuildError.errorToString
608
+ ( userModel, _ ) =
609
+ -- TODO if urlWithoutRedirectResolution is different from the url with redirect resolution, then
610
+ -- instead of calling update, call pushUrl (I think?)
611
+ -- TODO include user Cmd
612
+ config.update model.pageFormState
613
+ (model.inFlightFetchers |> toFetcherState)
614
+ (model.transition |> Maybe.map Tuple.second)
615
+ newSharedData
616
+ newPageData
617
+ model.key
618
+ (config.onPageChange
619
+ { protocol = model.url.protocol
620
+ , host = model.url.host
621
+ , port_ = model.url.port_
622
+ , path = urlPathToPath urlWithoutRedirectResolution
623
+ , query = urlWithoutRedirectResolution.query
624
+ , fragment = urlWithoutRedirectResolution.fragment
625
+ , metadata = config.urlToRoute urlWithoutRedirectResolution
626
+ }
461
627
  )
628
+ previousPageData.userModel
629
+
630
+ updatedModel : Model userModel pageData actionData sharedData
631
+ updatedModel =
632
+ -- TODO should these be the same (no if)?
633
+ if model.pendingRedirect || redirectPending then
634
+ { model
635
+ | url = newUrl
636
+ , pageData = Ok updatedPageData
637
+ , transition = Nothing
638
+ , pendingRedirect = False
639
+ , pageFormState = Dict.empty
640
+ }
462
641
 
463
- ( userModel, userCmd ) =
464
- config.update
465
- pageData.sharedData
466
- (updatedPageStaticData |> Result.withDefault pageData.pageData)
467
- (Just model.key)
468
- (config.onPageChange
469
- { protocol = model.url.protocol
470
- , host = model.url.host
471
- , port_ = model.url.port_
472
- , path = url |> urlPathToPath config
473
- , query = url.query
474
- , fragment = url.fragment
475
- , metadata = config.urlToRoute url
642
+ else
643
+ { model
644
+ | url = newUrl
645
+ , pageData = Ok updatedPageData
646
+ , pendingRedirect = False
647
+ , transition = Nothing
476
648
  }
477
- )
478
- pageData.userModel
479
649
 
480
- updatedModel : Model userModel pageData sharedData
481
- updatedModel =
482
- { model
483
- | url = url
484
- , contentCache = updatedCache
485
- , pageData = updatedPageData
486
- }
487
- in
488
- ( { updatedModel
489
- | ariaNavigationAnnouncement = mainView config updatedModel |> .title
490
- }
491
- , Cmd.batch
492
- [ userCmd |> Cmd.map UserMsg
493
- , Task.perform (\_ -> PageScrollComplete) (Dom.setViewport 0 0)
494
- ]
495
- )
650
+ onActionMsg : Maybe userMsg
651
+ onActionMsg =
652
+ newActionData |> Maybe.andThen config.onActionData
653
+ in
654
+ ( { updatedModel
655
+ | ariaNavigationAnnouncement = mainView config updatedModel |> .title
656
+ , currentPath = newUrl.path
657
+ }
658
+ , if not stayingOnSamePath && scrollToTopWhenDone then
659
+ ScrollToTop
660
+
661
+ else
662
+ NoEffect
663
+ )
664
+ |> (case maybeUserMsg of
665
+ Just userMsg ->
666
+ withUserMsg config userMsg
496
667
 
497
- Err error ->
668
+ Nothing ->
669
+ identity
670
+ )
671
+ |> (case onActionMsg of
672
+ Just actionMsg ->
673
+ withUserMsg config actionMsg
674
+
675
+ Nothing ->
676
+ identity
677
+ )
678
+
679
+ Err _ ->
498
680
  {-
499
- When there is an error loading the content.json, we are either
681
+ When there is an error loading the content.dat, we are either
500
682
  1) in the dev server, and should show the relevant DataSource error for the page
501
683
  we're navigating to. This could be done more cleanly, but it's simplest to just
502
684
  do a fresh page load and use the code path for presenting an error for a fresh page.
@@ -509,237 +691,795 @@ update config appMsg model =
509
691
 
510
692
  -}
511
693
  ( model
512
- , url
694
+ , urlWithoutRedirectResolution
513
695
  |> Url.toString
514
- |> Browser.Navigation.load
696
+ |> BrowserLoadUrl
515
697
  )
516
698
 
517
699
  PageScrollComplete ->
518
- ( model, Cmd.none )
700
+ ( model, NoEffect )
701
+
702
+ HotReloadCompleteNew pageDataBytes ->
703
+ model.pageData
704
+ |> Result.map
705
+ (\pageData ->
706
+ let
707
+ newThing : Maybe (ResponseSketch pageData actionData sharedData)
708
+ newThing =
709
+ -- TODO if ErrorPage, call ErrorPage.init to get appropriate Model?
710
+ pageDataBytes
711
+ |> Bytes.Decode.decode config.decodeResponse
712
+ in
713
+ case newThing of
714
+ Just (ResponseSketch.RenderPage newPageData newActionData) ->
715
+ ( { model
716
+ | pageData =
717
+ Ok
718
+ { userModel = pageData.userModel
719
+ , sharedData = pageData.sharedData
720
+ , pageData = newPageData
721
+ , actionData = newActionData
722
+ }
723
+ , notFound = Nothing
724
+ }
725
+ , NoEffect
726
+ )
727
+
728
+ Just (ResponseSketch.HotUpdate newPageData newSharedData newActionData) ->
729
+ ( { model
730
+ | pageData =
731
+ Ok
732
+ { userModel = pageData.userModel
733
+ , sharedData = newSharedData
734
+ , pageData = newPageData
735
+ , actionData = newActionData
736
+ }
737
+ , notFound = Nothing
738
+ }
739
+ , NoEffect
740
+ )
741
+
742
+ Just (ResponseSketch.NotFound info) ->
743
+ ( { model | notFound = Just info }, NoEffect )
744
+
745
+ _ ->
746
+ ( model, NoEffect )
747
+ )
748
+ |> Result.withDefault ( model, NoEffect )
749
+
750
+ FetcherStarted fetcherKey transitionId fetcherData initiatedAt ->
751
+ ( { model
752
+ | inFlightFetchers =
753
+ model.inFlightFetchers
754
+ |> Dict.insert fetcherKey
755
+ ( transitionId
756
+ , { payload = fetcherData
757
+ , status = Pages.Transition.FetcherSubmitting
758
+ , initiatedAt = initiatedAt
759
+ }
760
+ )
761
+ }
762
+ , NoEffect
763
+ )
764
+
765
+
766
+ toFetcherState : Dict String ( Int, Pages.Transition.FetcherState actionData ) -> Dict String (Pages.Transition.FetcherState actionData)
767
+ toFetcherState inFlightFetchers =
768
+ inFlightFetchers
769
+ |> Dict.map (\_ ( index, fetcherState ) -> fetcherState)
519
770
 
520
- HotReloadComplete contentJson ->
771
+
772
+ performUserMsg :
773
+ userMsg
774
+ -> ProgramConfig userMsg userModel route pageData actionData sharedData userEffect (Msg userMsg pageData actionData sharedData errorPage) errorPage
775
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
776
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
777
+ performUserMsg userMsg config ( model, effect ) =
778
+ case model.pageData of
779
+ Ok pageData ->
521
780
  let
522
- urls : { currentUrl : Url, basePath : List String }
523
- urls =
524
- { currentUrl = model.url
525
- , basePath = config.basePath
526
- }
781
+ ( userModel, userCmd ) =
782
+ config.update model.pageFormState (model.inFlightFetchers |> toFetcherState) (model.transition |> Maybe.map Tuple.second) pageData.sharedData pageData.pageData model.key userMsg pageData.userModel
527
783
 
528
- pageDataResult : Result BuildError pageData
529
- pageDataResult =
530
- StaticHttpRequest.resolve ApplicationType.Browser
531
- (config.data (config.urlToRoute model.url))
532
- contentJson.staticData
533
- |> Result.mapError (StaticHttpRequest.toBuildError model.url.path)
534
-
535
- sharedDataResult : Result BuildError sharedData
536
- sharedDataResult =
537
- StaticHttpRequest.resolve ApplicationType.Browser
538
- config.sharedData
539
- contentJson.staticData
540
- |> Result.mapError (StaticHttpRequest.toBuildError model.url.path)
541
-
542
- from404ToNon404 : Bool
543
- from404ToNon404 =
544
- not contentJson.is404
545
- && was404
546
-
547
- was404 : Bool
548
- was404 =
549
- ContentCache.is404 model.contentCache urls
784
+ updatedPageData : Result error { userModel : userModel, pageData : pageData, actionData : Maybe actionData, sharedData : sharedData }
785
+ updatedPageData =
786
+ Ok { pageData | userModel = userModel }
550
787
  in
551
- case Result.map2 Tuple.pair sharedDataResult pageDataResult of
552
- Ok ( sharedData, pageData ) ->
553
- let
554
- updateResult : Maybe ( userModel, Cmd userMsg )
555
- updateResult =
556
- if from404ToNon404 then
557
- case model.pageData of
558
- Ok pageData_ ->
559
- config.update
560
- sharedData
561
- pageData
562
- (Just model.key)
563
- (config.onPageChange
564
- { protocol = model.url.protocol
565
- , host = model.url.host
566
- , port_ = model.url.port_
567
- , path = model.url |> urlPathToPath config
568
- , query = model.url.query
569
- , fragment = model.url.fragment
570
- , metadata = config.urlToRoute model.url
571
- }
572
- )
573
- pageData_.userModel
574
- |> Just
788
+ ( { model | pageData = updatedPageData }
789
+ , Batch [ effect, UserCmd userCmd ]
790
+ )
575
791
 
576
- Err error ->
577
- Nothing
792
+ Err _ ->
793
+ ( model, effect )
578
794
 
579
- else
580
- Nothing
795
+
796
+ perform : ProgramConfig userMsg userModel route pageData actionData sharedData userEffect (Msg userMsg pageData actionData sharedData errorPage) errorPage -> Model userModel pageData actionData sharedData -> Effect userMsg pageData actionData sharedData userEffect errorPage -> Cmd (Msg userMsg pageData actionData sharedData errorPage)
797
+ perform config model effect =
798
+ -- elm-review: known-unoptimized-recursion
799
+ case effect of
800
+ NoEffect ->
801
+ Cmd.none
802
+
803
+ Batch effects ->
804
+ effects
805
+ |> List.map (perform config model)
806
+ |> Cmd.batch
807
+
808
+ ScrollToTop ->
809
+ Task.perform (\_ -> PageScrollComplete) (Dom.setViewport 0 0)
810
+
811
+ BrowserLoadUrl url ->
812
+ Browser.Navigation.load url
813
+
814
+ BrowserPushUrl url ->
815
+ model.key
816
+ |> Maybe.map
817
+ (\key ->
818
+ Browser.Navigation.pushUrl key url
819
+ )
820
+ |> Maybe.withDefault Cmd.none
821
+
822
+ BrowserReplaceUrl url ->
823
+ model.key
824
+ |> Maybe.map
825
+ (\key ->
826
+ Browser.Navigation.replaceUrl key url
827
+ )
828
+ |> Maybe.withDefault Cmd.none
829
+
830
+ FetchPageData transitionKey maybeRequestInfo url toMsg ->
831
+ fetchRouteData True transitionKey toMsg config url maybeRequestInfo
832
+
833
+ Submit fields ->
834
+ if fields.method == Get then
835
+ model.key
836
+ |> Maybe.map (\key -> Browser.Navigation.pushUrl key (appendFormQueryParams fields))
837
+ |> Maybe.withDefault Cmd.none
838
+
839
+ else
840
+ let
841
+ urlToSubmitTo : Url
842
+ urlToSubmitTo =
843
+ -- TODO add optional path parameter to Submit variant to allow submitting to other routes
844
+ model.url
845
+ in
846
+ fetchRouteData False -1 (UpdateCacheAndUrlNew False model.url Nothing) config urlToSubmitTo (Just fields)
847
+
848
+ SubmitFetcher fetcherKey transitionId formData ->
849
+ startFetcher2 config False fetcherKey transitionId formData model
850
+
851
+ UserCmd cmd ->
852
+ case model.key of
853
+ Just key ->
854
+ let
855
+ prepare :
856
+ (Result Http.Error Url -> userMsg)
857
+ -> Result Http.Error ( Url, ResponseSketch pageData actionData sharedData )
858
+ -> Msg userMsg pageData actionData sharedData errorPage
859
+ prepare toMsg info =
860
+ UpdateCacheAndUrlNew False model.url (info |> Result.map Tuple.first |> toMsg |> Just) info
581
861
  in
582
- case updateResult of
583
- Just ( userModel, userCmd ) ->
584
- ( { model
585
- | contentCache =
586
- ContentCache.init
587
- (Just
588
- ( urls.currentUrl
589
- |> config.urlToRoute
590
- |> config.routeToPath
591
- , contentJson
592
- )
593
- )
594
- , pageData =
595
- Ok
596
- { pageData = pageData
597
- , sharedData = sharedData
598
- , userModel = userModel
599
- }
600
- }
601
- , Cmd.batch
602
- [ userCmd |> Cmd.map UserMsg
603
- ]
604
- )
862
+ cmd
863
+ |> config.perform
864
+ { fetchRouteData =
865
+ \fetchInfo ->
866
+ fetchRouteData False
867
+ -1
868
+ (prepare fetchInfo.toMsg)
869
+ config
870
+ (urlFromAction model.url fetchInfo.data)
871
+ fetchInfo.data
872
+
873
+ ---- TODO map the Msg with the wrapper type (like in the PR branch)
874
+ , submit =
875
+ \fetchInfo ->
876
+ fetchRouteData False -1 (prepare fetchInfo.toMsg) config (fetchInfo.values.action |> Url.fromString |> Maybe.withDefault model.url) (Just fetchInfo.values)
877
+ , runFetcher =
878
+ \(Pages.Fetcher.Fetcher options) ->
879
+ -- TODO need to get the fetcherId here
880
+ -- TODO need to increment and pass in the transitionId
881
+ startFetcher "TODO" -1 options model
882
+ , fromPageMsg = Pages.Msg.UserMsg >> UserMsg
883
+ , key = key
884
+ , setField = \info -> Task.succeed (SetField info) |> Task.perform identity
885
+ }
605
886
 
606
- Nothing ->
607
- let
608
- pagePath : Path
609
- pagePath =
610
- urlsToPagePath urls
611
-
612
- userFlags : Pages.Flags.Flags
613
- userFlags =
614
- model.userFlags
615
- |> Decode.decodeValue
616
- (Decode.field "userFlags" Decode.value)
617
- |> Result.withDefault Json.Encode.null
618
- |> Pages.Flags.BrowserFlags
619
-
620
- ( userModel, userCmd ) =
621
- Just
622
- { path =
623
- { path = pagePath
624
- , query = model.url.query
625
- , fragment = model.url.fragment
626
- }
627
- , metadata = config.urlToRoute model.url
628
- , pageUrl =
629
- Just
630
- { protocol = model.url.protocol
631
- , host = model.url.host
632
- , port_ = model.url.port_
633
- , path = pagePath
634
- , query = model.url.query |> Maybe.map QueryParams.fromString
635
- , fragment = model.url.fragment
636
- }
637
- }
638
- |> config.init userFlags sharedData pageData (Just model.key)
639
- in
640
- ( { model
641
- | contentCache =
642
- ContentCache.init
643
- (Just
644
- ( urls.currentUrl
645
- |> config.urlToRoute
646
- |> config.routeToPath
647
- , contentJson
648
- )
649
- )
650
- , pageData =
651
- model.pageData
652
- |> Result.map
653
- (\previousPageData ->
654
- { pageData = pageData
655
- , sharedData = sharedData
656
- , userModel = previousPageData.userModel
657
- }
658
- )
659
- |> Result.withDefault
660
- { pageData = pageData
661
- , sharedData = sharedData
662
- , userModel = userModel
663
- }
664
- |> Ok
665
- }
666
- , userCmd |> Cmd.map UserMsg
667
- )
887
+ Nothing ->
888
+ Cmd.none
668
889
 
669
- Err error ->
670
- ( { model
671
- | contentCache =
672
- ContentCache.init
673
- (Just
674
- ( urls.currentUrl
675
- |> config.urlToRoute
676
- |> config.routeToPath
677
- , contentJson
678
- )
890
+ CancelRequest transitionKey ->
891
+ Http.cancel (String.fromInt transitionKey)
892
+
893
+
894
+ startFetcher : String -> Int -> { fields : List ( String, String ), url : Maybe String, decoder : Result error Bytes -> value, headers : List ( String, String ) } -> Model userModel pageData actionData sharedData -> Cmd (Msg value pageData actionData sharedData errorPage)
895
+ startFetcher fetcherKey transitionId options model =
896
+ let
897
+ encodedBody : String
898
+ encodedBody =
899
+ FormDecoder.encodeFormData
900
+ { fields = options.fields
901
+
902
+ -- TODO remove hardcoding
903
+ , action = ""
904
+
905
+ -- TODO remove hardcoding
906
+ , method = Post
907
+ , id = Nothing
908
+ }
909
+
910
+ formData : { method : Method, action : String, fields : List ( String, String ), id : Maybe String }
911
+ formData =
912
+ { -- TODO remove hardcoding
913
+ method = Get
914
+
915
+ -- TODO pass FormData directly
916
+ , action = options.url |> Maybe.withDefault model.url.path
917
+ , fields = options.fields
918
+ , id = Nothing
919
+ }
920
+ in
921
+ -- TODO make sure that `actionData` isn't updated in Model for fetchers
922
+ Cmd.batch
923
+ [ cancelStaleFetchers model
924
+ , Time.now |> Task.map (FetcherStarted fetcherKey transitionId formData) |> Task.perform identity
925
+ , Http.request
926
+ { expect =
927
+ Http.expectBytesResponse (FetcherComplete False fetcherKey model.nextTransitionKey)
928
+ (\bytes ->
929
+ case bytes of
930
+ Http.GoodStatus_ metadata bytesBody ->
931
+ ( options.decoder (Ok bytesBody)
932
+ |> Just
933
+ , Nothing
679
934
  )
680
- }
681
- , Cmd.none
935
+ |> Ok
936
+
937
+ Http.BadUrl_ string ->
938
+ Err <| Http.BadUrl string
939
+
940
+ Http.Timeout_ ->
941
+ Err <| Http.Timeout
942
+
943
+ Http.NetworkError_ ->
944
+ Err <| Http.NetworkError
945
+
946
+ Http.BadStatus_ metadata body ->
947
+ Err <| Http.BadStatus metadata.statusCode
682
948
  )
949
+ , tracker = Nothing
950
+ , body = Http.stringBody "application/x-www-form-urlencoded" encodedBody
951
+ , headers = options.headers |> List.map (\( name, value ) -> Http.header name value)
952
+ , url = options.url |> Maybe.withDefault (Path.join [ model.url.path, "content.dat" ] |> Path.toAbsolute)
953
+ , method = "POST"
954
+ , timeout = Nothing
955
+ }
956
+ ]
957
+
958
+
959
+ startFetcher2 :
960
+ ProgramConfig userMsg userModel route pageData actionData sharedData effect (Msg userMsg pageData actionData sharedData errorPage) errorPage
961
+ -> Bool
962
+ -> String
963
+ -> Int
964
+ -> FormData
965
+ -> Model userModel pageData actionData sharedData
966
+ -> Cmd (Msg userMsg pageData actionData sharedData errorPage)
967
+ startFetcher2 config fromPageReload fetcherKey transitionId formData model =
968
+ let
969
+ encodedBody : String
970
+ encodedBody =
971
+ FormDecoder.encodeFormData formData
972
+ in
973
+ -- TODO make sure that `actionData` isn't updated in Model for fetchers
974
+ Cmd.batch
975
+ [ cancelStaleFetchers model
976
+ , case Dict.get fetcherKey model.inFlightFetchers of
977
+ Just ( inFlightId, inFlightFetcher ) ->
978
+ Http.cancel (String.fromInt inFlightId)
979
+
980
+ Nothing ->
981
+ Cmd.none
982
+ , Time.now |> Task.map (FetcherStarted fetcherKey transitionId formData) |> Task.perform identity
983
+ , Http.request
984
+ { expect =
985
+ Http.expectBytesResponse (FetcherComplete fromPageReload fetcherKey model.nextTransitionKey)
986
+ (\bytes ->
987
+ case bytes of
988
+ Http.GoodStatus_ metadata bytesBody ->
989
+ let
990
+ decodedAction : Maybe actionData
991
+ decodedAction =
992
+ case Bytes.Decode.decode config.decodeResponse bytesBody of
993
+ Just (ResponseSketch.RenderPage _ maybeAction) ->
994
+ maybeAction
995
+
996
+ Just (ResponseSketch.HotUpdate pageData shared maybeAction) ->
997
+ maybeAction
998
+
999
+ Just (ResponseSketch.NotFound notFound) ->
1000
+ Nothing
1001
+
1002
+ _ ->
1003
+ Nothing
1004
+ in
1005
+ -- TODO maybe have an optional way to pass the bytes through?
1006
+ Ok ( Nothing, decodedAction )
1007
+
1008
+ Http.BadUrl_ string ->
1009
+ Err <| Http.BadUrl string
1010
+
1011
+ Http.Timeout_ ->
1012
+ Err <| Http.Timeout
1013
+
1014
+ Http.NetworkError_ ->
1015
+ Err <| Http.NetworkError
1016
+
1017
+ Http.BadStatus_ metadata body ->
1018
+ Err <| Http.BadStatus metadata.statusCode
1019
+ )
1020
+ , tracker = Just (String.fromInt transitionId)
1021
+
1022
+ -- TODO use formData.method to do either query params or POST body
1023
+ , body = Http.stringBody "application/x-www-form-urlencoded" encodedBody
1024
+ , headers = []
1025
+
1026
+ -- TODO use formData.method to do either query params or POST body
1027
+ , url = formData.action |> Url.fromString |> Maybe.map (\{ path } -> Path.join [ path, "content.dat" ] |> Path.toAbsolute) |> Maybe.withDefault "/"
1028
+ , method = formData.method |> FormDecoder.methodToString
1029
+ , timeout = Nothing
1030
+ }
1031
+ ]
1032
+
1033
+
1034
+ cancelStaleFetchers : Model userModel pageData actionData sharedData -> Cmd msg
1035
+ cancelStaleFetchers model =
1036
+ model.inFlightFetchers
1037
+ |> Dict.toList
1038
+ |> List.filterMap
1039
+ (\( fetcherKey, ( id, fetcher ) ) ->
1040
+ case fetcher.status of
1041
+ Pages.Transition.FetcherReloading _ ->
1042
+ Http.cancel (String.fromInt id)
1043
+ |> Just
1044
+
1045
+ Pages.Transition.FetcherSubmitting ->
1046
+ Nothing
1047
+
1048
+ Pages.Transition.FetcherComplete _ ->
1049
+ Nothing
1050
+ )
1051
+ |> Cmd.batch
1052
+
683
1053
 
684
- NoOp ->
685
- ( model, Cmd.none )
1054
+ appendFormQueryParams : FormData -> String
1055
+ appendFormQueryParams fields =
1056
+ (fields.action
1057
+ |> Url.fromString
1058
+ |> Maybe.map .path
1059
+ |> Maybe.withDefault "/"
1060
+ )
1061
+ ++ (case fields.method of
1062
+ Get ->
1063
+ "?" ++ FormDecoder.encodeFormData fields
1064
+
1065
+ Post ->
1066
+ ""
1067
+ )
1068
+
1069
+
1070
+ urlFromAction : Url -> Maybe FormData -> Url
1071
+ urlFromAction currentUrl fetchInfo =
1072
+ fetchInfo |> Maybe.map .action |> Maybe.andThen Url.fromString |> Maybe.withDefault currentUrl
686
1073
 
687
1074
 
688
1075
  {-| -}
689
1076
  application :
690
- ProgramConfig userMsg userModel route staticData pageData sharedData
691
- -> Platform.Program Flags (Model userModel pageData sharedData) (Msg userMsg)
1077
+ ProgramConfig userMsg userModel route pageData actionData sharedData effect (Msg userMsg pageData actionData sharedData errorPage) errorPage
1078
+ -> Platform.Program Flags (Model userModel pageData actionData sharedData) (Msg userMsg pageData actionData sharedData errorPage)
692
1079
  application config =
693
1080
  Browser.application
694
1081
  { init =
695
1082
  \flags url key ->
696
- init config flags url key
1083
+ let
1084
+ ( model, effect ) =
1085
+ init config flags url (Just key)
1086
+ in
1087
+ ( model
1088
+ , effect |> perform config model
1089
+ )
697
1090
  , view = view config
698
- , update = update config
1091
+ , update =
1092
+ \msg model ->
1093
+ update config msg model |> Tuple.mapSecond (perform config model)
699
1094
  , subscriptions =
700
1095
  \model ->
701
- let
702
- urls : { currentUrl : Url }
703
- urls =
704
- { currentUrl = model.url }
705
- in
706
1096
  case model.pageData of
707
1097
  Ok pageData ->
1098
+ let
1099
+ urls : { currentUrl : Url }
1100
+ urls =
1101
+ { currentUrl = model.url }
1102
+ in
708
1103
  Sub.batch
709
1104
  [ config.subscriptions (model.url |> config.urlToRoute)
710
1105
  (urls.currentUrl |> config.urlToRoute |> config.routeToPath |> Path.join)
711
1106
  pageData.userModel
712
- |> Sub.map UserMsg
713
- , config.fromJsPort
714
- |> Sub.map
715
- (\decodeValue ->
716
- case decodeValue |> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder) of
717
- Ok contentJson ->
718
- HotReloadComplete contentJson
719
-
720
- Err _ ->
721
- -- TODO should be no message here
722
- NoOp
723
- )
1107
+ |> Sub.map (Pages.Msg.UserMsg >> UserMsg)
1108
+ , config.hotReloadData
1109
+ |> Sub.map HotReloadCompleteNew
724
1110
  ]
725
1111
 
726
1112
  Err _ ->
727
- config.fromJsPort
728
- |> Sub.map
729
- (\decodeValue ->
730
- case decodeValue |> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder) of
731
- Ok contentJson ->
732
- HotReloadComplete contentJson
733
-
734
- Err _ ->
735
- -- TODO should be no message here
736
- NoOp
737
- )
1113
+ config.hotReloadData
1114
+ |> Sub.map HotReloadCompleteNew
738
1115
  , onUrlChange = UrlChanged
739
1116
  , onUrlRequest = LinkClicked
740
1117
  }
741
1118
 
742
1119
 
743
- urlPathToPath : ProgramConfig userMsg userModel route siteData pageData sharedData -> Url -> Path
744
- urlPathToPath config urls =
1120
+ {-| -}
1121
+ type alias RequestInfo =
1122
+ { contentType : String
1123
+ , body : String
1124
+ }
1125
+
1126
+
1127
+ withUserMsg :
1128
+ ProgramConfig userMsg userModel route pageData actionData sharedData userEffect (Msg userMsg pageData actionData sharedData errorPage) errorPage
1129
+ -> userMsg
1130
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
1131
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
1132
+ withUserMsg config userMsg ( model, effect ) =
1133
+ case model.pageData of
1134
+ Ok pageData ->
1135
+ let
1136
+ ( userModel, userCmd ) =
1137
+ config.update model.pageFormState (model.inFlightFetchers |> toFetcherState) (model.transition |> Maybe.map Tuple.second) pageData.sharedData pageData.pageData model.key userMsg pageData.userModel
1138
+
1139
+ updatedPageData : Result error { userModel : userModel, pageData : pageData, actionData : Maybe actionData, sharedData : sharedData }
1140
+ updatedPageData =
1141
+ Ok { pageData | userModel = userModel }
1142
+ in
1143
+ ( { model | pageData = updatedPageData }
1144
+ , Batch
1145
+ [ effect
1146
+ , UserCmd userCmd
1147
+ ]
1148
+ )
1149
+
1150
+ Err _ ->
1151
+ ( model, effect )
1152
+
1153
+
1154
+ urlPathToPath : Url -> Path
1155
+ urlPathToPath urls =
745
1156
  urls.path |> Path.fromString
1157
+
1158
+
1159
+ fetchRouteData :
1160
+ Bool
1161
+ -> Int
1162
+ -> (Result Http.Error ( Url, ResponseSketch pageData actionData sharedData ) -> Msg userMsg pageData actionData sharedData errorPage)
1163
+ -> ProgramConfig userMsg userModel route pageData actionData sharedData effect (Msg userMsg pageData actionData sharedData errorPage) errorPage
1164
+ -> Url
1165
+ -> Maybe FormData
1166
+ -> Cmd (Msg userMsg pageData actionData sharedData errorPage)
1167
+ fetchRouteData forPageDataReload transitionKey toMsg config url details =
1168
+ {-
1169
+ TODO:
1170
+ - [X] `toMsg` needs a parameter for the callback Msg so it can pass it on if there is a Redirect response
1171
+ - [X] Handle ResponseSketch.Redirect in `update`
1172
+ - [ ] Set transition state when loading
1173
+ - [ ] Set transition state when submitting
1174
+ - [ ] Should transition state for redirect after submit be the same as a regular load transition state?
1175
+ - [ ] Expose transition state (in Shared?)
1176
+ - [ ] Abort stale transitions
1177
+ - [ ] Increment cancel key counter in Model on new transitions
1178
+
1179
+ -}
1180
+ let
1181
+ formMethod : Method
1182
+ formMethod =
1183
+ details
1184
+ |> Maybe.map .method
1185
+ |> Maybe.withDefault Get
1186
+ in
1187
+ Http.request
1188
+ { method = details |> Maybe.map (.method >> FormDecoder.methodToString) |> Maybe.withDefault "GET"
1189
+ , headers = []
1190
+ , url =
1191
+ "/"
1192
+ ++ ((details
1193
+ |> Maybe.map .action
1194
+ |> Maybe.andThen Url.fromString
1195
+ -- TODO what should happen when there is an action pointing to a different domain? Should it be a no-op? Log a warning?
1196
+ |> Maybe.withDefault url
1197
+ )
1198
+ |> .path
1199
+ |> chopForwardSlashes
1200
+ |> String.split "/"
1201
+ |> List.filter ((/=) "")
1202
+ |> (\l -> l ++ [ "content.dat" ])
1203
+ |> String.join "/"
1204
+ )
1205
+ ++ (case formMethod of
1206
+ Post ->
1207
+ "/"
1208
+
1209
+ Get ->
1210
+ details
1211
+ |> Maybe.map FormDecoder.encodeFormData
1212
+ |> Maybe.map (\encoded -> "?" ++ encoded)
1213
+ |> Maybe.withDefault ""
1214
+ )
1215
+ ++ (case formMethod of
1216
+ -- TODO extract this to something unit testable
1217
+ -- TODO make states mutually exclusive for submissions and direct URL requests (shouldn't be possible to append two query param strings)
1218
+ Post ->
1219
+ ""
1220
+
1221
+ Get ->
1222
+ url.query
1223
+ |> Maybe.map (\encoded -> "?" ++ encoded)
1224
+ |> Maybe.withDefault ""
1225
+ )
1226
+ , body =
1227
+ case formMethod of
1228
+ Post ->
1229
+ let
1230
+ urlEncodedFields : Maybe String
1231
+ urlEncodedFields =
1232
+ details
1233
+ |> Maybe.map FormDecoder.encodeFormData
1234
+ in
1235
+ urlEncodedFields
1236
+ |> Maybe.map (\encoded -> Http.stringBody "application/x-www-form-urlencoded" encoded)
1237
+ |> Maybe.withDefault Http.emptyBody
1238
+
1239
+ _ ->
1240
+ Http.emptyBody
1241
+ , expect =
1242
+ Http.expectBytesResponse (\response -> ProcessFetchResponse transitionKey response toMsg)
1243
+ (\response ->
1244
+ case response of
1245
+ Http.BadUrl_ url_ ->
1246
+ Err (Http.BadUrl url_)
1247
+
1248
+ Http.Timeout_ ->
1249
+ Err Http.Timeout
1250
+
1251
+ Http.NetworkError_ ->
1252
+ Err Http.NetworkError
1253
+
1254
+ Http.BadStatus_ metadata body ->
1255
+ body
1256
+ |> Bytes.Decode.decode config.decodeResponse
1257
+ |> Result.fromMaybe "Decoding error"
1258
+ |> Result.mapError Http.BadBody
1259
+ |> Result.map (\okResponse -> ( url, okResponse ))
1260
+
1261
+ Http.GoodStatus_ _ body ->
1262
+ body
1263
+ |> Bytes.Decode.decode config.decodeResponse
1264
+ |> Result.fromMaybe "Decoding error"
1265
+ |> Result.mapError Http.BadBody
1266
+ |> Result.map (\okResponse -> ( url, okResponse ))
1267
+ )
1268
+ , timeout = Nothing
1269
+ , tracker = Just (String.fromInt transitionKey)
1270
+ }
1271
+
1272
+
1273
+ chopForwardSlashes : String -> String
1274
+ chopForwardSlashes =
1275
+ chopStart "/" >> chopEnd "/"
1276
+
1277
+
1278
+ chopStart : String -> String -> String
1279
+ chopStart needle string =
1280
+ if String.startsWith needle string then
1281
+ chopStart needle (String.dropLeft (String.length needle) string)
1282
+
1283
+ else
1284
+ string
1285
+
1286
+
1287
+ chopEnd : String -> String -> String
1288
+ chopEnd needle string =
1289
+ if String.endsWith needle string then
1290
+ chopEnd needle (String.dropRight (String.length needle) string)
1291
+
1292
+ else
1293
+ string
1294
+
1295
+
1296
+ startNewGetLoad :
1297
+ Url
1298
+ -> (Result Http.Error ( Url, ResponseSketch pageData actionData sharedData ) -> Msg userMsg pageData actionData sharedData errorPage)
1299
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
1300
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
1301
+ startNewGetLoad urlToGet toMsg ( model, effect ) =
1302
+ let
1303
+ cancelIfStale : Effect userMsg pageData actionData sharedData userEffect errorPage
1304
+ cancelIfStale =
1305
+ case model.transition of
1306
+ Just ( transitionKey, Pages.Transition.Loading path loadingKind ) ->
1307
+ CancelRequest transitionKey
1308
+
1309
+ _ ->
1310
+ NoEffect
1311
+ in
1312
+ ( { model
1313
+ | nextTransitionKey = model.nextTransitionKey + 1
1314
+ , transition =
1315
+ ( model.nextTransitionKey
1316
+ , case model.transition of
1317
+ Just ( transitionKey, Pages.Transition.LoadAfterSubmit submitData _ _ ) ->
1318
+ Pages.Transition.LoadAfterSubmit
1319
+ submitData
1320
+ (urlToGet.path |> Path.fromString)
1321
+ Pages.Transition.Load
1322
+
1323
+ Just ( transitionKey, Pages.Transition.Submitting submitData ) ->
1324
+ Pages.Transition.LoadAfterSubmit
1325
+ submitData
1326
+ (urlToGet.path |> Path.fromString)
1327
+ Pages.Transition.Load
1328
+
1329
+ _ ->
1330
+ Pages.Transition.Loading
1331
+ (urlToGet.path |> Path.fromString)
1332
+ Pages.Transition.Load
1333
+ )
1334
+ |> Just
1335
+ }
1336
+ , Batch
1337
+ [ FetchPageData
1338
+ model.nextTransitionKey
1339
+ Nothing
1340
+ urlToGet
1341
+ toMsg
1342
+ , cancelIfStale
1343
+ , effect
1344
+ ]
1345
+ )
1346
+
1347
+
1348
+ clearLoadingFetchersAfterDataLoad : Int -> Model userModel pageData actionData sharedData -> Model userModel pageData actionData sharedData
1349
+ clearLoadingFetchersAfterDataLoad completedTransitionId model =
1350
+ { model
1351
+ | inFlightFetchers =
1352
+ model.inFlightFetchers
1353
+ |> Dict.map
1354
+ (\_ ( transitionId, fetcherState ) ->
1355
+ -- TODO fetchers are never removed from the list. Need to decide how and when to remove them.
1356
+ --(fetcherState.status /= Pages.Transition.FetcherReloading) || (transitionId > completedTransitionId)
1357
+ case ( transitionId > completedTransitionId, fetcherState.status ) of
1358
+ ( False, Pages.Transition.FetcherReloading actionData ) ->
1359
+ ( transitionId
1360
+ , { fetcherState | status = Pages.Transition.FetcherComplete actionData }
1361
+ )
1362
+
1363
+ _ ->
1364
+ ( transitionId, fetcherState )
1365
+ )
1366
+ }
1367
+
1368
+
1369
+ currentUrlWithPath : String -> Model userModel pageData actionData sharedData -> Url
1370
+ currentUrlWithPath path { url } =
1371
+ { url | path = path }
1372
+
1373
+
1374
+ loadDataAndUpdateUrl :
1375
+ ( pageData, sharedData, Maybe actionData )
1376
+ -> Maybe userMsg
1377
+ -> Url
1378
+ -> Url
1379
+ -> Bool
1380
+ -> ProgramConfig userMsg userModel route pageData actionData sharedData userEffect (Msg userMsg pageData actionData sharedData errorPage) errorPage
1381
+ -> Model userModel pageData actionData sharedData
1382
+ -> ( Model userModel pageData actionData sharedData, Effect userMsg pageData actionData sharedData userEffect errorPage )
1383
+ loadDataAndUpdateUrl ( newPageData, newSharedData, newActionData ) maybeUserMsg urlWithoutRedirectResolution newUrl redirectPending config model =
1384
+ case model.pageData of
1385
+ Ok previousPageData ->
1386
+ let
1387
+ updatedPageData : { userModel : userModel, sharedData : sharedData, actionData : Maybe actionData, pageData : pageData }
1388
+ updatedPageData =
1389
+ { userModel = userModel
1390
+ , sharedData = newSharedData
1391
+ , pageData = newPageData
1392
+ , actionData = newActionData
1393
+ }
1394
+
1395
+ ( userModel, _ ) =
1396
+ -- TODO if urlWithoutRedirectResolution is different from the url with redirect resolution, then
1397
+ -- instead of calling update, call pushUrl (I think?)
1398
+ -- TODO include user Cmd
1399
+ config.update model.pageFormState
1400
+ (model.inFlightFetchers |> toFetcherState)
1401
+ (model.transition |> Maybe.map Tuple.second)
1402
+ newSharedData
1403
+ newPageData
1404
+ model.key
1405
+ (config.onPageChange
1406
+ { protocol = model.url.protocol
1407
+ , host = model.url.host
1408
+ , port_ = model.url.port_
1409
+ , path = urlPathToPath urlWithoutRedirectResolution
1410
+ , query = urlWithoutRedirectResolution.query
1411
+ , fragment = urlWithoutRedirectResolution.fragment
1412
+ , metadata = config.urlToRoute urlWithoutRedirectResolution
1413
+ }
1414
+ )
1415
+ previousPageData.userModel
1416
+
1417
+ updatedModel : Model userModel pageData actionData sharedData
1418
+ updatedModel =
1419
+ -- TODO should these be the same (no if)?
1420
+ if model.pendingRedirect || redirectPending then
1421
+ { model
1422
+ | url = newUrl
1423
+ , pageData = Ok updatedPageData
1424
+ , transition = Nothing
1425
+ , pendingRedirect = False
1426
+ , pageFormState = Dict.empty
1427
+
1428
+ --, inFlightFetchers = Dict.empty
1429
+ , pendingData = Nothing
1430
+ }
1431
+
1432
+ else
1433
+ { model
1434
+ | url = newUrl
1435
+ , pageData = Ok updatedPageData
1436
+ , pendingRedirect = False
1437
+ , transition = Nothing
1438
+ , inFlightFetchers = Dict.empty
1439
+ , pendingData = Nothing
1440
+ }
1441
+
1442
+ onActionMsg : Maybe userMsg
1443
+ onActionMsg =
1444
+ newActionData |> Maybe.andThen config.onActionData
1445
+ in
1446
+ ( { updatedModel
1447
+ | ariaNavigationAnnouncement = mainView config updatedModel |> .title
1448
+ , currentPath = newUrl.path
1449
+ }
1450
+ , ScrollToTop
1451
+ )
1452
+ |> (case maybeUserMsg of
1453
+ Just userMsg ->
1454
+ withUserMsg config userMsg
1455
+
1456
+ Nothing ->
1457
+ identity
1458
+ )
1459
+ |> (case onActionMsg of
1460
+ Just actionMsg ->
1461
+ withUserMsg config actionMsg
1462
+
1463
+ Nothing ->
1464
+ identity
1465
+ )
1466
+
1467
+ Err _ ->
1468
+ {-
1469
+ When there is an error loading the content.dat, we are either
1470
+ 1) in the dev server, and should show the relevant DataSource error for the page
1471
+ we're navigating to. This could be done more cleanly, but it's simplest to just
1472
+ do a fresh page load and use the code path for presenting an error for a fresh page.
1473
+ 2) In a production app. That means we had a successful build, so there were no DataSource failures,
1474
+ so the app must be stale (unless it's in some unexpected state from a bug). In the future,
1475
+ it probably makes sense to include some sort of hash of the app version we are fetching, match
1476
+ it with the current version that's running, and perform this logic when we see there is a mismatch.
1477
+ But for now, if there is any error we do a full page load (not a single-page navigation), which
1478
+ gives us a fresh version of the app to make sure things are in sync.
1479
+
1480
+ -}
1481
+ ( model
1482
+ , urlWithoutRedirectResolution
1483
+ |> Url.toString
1484
+ |> BrowserLoadUrl
1485
+ )