Skip to content

Commit

Permalink
✨ Add a 'Cmd' abstraction for performing side effects and bringing di…
Browse files Browse the repository at this point in the history
…spatching actions based on their result.
  • Loading branch information
hayleigh-dot-dev committed May 21, 2022
1 parent 46e8b7a commit 815090a
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 11 deletions.
27 changes: 23 additions & 4 deletions src/ffi.mjs
Original file line number Diff line number Diff line change
@@ -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'

// -----------------------------------------------------------------------------

Expand All @@ -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)
}

// -----------------------------------------------------------------------------
Expand All @@ -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.
Expand Down
19 changes: 12 additions & 7 deletions src/lustre.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

// IMPORTS ---------------------------------------------------------------------

import lustre/cmd
import lustre/element
import lustre/attribute
import gleam/result
Expand All @@ -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)
Expand All @@ -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)


Expand All @@ -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)
Expand All @@ -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)
}

Expand All @@ -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"
54 changes: 54 additions & 0 deletions src/lustre/cmd.gleam
Original file line number Diff line number Diff line change
@@ -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 ->
[]
}
}

0 comments on commit 815090a

Please sign in to comment.