Packages
lustre
2.0.1
5.7.1
5.7.0
5.6.0
5.5.2
5.5.1
5.5.0
5.4.0
5.3.5
5.3.4
5.3.3
5.3.2
5.3.1
5.3.0
5.2.1
5.2.0
5.1.1
5.1.0
5.0.3
5.0.2
5.0.1
5.0.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.0
4.0.0-rc1
4.0.0-rc.2
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.12
3.0.11
3.0.10
3.0.9
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.5
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
2.0.1
2.0.0
1.3.0
1.2.0
1.1.0
1.0.0
Create HTML templates, single page applications, Web Components, and real-time server components in Gleam!
Current section
Files
Jump to
Current section
Files
src/ffi.mjs
import * as Cmd from "./lustre/cmd.mjs";
import * as Gleam from "./gleam.mjs";
import * as React from "react";
import * as ReactDOM from "react-dom";
// -----------------------------------------------------------------------------
export const mount = ({ init, update, render }, selector) => {
const el = document.querySelector(selector);
if (!el) {
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. There's no
// chance of accidentally returning `null` and our Gleam code consuming it as
// if it was a function.
//
let dispatch = null;
let init_cmds = null;
const App = React.createElement(() => {
const [state, $dispatch] = React.useReducer(
(state, action) => {
let [new_state, cmds] = update(state, action)
// Handle cmds immediately, they're not React-kind-of-state
for (const cmd of Cmd.to_list(cmds)) {
cmd(dispatch);
}
return new_state
},
undefined,
() => {
let [state, cmds] = init
// postpone handling cmds, as we do not have the dispatch, yet
init_cmds = cmds
return state
}
);
if (dispatch === null) dispatch = $dispatch;
const el = render(state);
React.useEffect(() => {
for (const cmd of Cmd.to_list(init_cmds)) {
cmd($dispatch);
}
init_cmds = undefined; // Just so we get an error, rather than re-execute
}, []); // empty deps array means this effect will only run on initial render
return typeof el == "string" ? el : el($dispatch);
});
ReactDOM.createRoot(el).render(App);
return new Gleam.Ok(dispatch);
};
// -----------------------------------------------------------------------------
export const node = (tag, attributes, children) => (dispatch) => {
return React.createElement(
tag,
toProps(attributes, dispatch),
// 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)));
};
// 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 below converts our Gleam list of attributes to a a plain ol'
// object. Duplicate attributes are replaced with the last one in the list,
// except for string attributes which are concatenated together.
//
export const toProps = (attributes, dispatch) => {
const capitalise = (s) => s && s[0].toUpperCase() + s.slice(1);
return attributes.toArray().reduce((props, attr) => {
// The constructors for the `Attribute` type are not public in the
// gleam source to prevent users from constructing them directly.
// This has the unfortunate side effect of not letting us `instanceof`
// the constructors to pattern match on them and instead we have to
// rely on the structure to work out what kind of attribute it is.
//
if ("name" in attr && "value" in attr) {
const prop =
attr.name in props && typeof props[attr.name] === "string"
? props[attr.name] + " " + attr.value
: attr.value;
return { ...props, [attr.name]: prop };
}
// This case handles `Event` variants.
else if ("name" in attr && "handler" in attr) {
const name = "on" + capitalise(attr.name);
const handler = (e) => attr.handler(e, dispatch);
return { ...props, [name]: handler };
}
// 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.
//
else {
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 props;
}
}, {});
};
// -----------------------------------------------------------------------------
export const object = (entries) => Object.fromEntries(entries);