aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2023-02-13 13:12:17 +0000
committerHayleigh Thompson <me@hayleigh.dev>2023-02-13 13:12:17 +0000
commit2a2ecfc53b9fbd71e068ab92b669de67ad6c9408 (patch)
tree00b4a1c87cc103411ef60385d394053bec5f9442
parent8fdc595ebbf7d53793f1ae8641ae717ab09da23d (diff)
downloadlustre-2a2ecfc53b9fbd71e068ab92b669de67ad6c9408.tar.gz
lustre-2a2ecfc53b9fbd71e068ab92b669de67ad6c9408.zip
:sparkles: Merge string attributes together.
-rw-r--r--src/ffi.mjs243
1 files changed, 128 insertions, 115 deletions
diff --git a/src/ffi.mjs b/src/ffi.mjs
index 73879d6..cb458d4 100644
--- a/src/ffi.mjs
+++ b/src/ffi.mjs
@@ -1,144 +1,157 @@
-import * as React from 'react'
-import * as ReactDOM from 'react-dom'
-
-import * as Gleam from './gleam.mjs'
-import * as Cmd from './lustre/cmd.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 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)
-}
+ 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;
+
+ 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);
+ }
+ }, [cmds]);
+
+ 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
- )
- )
-}
+ 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)
+ const [state, setState] = React.useState(init);
- return render(state, setState)(dispatch)
-}
+ 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
- )
- )
-}
+ 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 text = (content) => content;
// -----------------------------------------------------------------------------
export const map = (element, f) => (dispatch) => {
- return element(action => dispatch(f(action)))
-}
+ 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 JavaScript
-// array of key/value pairs, and then a plain ol' object.
+// 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 Object.fromEntries(
- attributes.toArray().map(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.
- //
- // This case handles `Attribute` and `Property` variants.
- if ('name' in attr && 'value' in attr) {
- return [attr.name, attr.value]
- }
-
- // This case handles `Event` variants.
- else if ('name' in attr && 'handler' in attr) {
- 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.
- 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 []
- }
- })
- )
-}
+ 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) \ No newline at end of file
+export const object = (entries) => Object.fromEntries(entries);