From ad02bc801e28b21f052e8a48f744c900f2ccc975 Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Sat, 14 May 2022 06:44:44 +0100 Subject: :truck: Move FFI code to project root. --- src/lustre.gleam | 23 ++++++++-- src/lustre.mjs | 111 +++++++++++++++++++++++++++++++++++++++++++++++ src/lustre/element.gleam | 10 ++--- src/lustre/event.gleam | 2 +- src/lustre/ffi.mjs | 108 --------------------------------------------- 5 files changed, 137 insertions(+), 117 deletions(-) create mode 100644 src/lustre.mjs delete mode 100644 src/lustre/ffi.mjs diff --git a/src/lustre.gleam b/src/lustre.gleam index da0730b..adba034 100644 --- a/src/lustre.gleam +++ b/src/lustre.gleam @@ -5,6 +5,7 @@ import lustre/element import lustre/attribute +import gleam/result // TYPES ----------------------------------------------------------------------- @@ -22,6 +23,9 @@ pub opaque type Program(state, action) { pub type Element(action) = element.Element(action) pub type Attribute(action) = attribute.Attribute(action) +pub type Error { + ElementNotFound +} // These types aren't exposed, but they're just here to try and shrink the type // annotations for `Program` and `program` a little bit. When generating docs, @@ -39,7 +43,8 @@ type Render(state, action) = fn (state) -> Element(action) /// you can still create components with local state. /// /// Basic lustre programs don't have any *global* application state and so the -/// plumbing is a lot simpler. If you find yourself passing state +/// plumbing is a lot simpler. If you find yourself passing lot's state of state +/// around, you might want to consider using `application` instead. /// pub fn basic (element: Element(any)) -> Program(Nil, any) { let init = Nil @@ -53,7 +58,10 @@ pub fn basic (element: Element(any)) -> Program(Nil, any) { /// start with some initial `state`, a function to update that state, and then /// a render function to derive our program's view from that state. /// -/// +/// Events produced by elements are passed a `dispatch` function that can be +/// 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) { Program(init, update, render) } @@ -61,6 +69,15 @@ pub fn application (init: state, update: Update(state, action), render: Render(s // EFFECTS --------------------------------------------------------------------- +/// Once you have created a program with either `basic` or `application`, you +/// need to actually start it! This function will mount your program to the DOM +/// node that matches the query selector you provide. /// -pub external fn start (program: Program(state, action), selector: String) -> Nil +pub fn start (program: Program(state, action), selector: String) -> Result(Nil, Error) { + mount(program, selector) + |> result.replace_error(ElementNotFound) +} + + +external fn mount (program: Program(state, action), selector: String) -> Result(Nil, Nil) = "./lustre/ffi.mjs" "mount" diff --git a/src/lustre.mjs b/src/lustre.mjs new file mode 100644 index 0000000..2352f01 --- /dev/null +++ b/src/lustre.mjs @@ -0,0 +1,111 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import * as Gleam from './gleam.mjs' + +// ----------------------------------------------------------------------------- + +export const mount = ({ init, update, view }, 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() + } + + const App = React.createElement(() => { + const [state, dispatch] = React.useReducer(update, init) + + return view(state)(dispatch) + }) + + ReactDOM.render(App, root) + + return new Gleam.Ok() +} + +// ----------------------------------------------------------------------------- + +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))) +} + + +// ----------------------------------------------------------------------------- + +const capitalise = s => s && s[0].toUpperCase() + s.slice(1) diff --git a/src/lustre/element.gleam b/src/lustre/element.gleam index e5bb180..48784ee 100644 --- a/src/lustre/element.gleam +++ b/src/lustre/element.gleam @@ -16,7 +16,7 @@ pub external type Element(action) /// child elements. /// pub external fn node (tag: String, attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) - = "./ffi.mjs" "node" + = "../lustre.mjs" "node" /// A stateful element is exactly what it sounds like: some element with local /// encapsulated state! The `render` function we must provide is called with the @@ -27,18 +27,18 @@ pub external fn node (tag: String, attributes: List(Attribute(action)), children /// Those are just regular Gleam functions that return `Element`s! /// pub external fn stateful (init: state, render: fn (state, fn (state) -> Nil) -> Element(action)) -> Element(action) - = "./ffi.mjs" "stateful" + = "../lustre.mjs" "stateful" /// A fragment doesn't appear in the DOM, but allows us to treat a list of elements /// as if it were a single one. /// pub external fn fragment (children: List(Element(action))) -> Element(action) - = "./ffi.mjs" "fragment" + = "../lustre.mjs" "fragment" /// Render a Gleam string as an HTML text node. /// pub external fn text (content: String) -> Element(action) - = "./ffi.mjs" "text" + = "../lustre.mjs" "text" // MANIPULATIONS --------------------------------------------------------------- @@ -46,7 +46,7 @@ pub external fn text (content: String) -> Element(action) /// Transforms the actions produced by some element. /// pub external fn map (element: Element(a), f: fn (a) -> b) -> Element(b) - = "./ffi.mjs" "map" + = "../lustre.mjs" "map" // CONSTRUCTING NODES ---------------------------------------------------------- diff --git a/src/lustre/event.gleam b/src/lustre/event.gleam index 75c540b..088dfd0 100644 --- a/src/lustre/event.gleam +++ b/src/lustre/event.gleam @@ -2,7 +2,7 @@ import gleam/dynamic.{ Dynamic } import lustre/attribute.{ Attribute } pub external fn ignore () -> action - = "./ffi.mjs" "ignore" + = "../lustre.mjs" "ignore" pub fn on (name: String, handler: fn (Dynamic, fn (action) -> Nil) -> Nil) -> Attribute(action) { diff --git a/src/lustre/ffi.mjs b/src/lustre/ffi.mjs deleted file mode 100644 index 594b71f..0000000 --- a/src/lustre/ffi.mjs +++ /dev/null @@ -1,108 +0,0 @@ -import * as React from 'react' -import * as ReactDOM from 'react-dom' - -// ----------------------------------------------------------------------------- - -export const mount = ({ init, update, view }, 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 - } - - const App = React.createElement(() => { - const [state, dispatch] = React.useReducer(update, init) - - return view(state)(dispatch) - }) - - ReactDOM.render(App, root) -} - -// ----------------------------------------------------------------------------- - -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))) -} - - -// ----------------------------------------------------------------------------- - -const capitalise = s => s && s[0].toUpperCase() + s.slice(1) -- cgit v1.2.3