diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2022-05-21 03:51:28 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2022-05-21 03:51:28 +0100 |
commit | 815090ada742b97a918963d90fc347914147342f (patch) | |
tree | 8daf408a652f21c159544955f41386acdbae2e62 | |
parent | 46e8b7a469f90bcda8cc26046babda4aa8657cc4 (diff) | |
download | lustre-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.mjs | 27 | ||||
-rw-r--r-- | src/lustre.gleam | 19 | ||||
-rw-r--r-- | src/lustre/cmd.gleam | 54 |
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 -> + [] + } +} |