Current section

Files

Jump to
lustre src ffi.mjs
Raw

src/ffi.mjs

import * as React from 'react'
import * as ReactDOM from 'react-dom'
import * as Gleam from './gleam.mjs'
import * as Cmd from './lustre/cmd.mjs'
// -----------------------------------------------------------------------------
export const mount = ({ init, update, render }, selector) => {
const root = document.querySelector(selector)
if (!root) {
console.warn([
'[lustre] Oops, it looks like I couldn\'t find an element on the ',
'page matching the selector "' + selector + '".',
'',
'Hint: make sure you aren\'t running your script before the rest of ',
'the HTML document has been parsed! you can add the `defer` attribute ',
'to your script tag to make sure that can\'t happen.'
].join('\n'))
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, 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)
})
ReactDOM.render(App, root)
return new Gleam.Ok(dispatch)
}
// -----------------------------------------------------------------------------
export const node = (tag, attributes, children) => (dispatch) => {
const props = attributes.toArray().map(attr => {
switch (attr.constructor.name) {
case "Attribute":
case "Property":
return [attr.name, attr.value]
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.
default: {
console.warn([
'[lustre] Oops, I\'m not sure how to handle attributes with ',
'the type "' + attr.constructor.name + '". Did you try calling ',
'this function from JavaScript by mistake?',
'',
'If not, it might be an error in lustre itself. Please open ',
'an issue at https://github.com/hayleigh-dot-dev/gleam-lustre/issues'
].join('\n'))
return []
}
}
})
return React.createElement(tag,
// React expects an object of "props" where the keys are attribute names
// like "class" or "onClick" and their corresponding values. Lustre's API
// works with lists, though.
//
// The snippet above converts our Gleam list of attributes to a JavaScript
// array of key/value pairs. This below turns that array into an object.
Object.fromEntries(props),
// Recursively pass down the dispatch function to all children. Text nodes
// – constructed below – aren't functions
...children.toArray().map(child => typeof child === 'function'
? child(dispatch)
: child
)
)
}
export const stateful = (init, render) => (dispatch) => {
const [state, setState] = React.useState(init)
return render(state, setState)(dispatch)
}
export const fragment = (children) => (dispatch) => {
return React.createElement(React.Fragment, null,
...children.toArray().map(child => typeof child === 'function'
? child(dispatch)
: child
)
)
}
// This is just an identity function! We need it because we want to trick Gleam
// into converting a `String` into an `Element(action)` .
export const text = (content) => content
// -----------------------------------------------------------------------------
export const map = (element, f) => (dispatch) => {
return element(action => dispatch(f(action)))
}
// -----------------------------------------------------------------------------
export const object = (entries) => Object.fromEntries(entries)
const capitalise = s => s && s[0].toUpperCase() + s.slice(1)