elm-ssr 0.2.0 → 0.3.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.
- package/README.md +48 -342
- package/elm-src/ElmSsr/Island/Sse.elm +151 -0
- package/package.json +53 -24
- package/{packages/elm-ssr/src → src}/client-runtime/islands.ts +113 -15
- package/src/sse.ts +162 -0
- package/AGENTS.md +0 -289
- package/CHANGELOG.md +0 -87
- package/LICENSE +0 -21
- package/bun.lock +0 -259
- package/docker-compose.yml +0 -33
- package/docs/README.md +0 -51
- package/docs/backends.md +0 -146
- package/docs/cli.md +0 -117
- package/docs/effects.md +0 -91
- package/docs/getting-started.md +0 -94
- package/docs/islands.md +0 -197
- package/docs/loaders-and-actions.md +0 -241
- package/docs/middleware.md +0 -93
- package/docs/migrations.md +0 -143
- package/docs/routing.md +0 -108
- package/docs/sessions.md +0 -218
- package/docs/tasks.md +0 -149
- package/docs/testing.md +0 -84
- package/elm-ssr.config.json +0 -14
- package/examples/basic/elm.json +0 -27
- package/examples/basic/migrations/0001_guestbook.down.sql +0 -1
- package/examples/basic/migrations/0001_guestbook.sql +0 -10
- package/examples/basic/package.json +0 -10
- package/examples/basic/runtime.ts +0 -148
- package/examples/basic/src/Example/Basic/Islands/Counter.elm +0 -110
- package/examples/basic/src/Example/Basic/Islands/Observer.elm +0 -67
- package/examples/basic/src/Example/Basic/Islands/Tasks.elm +0 -151
- package/examples/basic/src/Example/Basic/Routes/Chart.elm +0 -87
- package/examples/basic/src/Example/Basic/Routes/Counter.elm +0 -42
- package/examples/basic/src/Example/Basic/Routes/Echo.elm +0 -76
- package/examples/basic/src/Example/Basic/Routes/Greet/Name_.elm +0 -37
- package/examples/basic/src/Example/Basic/Routes/Guestbook.elm +0 -86
- package/examples/basic/src/Example/Basic/Routes/Index.elm +0 -41
- package/examples/basic/src/Example/Basic/Routes/NotFound.elm +0 -37
- package/examples/basic/src/Example/Basic/Routes/Profile.elm +0 -112
- package/examples/basic/src/Example/Basic/Routes/Session.elm +0 -89
- package/examples/basic/src/Example/Basic/Routes/Status.elm +0 -90
- package/examples/basic/src/Example/Basic/View/Shared.elm +0 -60
- package/examples/basic/styles.ts +0 -204
- package/examples/basic/worker.ts +0 -3
- package/examples/crypto-dashboard/elm.json +0 -30
- package/examples/crypto-dashboard/package.json +0 -10
- package/examples/crypto-dashboard/runtime.ts +0 -97
- package/examples/crypto-dashboard/src/CryptoDashboard/Islands/MarketOverview.elm +0 -204
- package/examples/crypto-dashboard/src/CryptoDashboard/Islands/PriceChart.elm +0 -200
- package/examples/crypto-dashboard/src/CryptoDashboard/Routes/Index.elm +0 -67
- package/examples/crypto-dashboard/src/CryptoDashboard/Routes/NotFound.elm +0 -30
- package/examples/crypto-dashboard/src/CryptoDashboard/View/Shared.elm +0 -39
- package/examples/crypto-dashboard/styles.ts +0 -23
- package/examples/crypto-dashboard/worker.ts +0 -3
- package/llms.txt +0 -69
- package/packages/elm-ssr/README.md +0 -67
- package/packages/elm-ssr/package.json +0 -61
- package/scripts/benchmark.mjs +0 -60
- package/test/action.test.ts +0 -81
- package/test/adapters.test.ts +0 -173
- package/test/advanced-robustness.test.ts +0 -75
- package/test/app.test.ts +0 -209
- package/test/browser-island.test.ts +0 -184
- package/test/cli-migrate.test.ts +0 -97
- package/test/cli.test.ts +0 -94
- package/test/cookies.test.ts +0 -156
- package/test/crypto-dashboard.test.ts +0 -35
- package/test/effects.test.ts +0 -117
- package/test/http.test.ts +0 -50
- package/test/integration/redis-postgres.test.ts +0 -174
- package/test/island-runtime.test.ts +0 -214
- package/test/middleware.test.ts +0 -134
- package/test/migrations.test.ts +0 -244
- package/test/profile.test.ts +0 -159
- package/test/robustness.test.ts +0 -135
- package/test/serialize.test.ts +0 -92
- package/test/sessions.test.ts +0 -429
- package/test/svg.test.ts +0 -65
- package/tsconfig.json +0 -20
- package/wrangler.jsonc +0 -11
- /package/{packages/elm-ssr/bin → bin}/elm-ssr.mjs +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Action.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Encode.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document/Events.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Document.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Attributes.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html/Events.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Html.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island/Shared.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Island.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Loader.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Page.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Route.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Runtime.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg/Attributes.elm +0 -0
- /package/{packages/elm-ssr/elm-src → elm-src}/ElmSsr/Svg.elm +0 -0
- /package/{packages/elm-ssr/lib → lib}/build.mjs +0 -0
- /package/{packages/elm-ssr/lib → lib}/migrate.mjs +0 -0
- /package/{packages/elm-ssr/lib → lib}/scaffold.mjs +0 -0
- /package/{packages/elm-ssr/lib → lib}/workspace.mjs +0 -0
- /package/{packages/elm-ssr/src → src}/app.ts +0 -0
- /package/{packages/elm-ssr/src → src}/backends.ts +0 -0
- /package/{packages/elm-ssr/src → src}/effects.ts +0 -0
- /package/{packages/elm-ssr/src → src}/http.ts +0 -0
- /package/{packages/elm-ssr/src → src}/middleware.ts +0 -0
- /package/{packages/elm-ssr/src → src}/migrations.ts +0 -0
- /package/{packages/elm-ssr/src → src}/protocol.ts +0 -0
- /package/{packages/elm-ssr/src → src}/render.ts +0 -0
- /package/{packages/elm-ssr/src → src}/request-handler.ts +0 -0
- /package/{packages/elm-ssr/src → src}/response-headers.ts +0 -0
- /package/{packages/elm-ssr/src → src}/serialize.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/crypto.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/effects.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/index.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/middleware.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/store.ts +0 -0
- /package/{packages/elm-ssr/src → src}/sessions/types.ts +0 -0
- /package/{packages/elm-ssr/src → src}/tasks.ts +0 -0
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Islands.Counter exposing
|
|
2
|
-
( embed
|
|
3
|
-
, Flags, Model, Msg
|
|
4
|
-
, encodeFlags
|
|
5
|
-
, init, main, subscriptions, update, view
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
import Browser
|
|
9
|
-
import ElmSsr.Html as SsrHtml exposing (Node)
|
|
10
|
-
import ElmSsr.Html.Attributes as SsrAttributes
|
|
11
|
-
import ElmSsr.Island as Island
|
|
12
|
-
import ElmSsr.Island.Shared as Shared
|
|
13
|
-
import Html exposing (Html, button, code, div, span, text)
|
|
14
|
-
import Html.Attributes exposing (class, type_)
|
|
15
|
-
import Html.Events exposing (onClick)
|
|
16
|
-
import Json.Encode as Encode
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type alias Flags =
|
|
20
|
-
{ start : Int
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type alias Model =
|
|
25
|
-
{ count : Int
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
type Msg
|
|
30
|
-
= Increment
|
|
31
|
-
| Decrement
|
|
32
|
-
| Reset
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
embed : Flags -> Node msg
|
|
36
|
-
embed =
|
|
37
|
-
Island.embed "Counter"
|
|
38
|
-
{ encodeFlags = encodeFlags
|
|
39
|
-
, fallback = fallback
|
|
40
|
-
, id = Just "global-counter"
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
encodeFlags : Flags -> Encode.Value
|
|
45
|
-
encodeFlags flags =
|
|
46
|
-
Encode.object [ ( "start", Encode.int flags.start ) ]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
fallback : Flags -> List (Node msg)
|
|
50
|
-
fallback flags =
|
|
51
|
-
[ SsrHtml.div [ SsrAttributes.class "counter-island fallback" ]
|
|
52
|
-
[ SsrHtml.div [ SsrAttributes.class "counter-actions" ]
|
|
53
|
-
[ SsrHtml.span [ SsrAttributes.class "counter-value" ] [ SsrHtml.text (String.fromInt flags.start) ] ]
|
|
54
|
-
, SsrHtml.code [ SsrAttributes.class "counter-code" ] [ SsrHtml.text "Loading counter island…" ]
|
|
55
|
-
]
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
main : Program Flags Model Msg
|
|
60
|
-
main =
|
|
61
|
-
Browser.element
|
|
62
|
-
{ init = init
|
|
63
|
-
, update = update
|
|
64
|
-
, view = view
|
|
65
|
-
, subscriptions = subscriptions
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
init : Flags -> ( Model, Cmd Msg )
|
|
70
|
-
init flags =
|
|
71
|
-
( { count = flags.start }, Cmd.none )
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
update : Msg -> Model -> ( Model, Cmd Msg )
|
|
75
|
-
update msg model =
|
|
76
|
-
case msg of
|
|
77
|
-
Increment ->
|
|
78
|
-
let
|
|
79
|
-
newCount =
|
|
80
|
-
model.count + 1
|
|
81
|
-
in
|
|
82
|
-
( { model | count = newCount }, Shared.broadcast "count-changed" (Encode.int newCount) )
|
|
83
|
-
|
|
84
|
-
Decrement ->
|
|
85
|
-
let
|
|
86
|
-
newCount =
|
|
87
|
-
model.count - 1
|
|
88
|
-
in
|
|
89
|
-
( { model | count = newCount }, Shared.broadcast "count-changed" (Encode.int newCount) )
|
|
90
|
-
|
|
91
|
-
Reset ->
|
|
92
|
-
( { model | count = 0 }, Shared.broadcast "count-changed" (Encode.int 0) )
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
subscriptions : Model -> Sub Msg
|
|
96
|
-
subscriptions _ =
|
|
97
|
-
Sub.none
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
view : Model -> Html Msg
|
|
101
|
-
view model =
|
|
102
|
-
div [ class "counter-island" ]
|
|
103
|
-
[ div [ class "counter-actions" ]
|
|
104
|
-
[ button [ class "counter-button", type_ "button", onClick Decrement ] [ text "-" ]
|
|
105
|
-
, span [ class "counter-value" ] [ text (String.fromInt model.count) ]
|
|
106
|
-
, button [ class "counter-button primary", type_ "button", onClick Increment ] [ text "+" ]
|
|
107
|
-
, button [ class "counter-button", type_ "button", onClick Reset ] [ text "reset" ]
|
|
108
|
-
]
|
|
109
|
-
, code [ class "counter-code" ] [ text "Browser.element island using standard elm/html" ]
|
|
110
|
-
]
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Islands.Observer exposing (embed, main)
|
|
2
|
-
|
|
3
|
-
import Browser
|
|
4
|
-
import ElmSsr.Html as SsrHtml exposing (Node)
|
|
5
|
-
import ElmSsr.Html.Attributes as SsrAttributes
|
|
6
|
-
import ElmSsr.Island as Island
|
|
7
|
-
import ElmSsr.Island.Shared as Shared
|
|
8
|
-
import Html exposing (Html, div, span, text)
|
|
9
|
-
import Html.Attributes exposing (class)
|
|
10
|
-
import Json.Decode as Decode
|
|
11
|
-
import Json.Encode as Encode
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type alias Flags =
|
|
15
|
-
{}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type alias Model =
|
|
19
|
-
{ lastCount : Int
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type Msg
|
|
24
|
-
= OnGlobalEvent Shared.GlobalEvent
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
embed : Flags -> Node msg
|
|
28
|
-
embed =
|
|
29
|
-
Island.embed "Observer"
|
|
30
|
-
{ encodeFlags = \_ -> Encode.null
|
|
31
|
-
, fallback = \_ -> [ SsrHtml.text "Waiting for updates..." ]
|
|
32
|
-
, id = Just "global-observer"
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
main : Program Flags Model Msg
|
|
37
|
-
main =
|
|
38
|
-
Browser.element
|
|
39
|
-
{ init = \_ -> ( { lastCount = 0 }, Cmd.none )
|
|
40
|
-
, update = update
|
|
41
|
-
, view = view
|
|
42
|
-
, subscriptions = \_ -> Shared.listen OnGlobalEvent
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
update : Msg -> Model -> ( Model, Cmd Msg )
|
|
47
|
-
update msg model =
|
|
48
|
-
case msg of
|
|
49
|
-
OnGlobalEvent event ->
|
|
50
|
-
if event.tag == "count-changed" then
|
|
51
|
-
case Decode.decodeValue Decode.int event.payload of
|
|
52
|
-
Ok count ->
|
|
53
|
-
( { model | lastCount = count }, Cmd.none )
|
|
54
|
-
|
|
55
|
-
Err _ ->
|
|
56
|
-
( model, Cmd.none )
|
|
57
|
-
|
|
58
|
-
else
|
|
59
|
-
( model, Cmd.none )
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
view : Model -> Html Msg
|
|
63
|
-
view model =
|
|
64
|
-
div [ class "observer-island" ]
|
|
65
|
-
[ span [ class "eyebrow" ] [ text "Observer Island" ]
|
|
66
|
-
, div [] [ text ("Last count from bus: " ++ String.fromInt model.lastCount) ]
|
|
67
|
-
]
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Islands.Tasks exposing
|
|
2
|
-
( embed
|
|
3
|
-
, Flags, Model, Msg
|
|
4
|
-
, encodeFlags
|
|
5
|
-
, init, main, subscriptions, update, view
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
import Browser
|
|
9
|
-
import ElmSsr.Html as SsrHtml exposing (Node)
|
|
10
|
-
import ElmSsr.Html.Attributes as SsrAttributes
|
|
11
|
-
import ElmSsr.Island as Island
|
|
12
|
-
import Html exposing (Html, button, div, input, li, span, text, ul)
|
|
13
|
-
import Html.Attributes exposing (class, placeholder, type_, value)
|
|
14
|
-
import Html.Events exposing (onClick, onInput)
|
|
15
|
-
import Html.Keyed as Keyed
|
|
16
|
-
import Json.Encode as Encode
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
type alias Flags =
|
|
20
|
-
{ items : List String
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type alias Item =
|
|
25
|
-
{ id : Int
|
|
26
|
-
, label : String
|
|
27
|
-
, note : String
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type alias Model =
|
|
32
|
-
{ items : List Item
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
type Msg
|
|
37
|
-
= Remove Int
|
|
38
|
-
| MoveUp Int
|
|
39
|
-
| UpdateNote Int String
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
embed : Flags -> Node msg
|
|
43
|
-
embed =
|
|
44
|
-
Island.embed "Tasks"
|
|
45
|
-
{ encodeFlags = encodeFlags
|
|
46
|
-
, fallback = fallback
|
|
47
|
-
, id = Nothing
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
encodeFlags : Flags -> Encode.Value
|
|
52
|
-
encodeFlags flags =
|
|
53
|
-
Encode.object [ ( "items", Encode.list Encode.string flags.items ) ]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
fallback : Flags -> List (Node msg)
|
|
57
|
-
fallback flags =
|
|
58
|
-
[ SsrHtml.div [ SsrAttributes.class "tasks-island fallback" ]
|
|
59
|
-
[ SsrHtml.ul [ SsrAttributes.class "tasks-list" ]
|
|
60
|
-
(List.map
|
|
61
|
-
(\label ->
|
|
62
|
-
SsrHtml.li [ SsrAttributes.class "task-item" ]
|
|
63
|
-
[ SsrHtml.span [ SsrAttributes.class "task-label" ] [ SsrHtml.text label ] ]
|
|
64
|
-
)
|
|
65
|
-
flags.items
|
|
66
|
-
)
|
|
67
|
-
]
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
main : Program Flags Model Msg
|
|
72
|
-
main =
|
|
73
|
-
Browser.element
|
|
74
|
-
{ init = init
|
|
75
|
-
, update = update
|
|
76
|
-
, view = view
|
|
77
|
-
, subscriptions = subscriptions
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
init : Flags -> ( Model, Cmd Msg )
|
|
82
|
-
init flags =
|
|
83
|
-
( { items = List.indexedMap (\index label -> { id = index, label = label, note = "" }) flags.items }
|
|
84
|
-
, Cmd.none
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
update : Msg -> Model -> ( Model, Cmd Msg )
|
|
89
|
-
update msg model =
|
|
90
|
-
case msg of
|
|
91
|
-
Remove id ->
|
|
92
|
-
( { model | items = List.filter (\item -> item.id /= id) model.items }, Cmd.none )
|
|
93
|
-
|
|
94
|
-
MoveUp id ->
|
|
95
|
-
( { model | items = moveUp id model.items }, Cmd.none )
|
|
96
|
-
|
|
97
|
-
UpdateNote id note ->
|
|
98
|
-
( { model | items = List.map (updateNote id note) model.items }, Cmd.none )
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
updateNote : Int -> String -> Item -> Item
|
|
102
|
-
updateNote id note item =
|
|
103
|
-
if item.id == id then
|
|
104
|
-
{ item | note = note }
|
|
105
|
-
|
|
106
|
-
else
|
|
107
|
-
item
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
moveUp : Int -> List Item -> List Item
|
|
111
|
-
moveUp id items =
|
|
112
|
-
case items of
|
|
113
|
-
first :: second :: rest ->
|
|
114
|
-
if second.id == id then
|
|
115
|
-
second :: first :: rest
|
|
116
|
-
|
|
117
|
-
else
|
|
118
|
-
first :: moveUp id (second :: rest)
|
|
119
|
-
|
|
120
|
-
_ ->
|
|
121
|
-
items
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
subscriptions : Model -> Sub Msg
|
|
125
|
-
subscriptions _ =
|
|
126
|
-
Sub.none
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
view : Model -> Html Msg
|
|
130
|
-
view model =
|
|
131
|
-
div [ class "tasks-island" ]
|
|
132
|
-
[ Keyed.ul [ class "tasks-list" ] (List.map viewItem model.items) ]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
viewItem : Item -> ( String, Html Msg )
|
|
136
|
-
viewItem item =
|
|
137
|
-
( String.fromInt item.id
|
|
138
|
-
, li [ class "task-item" ]
|
|
139
|
-
[ span [ class "task-label" ] [ text item.label ]
|
|
140
|
-
, input
|
|
141
|
-
[ class "task-note"
|
|
142
|
-
, type_ "text"
|
|
143
|
-
, placeholder "note (kept across moves)"
|
|
144
|
-
, value item.note
|
|
145
|
-
, onInput (UpdateNote item.id)
|
|
146
|
-
]
|
|
147
|
-
[]
|
|
148
|
-
, button [ class "task-up", type_ "button", onClick (MoveUp item.id) ] [ text "↑" ]
|
|
149
|
-
, button [ class "task-remove", type_ "button", onClick (Remove item.id) ] [ text "remove" ]
|
|
150
|
-
]
|
|
151
|
-
)
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Chart exposing (action, page)
|
|
2
|
-
|
|
3
|
-
-- File-based routing: GET /chart. A fully static page whose chart is inline SVG
|
|
4
|
-
-- produced by ElmSsr.Svg and serialized on the server — no client JavaScript.
|
|
5
|
-
|
|
6
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
7
|
-
import ElmSsr.Document exposing (Document)
|
|
8
|
-
import ElmSsr.Html exposing (Node, h1, p, section, span, text)
|
|
9
|
-
import ElmSsr.Html.Attributes exposing (class)
|
|
10
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
11
|
-
import ElmSsr.Route exposing (Request)
|
|
12
|
-
import ElmSsr.Svg as Svg exposing (Svg)
|
|
13
|
-
import ElmSsr.Svg.Attributes as SvgAttr
|
|
14
|
-
import Example.Basic.View.Shared as Shared
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
page : Request -> Loader (Document Never)
|
|
18
|
-
page _ =
|
|
19
|
-
Loader.succeed view
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
action : Request -> Action (Document Never)
|
|
23
|
-
action _ =
|
|
24
|
-
Action.fail 405 "Method not allowed"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
view : Document Never
|
|
28
|
-
view =
|
|
29
|
-
Shared.pageDocument "SVG Chart"
|
|
30
|
-
[ section [ class "panel" ]
|
|
31
|
-
[ span [ class "eyebrow" ] [ text "Server-rendered SVG" ]
|
|
32
|
-
, h1 [] [ text "Inline SVG, rendered on the edge" ]
|
|
33
|
-
, p [] [ text "This chart is built with ElmSsr.Svg and serialized server-side. It ships zero client JavaScript." ]
|
|
34
|
-
, sparkline
|
|
35
|
-
]
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
sparkline : Svg msg
|
|
40
|
-
sparkline =
|
|
41
|
-
Svg.svg
|
|
42
|
-
[ SvgAttr.viewBox "0 0 200 100"
|
|
43
|
-
, SvgAttr.width "200"
|
|
44
|
-
, SvgAttr.height "100"
|
|
45
|
-
, SvgAttr.class "sparkline"
|
|
46
|
-
]
|
|
47
|
-
[ Svg.defs []
|
|
48
|
-
[ Svg.linearGradient
|
|
49
|
-
[ SvgAttr.id "fill"
|
|
50
|
-
, SvgAttr.x1 "0"
|
|
51
|
-
, SvgAttr.y1 "0"
|
|
52
|
-
, SvgAttr.x2 "0"
|
|
53
|
-
, SvgAttr.y2 "1"
|
|
54
|
-
]
|
|
55
|
-
[ Svg.stop [ SvgAttr.offset "0%", SvgAttr.stopColor "#6366f1" ] []
|
|
56
|
-
, Svg.stop [ SvgAttr.offset "100%", SvgAttr.stopColor "#0ea5e9" ] []
|
|
57
|
-
]
|
|
58
|
-
]
|
|
59
|
-
, Svg.rect
|
|
60
|
-
[ SvgAttr.x "0"
|
|
61
|
-
, SvgAttr.y "0"
|
|
62
|
-
, SvgAttr.width "200"
|
|
63
|
-
, SvgAttr.height "100"
|
|
64
|
-
, SvgAttr.rx "8"
|
|
65
|
-
, SvgAttr.fill "url(#fill)"
|
|
66
|
-
]
|
|
67
|
-
[]
|
|
68
|
-
, Svg.polyline
|
|
69
|
-
[ SvgAttr.points "0,80 40,60 80,70 120,30 160,45 200,10"
|
|
70
|
-
, SvgAttr.fill "none"
|
|
71
|
-
, SvgAttr.stroke "#ffffff"
|
|
72
|
-
, SvgAttr.strokeWidth "3"
|
|
73
|
-
, SvgAttr.strokeLinecap "round"
|
|
74
|
-
, SvgAttr.strokeLinejoin "round"
|
|
75
|
-
]
|
|
76
|
-
[]
|
|
77
|
-
, Svg.g [ SvgAttr.transform "translate(0,0)" ]
|
|
78
|
-
[ Svg.circle [ SvgAttr.cx "120", SvgAttr.cy "30", SvgAttr.r "4", SvgAttr.fill "#ffffff" ] [] ]
|
|
79
|
-
, Svg.text_
|
|
80
|
-
[ SvgAttr.x "8"
|
|
81
|
-
, SvgAttr.y "20"
|
|
82
|
-
, SvgAttr.fontSize "12"
|
|
83
|
-
, SvgAttr.textAnchor "start"
|
|
84
|
-
, SvgAttr.fill "#ffffff"
|
|
85
|
-
]
|
|
86
|
-
[ Svg.text "7-day trend & momentum" ]
|
|
87
|
-
]
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Counter exposing (page, action)
|
|
2
|
-
|
|
3
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
4
|
-
import ElmSsr.Document exposing (Document)
|
|
5
|
-
import ElmSsr.Html exposing (h1, p, section, span, text)
|
|
6
|
-
import ElmSsr.Html.Attributes exposing (class)
|
|
7
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
8
|
-
import ElmSsr.Route exposing (Request)
|
|
9
|
-
import Example.Basic.Islands.Counter as Counter
|
|
10
|
-
import Example.Basic.Islands.Observer as Observer
|
|
11
|
-
import Example.Basic.Islands.Tasks as Tasks
|
|
12
|
-
import Example.Basic.View.Shared as Shared
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
page : Request -> Loader (Document Never)
|
|
16
|
-
page _ =
|
|
17
|
-
Loader.succeed view
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
action : Request -> Action (Document Never)
|
|
21
|
-
action _ =
|
|
22
|
-
Action.fail 405 "Method not allowed"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
view : Document Never
|
|
26
|
-
view =
|
|
27
|
-
Shared.pageDocument "Interactive Counter"
|
|
28
|
-
[ section [ class "panel counter-panel" ]
|
|
29
|
-
[ span [ class "eyebrow" ] [ text "Island" ]
|
|
30
|
-
, h1 [] [ text "Counter route" ]
|
|
31
|
-
, p [] [ text "This page is static. Only the counter below is an interactive island; clicking it patches just its subtree, not the page." ]
|
|
32
|
-
, Counter.embed { start = 0 }
|
|
33
|
-
, Observer.embed {}
|
|
34
|
-
]
|
|
35
|
-
, section [ class "panel tasks-panel" ]
|
|
36
|
-
[ span [ class "eyebrow" ] [ text "Keyed island" ]
|
|
37
|
-
, h1 [] [ text "Keyed list" ]
|
|
38
|
-
, p [] [ text "Reorder or remove rows: each row is keyed, so the surviving DOM nodes (and any note you type) are preserved rather than rebuilt." ]
|
|
39
|
-
, Tasks.embed { items = [ "Write the RFC", "Spike the runtime", "Cover it with tests" ] }
|
|
40
|
-
]
|
|
41
|
-
, Shared.featureSection
|
|
42
|
-
]
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Echo exposing (action, page)
|
|
2
|
-
|
|
3
|
-
-- File-based routing: GET /echo renders a form; POST /echo runs the action.
|
|
4
|
-
-- The action validates, performs a server effect, then redirects (PRG) — so the
|
|
5
|
-
-- form works with no client JavaScript at all.
|
|
6
|
-
|
|
7
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
8
|
-
import ElmSsr.Document exposing (Document)
|
|
9
|
-
import ElmSsr.Html exposing (Node, a, button, form, h1, input, label, p, section, span, text)
|
|
10
|
-
import ElmSsr.Html.Attributes as Attr
|
|
11
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
12
|
-
import ElmSsr.Route as Route exposing (Request)
|
|
13
|
-
import Example.Basic.View.Shared as Shared
|
|
14
|
-
import Json.Decode as Decode
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
page : Request -> Loader (Document Never)
|
|
18
|
-
page request =
|
|
19
|
-
Loader.succeed (view request)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
action : Request -> Action (Document Never)
|
|
23
|
-
action request =
|
|
24
|
-
case Maybe.map String.trim (Route.formValue "message" request) of
|
|
25
|
-
Just message ->
|
|
26
|
-
if String.isEmpty message then
|
|
27
|
-
Action.fail 422 "Message is required."
|
|
28
|
-
|
|
29
|
-
else
|
|
30
|
-
-- Run a server effect (confirm the edge region), then redirect.
|
|
31
|
-
Action.fromLoader confirmRegion
|
|
32
|
-
|> Action.andThen (\region -> Action.redirect ("/echo?status=received®ion=" ++ region))
|
|
33
|
-
|
|
34
|
-
Nothing ->
|
|
35
|
-
Action.fail 422 "Message is required."
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
confirmRegion : Loader String
|
|
39
|
-
confirmRegion =
|
|
40
|
-
Loader.fetchJson
|
|
41
|
-
{ url = "app://status"
|
|
42
|
-
, decoder = Decode.field "region" Decode.string
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
view : Request -> Document Never
|
|
47
|
-
view request =
|
|
48
|
-
Shared.pageDocument "Form Action"
|
|
49
|
-
[ section [ Attr.class "panel" ]
|
|
50
|
-
(case Route.query "status" request of
|
|
51
|
-
Just "received" ->
|
|
52
|
-
[ span [ Attr.class "eyebrow" ] [ text "Action complete" ]
|
|
53
|
-
, h1 [] [ text "Message received" ]
|
|
54
|
-
, p [] [ text ("Saved on the server (region: " ++ (Route.query "region" request |> Maybe.withDefault "unknown") ++ "). This page arrived via a POST → action → redirect, no client JavaScript.") ]
|
|
55
|
-
, p [] [ a [ Attr.class "button-link", Attr.href "/echo" ] [ text "Send another" ] ]
|
|
56
|
-
]
|
|
57
|
-
|
|
58
|
-
_ ->
|
|
59
|
-
[ span [ Attr.class "eyebrow" ] [ text "Forms without JS" ]
|
|
60
|
-
, h1 [] [ text "Server action" ]
|
|
61
|
-
, p [] [ text "Submitting this form POSTs to the route's action, which validates, runs a server effect, then redirects. It works with JavaScript disabled." ]
|
|
62
|
-
, echoForm
|
|
63
|
-
]
|
|
64
|
-
)
|
|
65
|
-
]
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
echoForm : Node msg
|
|
69
|
-
echoForm =
|
|
70
|
-
form [ Attr.class "echo-form", Attr.method "post", Attr.action "/echo" ]
|
|
71
|
-
[ label [ Attr.class "field" ]
|
|
72
|
-
[ span [] [ text "Message" ]
|
|
73
|
-
, input [ Attr.type_ "text", Attr.name "message", Attr.placeholder "Say something", Attr.required True ]
|
|
74
|
-
]
|
|
75
|
-
, button [ Attr.type_ "submit", Attr.class "button-link primary" ] [ text "Send" ]
|
|
76
|
-
]
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Greet.Name_ exposing (page, action)
|
|
2
|
-
|
|
3
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
4
|
-
import ElmSsr.Document exposing (Document)
|
|
5
|
-
import ElmSsr.Html exposing (Node, code, h1, p, section, span, text)
|
|
6
|
-
import ElmSsr.Html.Attributes exposing (class)
|
|
7
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
8
|
-
import ElmSsr.Route as Route exposing (Request)
|
|
9
|
-
import Example.Basic.View.Shared as Shared
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
page : Request -> Loader (Document Never)
|
|
13
|
-
page request =
|
|
14
|
-
Loader.succeed (view (Route.param "name" request |> Maybe.withDefault "stranger"))
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
action : Request -> Action (Document Never)
|
|
18
|
-
action _ =
|
|
19
|
-
Action.fail 405 "Method not allowed"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
view : String -> Document Never
|
|
23
|
-
view name =
|
|
24
|
-
Shared.pageDocument ("Hello " ++ name)
|
|
25
|
-
[ greeting name
|
|
26
|
-
, Shared.featureSection
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
greeting : String -> Node msg
|
|
31
|
-
greeting name =
|
|
32
|
-
section [ class "panel" ]
|
|
33
|
-
[ span [ class "eyebrow" ] [ text "Dynamic route" ]
|
|
34
|
-
, h1 [] [ text ("Hello, " ++ name ++ "!") ]
|
|
35
|
-
, p [] [ text "The name above was captured from the URL by the file Routes/Greet/Name_.elm." ]
|
|
36
|
-
, code [ class "counter-code" ] [ text "Route.param \"name\" request" ]
|
|
37
|
-
]
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Guestbook exposing (action, page)
|
|
2
|
-
|
|
3
|
-
-- File-based routing: GET /guestbook lists entries (Loader.query); POST inserts
|
|
4
|
-
-- one (Action via Loader.execute) then redirects. SQL is backend-neutral — D1 on
|
|
5
|
-
-- Cloudflare, SQLite/Postgres locally — the Elm code is identical.
|
|
6
|
-
|
|
7
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
8
|
-
import ElmSsr.Document exposing (Document)
|
|
9
|
-
import ElmSsr.Html exposing (Node, button, form, h1, input, li, p, section, span, text, ul)
|
|
10
|
-
import ElmSsr.Html.Attributes as Attr
|
|
11
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
12
|
-
import ElmSsr.Route as Route exposing (Request)
|
|
13
|
-
import Example.Basic.View.Shared as Shared
|
|
14
|
-
import Json.Decode as Decode
|
|
15
|
-
import Json.Encode as Encode
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
page : Request -> Loader (Document Never)
|
|
19
|
-
page _ =
|
|
20
|
-
Loader.map view recentEntries
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
recentEntries : Loader (List String)
|
|
24
|
-
recentEntries =
|
|
25
|
-
Loader.query
|
|
26
|
-
{ sql = "SELECT message FROM entries ORDER BY id DESC LIMIT 10"
|
|
27
|
-
, params = []
|
|
28
|
-
, decoder = Decode.field "message" Decode.string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
action : Request -> Action (Document Never)
|
|
33
|
-
action request =
|
|
34
|
-
case Maybe.map String.trim (Route.formValue "message" request) of
|
|
35
|
-
Just message ->
|
|
36
|
-
if String.isEmpty message then
|
|
37
|
-
Action.fail 422 "Message is required."
|
|
38
|
-
|
|
39
|
-
else
|
|
40
|
-
Action.fromLoader (saveAndAudit message)
|
|
41
|
-
|> Action.andThen (\_ -> Action.redirect "/guestbook")
|
|
42
|
-
|
|
43
|
-
Nothing ->
|
|
44
|
-
Action.fail 422 "Message is required."
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
saveAndAudit : String -> Loader ()
|
|
48
|
-
saveAndAudit message =
|
|
49
|
-
Loader.execute
|
|
50
|
-
{ sql = "INSERT INTO entries (message) VALUES (?)"
|
|
51
|
-
, params = [ Encode.string message ]
|
|
52
|
-
}
|
|
53
|
-
|> Loader.andThen
|
|
54
|
-
(\_ ->
|
|
55
|
-
-- Fire-and-forget: the audit runs after the response is sent.
|
|
56
|
-
Loader.enqueue
|
|
57
|
-
{ task = "auditEntry"
|
|
58
|
-
, payload = Encode.object [ ( "message", Encode.string message ) ]
|
|
59
|
-
}
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
view : List String -> Document Never
|
|
64
|
-
view entries =
|
|
65
|
-
Shared.pageDocument "Guestbook"
|
|
66
|
-
[ section [ Attr.class "panel" ]
|
|
67
|
-
[ span [ Attr.class "eyebrow" ] [ text "SQL via the effect runner" ]
|
|
68
|
-
, h1 [] [ text "Guestbook" ]
|
|
69
|
-
, p [] [ text "Entries are read with Loader.query; the form inserts with Loader.execute inside an Action. The SQL backend is swappable without touching this code." ]
|
|
70
|
-
, entryForm
|
|
71
|
-
, ul [ Attr.class "list" ] (List.map entryItem entries)
|
|
72
|
-
]
|
|
73
|
-
]
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
entryForm : Node msg
|
|
77
|
-
entryForm =
|
|
78
|
-
form [ Attr.class "echo-form", Attr.method "post", Attr.action "/guestbook" ]
|
|
79
|
-
[ input [ Attr.type_ "text", Attr.name "message", Attr.placeholder "Sign the guestbook", Attr.required True ]
|
|
80
|
-
, button [ Attr.type_ "submit", Attr.class "button-link primary" ] [ text "Sign" ]
|
|
81
|
-
]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
entryItem : String -> Node msg
|
|
85
|
-
entryItem message =
|
|
86
|
-
li [] [ text message ]
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
module Example.Basic.Routes.Index exposing (page, action)
|
|
2
|
-
|
|
3
|
-
import ElmSsr.Action as Action exposing (Action)
|
|
4
|
-
import ElmSsr.Document exposing (Document)
|
|
5
|
-
import ElmSsr.Html exposing (Node, a, div, h1, p, section, span, text)
|
|
6
|
-
import ElmSsr.Html.Attributes exposing (class, href)
|
|
7
|
-
import ElmSsr.Loader as Loader exposing (Loader)
|
|
8
|
-
import ElmSsr.Route exposing (Request)
|
|
9
|
-
import Example.Basic.View.Shared as Shared
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
page : Request -> Loader (Document Never)
|
|
13
|
-
page _ =
|
|
14
|
-
Loader.succeed view
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
action : Request -> Action (Document Never)
|
|
18
|
-
action _ =
|
|
19
|
-
Action.fail 405 "Method not allowed"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
view : Document Never
|
|
23
|
-
view =
|
|
24
|
-
Shared.pageDocument "Elm SSR Library"
|
|
25
|
-
[ heroSection
|
|
26
|
-
, Shared.featureSection
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
heroSection : Node msg
|
|
31
|
-
heroSection =
|
|
32
|
-
section [ class "hero panel" ]
|
|
33
|
-
[ span [ class "eyebrow" ] [ text "Pages, loaders, Browser.element islands" ]
|
|
34
|
-
, h1 [] [ text "Write Elm once, render on the edge, update in the browser." ]
|
|
35
|
-
, p []
|
|
36
|
-
[ text "Stateless pages load data and render once with zero client JavaScript. Interactive UI lives in standard Browser.element islands mounted inside the page." ]
|
|
37
|
-
, div [ class "hero-actions" ]
|
|
38
|
-
[ a [ class "button-link primary", href "/counter" ] [ text "Open island demo" ]
|
|
39
|
-
, a [ class "button-link", href "/status" ] [ text "See a loader page" ]
|
|
40
|
-
]
|
|
41
|
-
]
|