aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2022-05-21 03:51:28 +0100
committerHayleigh Thompson <me@hayleigh.dev>2022-05-21 03:51:28 +0100
commit815090ada742b97a918963d90fc347914147342f (patch)
tree8daf408a652f21c159544955f41386acdbae2e62
parent46e8b7a469f90bcda8cc26046babda4aa8657cc4 (diff)
downloadlustre-815090ada742b97a918963d90fc347914147342f.tar.gz
lustre-815090ada742b97a918963d90fc347914147342f.zip
:sparkles: Add a 'Cmd' abstraction for performing side effects and bringing dispatching actions based on their result.
-rw-r--r--src/ffi.mjs27
-rw-r--r--src/lustre.gleam19
-rw-r--r--src/lustre/cmd.gleam54
3 files changed, 89 insertions, 11 deletions
diff --git a/src/ffi.mjs b/src/ffi.mjs
index 89642c0..c2a068d 100644
--- a/src/ffi.mjs
+++ b/src/ffi.mjs
@@ -1,6 +1,8 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom'
+
import * as Gleam from './gleam.mjs'
+import * as Cmd from './lustre/cmd.mjs'
// -----------------------------------------------------------------------------
@@ -20,19 +22,37 @@ export const mount = ({ init, update, render }, selector) => {
return new Gleam.Error()
}
+ // OK this looks sus, what's going on here? We want to be able to return the
+ // dispatch function given to us from `useReducer` so that the rest of our
+ // Gleam code *outside* the application and dispatch commands.
+ //
+ // That's great but because `useReducer` is part of the callback passed to
+ // `createElement` we can't just save it to a variable and return it. We
+ // immediately render the app, so this all happens synchronously, so there's
+ // no chance of accidentally returning `null` and our Gleam code consuming it
+ // as if it was a function.
+ let dispatch = null
+
const App = React.createElement(() => {
- const [state, dispatch] = React.useReducer(update, init)
+ const [[state, cmds], $dispatch] = React.useReducer(([state, _], action) => update(state, action), init)
const el = render(state)
+ if (dispatch === null) dispatch = $dispatch
+
+ React.useEffect(() => {
+ for (const cmd of Cmd.to_list(cmds)) {
+ cmd($dispatch)
+ }
+ })
return typeof el == 'string'
? el
- : el(dispatch)
+ : el($dispatch)
})
ReactDOM.render(App, root)
- return new Gleam.Ok()
+ return new Gleam.Ok(dispatch)
}
// -----------------------------------------------------------------------------
@@ -47,7 +67,6 @@ export const node = (tag, attributes, children) => (dispatch) => {
case "Event":
return ['on' + capitalise(attr.name), (e) => attr.handler(e, dispatch)]
-
// This should Never Happen™️ but if it does we don't want everything
// to explode, so we'll print a friendly error, ignore the attribute
// and carry on as normal.
diff --git a/src/lustre.gleam b/src/lustre.gleam
index cfcf3da..d5fc1b1 100644
--- a/src/lustre.gleam
+++ b/src/lustre.gleam
@@ -3,6 +3,7 @@
// IMPORTS ---------------------------------------------------------------------
+import lustre/cmd
import lustre/element
import lustre/attribute
import gleam/result
@@ -13,12 +14,13 @@ import gleam/result
///
pub opaque type Program(state, action) {
Program(
- init: state,
+ init: #(state, Cmd(action)),
update: Update(state, action),
render: Render(state, action)
)
}
+pub type Cmd(action) = cmd.Cmd(action)
pub type Element(action) = element.Element(action)
pub type Attribute(action) = attribute.Attribute(action)
@@ -32,7 +34,7 @@ pub type Error {
// Gleam automatically expands type aliases so this is purely for the benefit of
// those reading the source.
//
-type Update(state, action) = fn (state, action) -> state
+type Update(state, action) = fn (state, action) -> #(state, Cmd(action))
type Render(state, action) = fn (state) -> Element(action)
@@ -47,8 +49,8 @@ type Render(state, action) = fn (state) -> Element(action)
/// around, you might want to consider using `application` instead.
///
pub fn basic (element: Element(any)) -> Program(Nil, any) {
- let init = Nil
- let update = fn (_, _) { Nil }
+ let init = #(Nil, cmd.none())
+ let update = fn (_, _) { #(Nil, cmd.none()) }
let render = fn (_) { element }
Program(init, update, render)
@@ -62,7 +64,7 @@ pub fn basic (element: Element(any)) -> Program(Nil, any) {
/// used to emit actions that trigger your `update` function to be called and
/// trigger a rerender.
///
-pub fn application (init: state, update: Update(state, action), render: Render(state, action)) -> Program(state, action) {
+pub fn application (init: #(state, Cmd(action)), update: Update(state, action), render: Render(state, action)) -> Program(state, action) {
Program(init, update, render)
}
@@ -73,11 +75,14 @@ pub fn application (init: state, update: Update(state, action), render: Render(s
/// need to actually start it! This function will mount your program to the DOM
/// node that matches the query selector you provide.
///
-pub fn start (program: Program(state, action), selector: String) -> Result(Nil, Error) {
+/// If everything mounted OK, we'll get back a dispatch function that you can
+/// call to send actions to your program and trigger an update.
+///
+pub fn start (program: Program(state, action), selector: String) -> Result(fn (action) -> Nil, Error) {
mount(program, selector)
|> result.replace_error(ElementNotFound)
}
-external fn mount (program: Program(state, action), selector: String) -> Result(Nil, Nil)
+external fn mount (program: Program(state, action), selector: String) -> Result(fn (action) -> Nil, Nil)
= "./ffi.mjs" "mount"
diff --git a/src/lustre/cmd.gleam b/src/lustre/cmd.gleam
new file mode 100644
index 0000000..445c3eb
--- /dev/null
+++ b/src/lustre/cmd.gleam
@@ -0,0 +1,54 @@
+// IMPORTS ---------------------------------------------------------------------
+
+import gleam/list
+
+// TYPES -----------------------------------------------------------------------
+
+pub opaque type Cmd(action) {
+ Cmd(fn (fn (action) -> Nil) -> Nil, Cmd(action))
+ None
+}
+
+// CONSTRUCTORS ----------------------------------------------------------------
+
+pub fn from (cmd: fn (fn (action) -> Nil) -> Nil) -> Cmd(action) {
+ Cmd(cmd, None)
+}
+
+pub fn none () -> Cmd(action) {
+ None
+}
+
+// MANIPULATIONS ---------------------------------------------------------------
+
+pub fn batch (cmds: List(Cmd(action))) -> Cmd(action) {
+ cmds
+ |> list.flat_map(to_list)
+ |> list.fold_right(None, fn (rest, cmd) { Cmd(cmd, rest) })
+}
+
+pub fn map (cmd: Cmd(a), f: fn (a) -> b) -> Cmd(b) {
+ case cmd {
+ Cmd(cmd, next) ->
+ Cmd(fn (dispatch) {
+ cmd(fn (a) {
+ dispatch(f(a))
+ })
+ }, map(next, f))
+
+ None ->
+ None
+ }
+}
+
+// CONVERSIONS -----------------------------------------------------------------
+
+pub fn to_list (cmd: Cmd(action)) -> List(fn (fn (action) -> Nil) -> Nil) {
+ case cmd {
+ Cmd(cmd, next) ->
+ [ cmd, ..to_list(next) ]
+
+ None ->
+ []
+ }
+}