From 9919bc2702c89168d1805eaa0db9e4baff091260 Mon Sep 17 00:00:00 2001 From: Hayleigh Thompson Date: Sat, 19 Aug 2023 22:21:19 +0100 Subject: :truck: Shift things around to accomodate a monorepo. --- src/lustre.ffi.mjs | 206 ------- src/lustre.gleam | 254 --------- src/lustre/attribute.gleam | 408 -------------- src/lustre/effect.gleam | 67 --- src/lustre/element.gleam | 126 ----- src/lustre/element/html.gleam | 1197 ----------------------------------------- src/lustre/element/svg.gleam | 351 ------------ src/lustre/event.gleam | 184 ------- src/runtime.ffi.mjs | 230 -------- 9 files changed, 3023 deletions(-) delete mode 100644 src/lustre.ffi.mjs delete mode 100644 src/lustre.gleam delete mode 100644 src/lustre/attribute.gleam delete mode 100644 src/lustre/effect.gleam delete mode 100644 src/lustre/element.gleam delete mode 100644 src/lustre/element/html.gleam delete mode 100644 src/lustre/element/svg.gleam delete mode 100644 src/lustre/event.gleam delete mode 100644 src/runtime.ffi.mjs (limited to 'src') diff --git a/src/lustre.ffi.mjs b/src/lustre.ffi.mjs deleted file mode 100644 index b99f6e2..0000000 --- a/src/lustre.ffi.mjs +++ /dev/null @@ -1,206 +0,0 @@ -import { ElementNotFound, ComponentAlreadyRegistered } from "./lustre.mjs"; -import { from } from "./lustre/effect.mjs"; -import { map } from "./lustre/element.mjs"; -import { morph } from "./runtime.ffi.mjs"; -import { Ok, Error, isEqual } from "./gleam.mjs"; - -// RUNTIME --------------------------------------------------------------------- - -/// -/// -export class App { - #root = null; - #state = null; - #queue = []; - #effects = []; - #didUpdate = false; - - #init = null; - #update = null; - #view = null; - - constructor(init, update, render) { - this.#init = init; - this.#update = update; - this.#view = render; - } - - start(selector = "body") { - if (this.#root) return this; - - try { - const el = - selector instanceof HTMLElement - ? selector - : document.querySelector(selector); - const [next, effects] = this.#init(); - - this.#root = el; - this.#state = next; - this.#effects = effects[0].toArray(); - this.#didUpdate = true; - - window.requestAnimationFrame(() => this.#tick()); - - return new Ok((msg) => this.dispatch(msg)); - } catch (_) { - return new Error(new ElementNotFound()); - } - } - - dispatch(msg) { - this.#queue.push(msg); - this.#tick(); - } - - emit(name, event = null) { - this.#root.dispatchEvent( - new CustomEvent(name, { - bubbles: true, - detail: event, - composed: true, - }) - ); - } - - destroy() { - this.#root.remove(); - this.#state = null; - this.#queue = []; - this.#effects = []; - this.#didUpdate = false; - this.#update = () => {}; - this.#view = () => {}; - } - - #render() { - const node = this.#view(this.#state); - const vdom = map(node, (msg) => this.dispatch(msg)); - - morph(this.#root, vdom); - } - - #tick() { - this.#flush(); - this.#didUpdate && this.#render(); - this.#didUpdate = false; - } - - #flush(times = 0) { - if (this.#queue.length) { - while (this.#queue.length) { - const [next, effects] = this.#update(this.#state, this.#queue.shift()); - - this.#state = next; - this.#effects = this.#effects.concat(effects[0].toArray()); - } - this.#didUpdate = true; - } - - // Each update can produce effects which must now be executed. - while (this.#effects[0]) - this.#effects.shift()( - (msg) => this.dispatch(msg), - (name, data) => this.emit(name, data) - ); - - // Synchronous effects will immediately queue a message to be processed. If - // it is reasonable, we can process those updates too before proceeding to - // the next render. - if (this.#queue.length) { - times >= 5 ? console.warn(tooManyUpdates) : this.#flush(++times); - } - } -} - -export const setup = (init, update, render) => new App(init, update, render); -export const start = (app, selector) => app.start(selector); - -export const emit = (name, data) => - // Normal `Effect`s constructed in Gleam from `effect.from` don't get told - // about the second argument, but it's there 👀. - from((_, emit) => { - emit(name, data); - }); - -// HTML EVENTS ----------------------------------------------------------------- - -export const prevent_default = (e) => e.preventDefault?.(); -export const stop_propagation = (e) => e.stopPropagation?.(); - -// CUSTOM ELEMENTS ------------------------------------------------------------- - -export const setup_component = ( - name, - init, - update, - render, - on_attribute_change -) => { - if (customElements.get(name)) { - return new Error(new ComponentAlreadyRegistered()); - } - - customElements.define( - name, - class extends HTMLElement { - static get observedAttributes() { - return on_attribute_change.entries().map(([name, _]) => name); - } - - #container = document.createElement("div"); - #app = null; - #dispatch = null; - - constructor() { - super(); - - this.#app = new App(init, update, render); - const dispatch = this.#app.start(this.#container); - this.#dispatch = dispatch[0]; - - on_attribute_change.forEach((decoder, name) => { - Object.defineProperty(this, name, { - get: () => { - return this[`_${name}`] || this.getAttribute(name); - }, - - set: (value) => { - const prev = this[name]; - const decoded = decoder(value); - - // We need this equality check to prevent constantly dispatching - // messages when the value is an object or array: it might not have - // changed but its reference might have and we don't want to trigger - // useless updates. - if (decoded.isOk() && !isEqual(prev, decoded[0])) { - this.#dispatch(decoded[0]); - } - - if (typeof value === "string") { - this.setAttribute(name, value); - } else { - this[`_${name}`] = value; - } - }, - }); - }); - } - - connectedCallback() { - this.appendChild(this.#container); - } - - attributeChangedCallback(name, prev, next) { - if (prev !== next) { - this[name] = next; - } - } - - disconnectedCallback() { - this.#app.destroy(); - } - } - ); - return new Ok(null); -}; diff --git a/src/lustre.gleam b/src/lustre.gleam deleted file mode 100644 index 673f982..0000000 --- a/src/lustre.gleam +++ /dev/null @@ -1,254 +0,0 @@ -//// Lustre is a declarative framework for building Web apps in Gleam. - -// IMPORTS --------------------------------------------------------------------- - -import gleam/dynamic.{Decoder} -import gleam/map.{Map} -import lustre/effect.{Effect} -import lustre/element.{Element} - -// TYPES ----------------------------------------------------------------------- - -/// An `App` describes a Lustre application: what state it holds and what kind -/// of actions get dispatched to update that model. The only useful thing you can -/// do with an `App` is pass it to [`start`](#start). -/// -/// You can construct an `App` from the two constructors exposed in this module: -/// [`basic`](#basic) and [`application`](#application). Although you can't do -/// anything but [`start`](#start) them, the constructors are separated in case -/// you want to set up an application but defer starting it until some later point -/// in time. -/// -/// ```text -/// +--------+ -/// | | -/// | update | -/// | | -/// +--------+ -/// ^ | -/// | | -/// Msg | | #(Model, Effect(Msg)) -/// | | -/// | v -/// +------+ +------------------------+ -/// | | #(Model, Effect(Msg)) | | -/// | init |------------------------>| Lustre Runtime | -/// | | | | -/// +------+ +------------------------+ -/// ^ | -/// | | -/// Msg | | Model -/// | | -/// | v -/// +--------+ -/// | | -/// | render | -/// | | -/// +--------+ -/// ``` -/// -pub type App(model, msg) - -pub type Error { - ElementNotFound - ComponentAlreadyRegistered -} - -// These types aren't exposed, but they're just here to try and shrink the type -// annotations for `App` and `application` a little bit. When generating docs, -// Gleam automatically expands type aliases so this is purely for the benefit of -// those reading the source. -// - -type Update(model, msg) = - fn(model, msg) -> #(model, Effect(msg)) - -type Render(model, msg) = - fn(model) -> Element(msg) - -// CONSTRUCTORS ---------------------------------------------------------------- - -@target(javascript) -/// Create a basic lustre app that just renders some element on the page. -/// Note that this doesn't mean the content is static! With `element.stateful` -/// you can still create components with local state. -/// -/// Basic lustre apps don't have any *global* application state and so the -/// plumbing is a lot simpler. If you find yourself passing lots of state around, -/// you might want to consider using [`simple`](#simple) or [`application`](#application) -/// instead. -/// -/// ```gleam -/// import lustre -/// import lustre/element -/// -/// pub fn main () { -/// let app = lustre.element( -/// element.h1([], [ -/// element.text("Hello, world!") -/// ]) -/// ) -/// -/// assert Ok(_) = lustre.start(app, "#root") -/// } -/// ``` -/// -pub fn element(element: Element(msg)) -> App(Nil, msg) { - let init = fn() { #(Nil, effect.none()) } - let update = fn(_, _) { #(Nil, effect.none()) } - let render = fn(_) { element } - - application(init, update, render) -} - -@target(javascript) -/// If you start off with a simple `[element`](#element) app, you may find -/// yourself leaning on [`stateful`](./lustrel/element.html#stateful) elements -/// to manage model used throughout your app. If that's the case or if you know -/// you need some global model from the get-go, you might want to construct a -/// [`simple`](#simple) app instead. -/// -/// This is one app constructor that allows your HTML elements to dispatch actions -/// to update your program model. -/// -/// ```gleam -/// import gleam/int -/// import lustre -/// import lustre/element -/// import lustre/event -/// -/// type Msg { -/// Decr -/// Incr -/// } -/// -/// pub fn main () { -/// let init = 0 -/// -/// let update = fn (model, msg) { -/// case msg { -/// Decr -> model - 1 -/// Incr -> model + 1 -/// } -/// } -/// -/// let render = fn (model) { -/// element.div([], [ -/// element.button([ event.on_click(Decr) ], [ -/// element.text("-") -/// ]), -/// -/// element.text(int.to_string(model)), -/// -/// element.button([ event.on_click(Incr) ], [ -/// element.text("+") -/// ]) -/// ]) -/// } -/// -/// let app = lustre.simple(init, update, render) -/// assert Ok(_) = lustre.start(app, "#root") -/// } -/// ``` -/// -pub fn simple( - init: fn() -> model, - update: fn(model, msg) -> model, - render: fn(model) -> Element(msg), -) -> App(model, msg) { - let init = fn() { #(init(), effect.none()) } - let update = fn(model, msg) { #(update(model, msg), effect.none()) } - - application(init, update, render) -} - -@target(javascript) -/// An evolution of a [`simple`](#simple) app that allows you to return a -/// [`Effect`](./lustre/effect.html#Effect) from your `init` and `update`s. Commands give -/// us a way to perform side effects like sending an HTTP request or running a -/// timer and then dispatch actions back to the runtime to trigger an `update`. -/// -///``` -/// import lustre -/// import lustre/effect -/// import lustre/element -/// -/// pub fn main () { -/// let init = #(0, tick()) -/// -/// let update = fn (model, msg) { -/// case msg { -/// Tick -> #(model + 1, tick()) -/// } -/// } -/// -/// let render = fn (model) { -/// element.div([], [ -/// element.text("Time elapsed: ") -/// element.text(int.to_string(model)) -/// ]) -/// } -/// -/// let app = lustre.simple(init, update, render) -/// assert Ok(_) = lustre.start(app, "#root") -/// } -/// -/// fn tick () -> Effect(Msg) { -/// effect.from(fn (dispatch) { -/// setInterval(fn () { -/// dispatch(Tick) -/// }, 1000) -/// }) -/// } -/// -/// external fn set_timeout (f: fn () -> a, delay: Int) -> Nil -/// = "" "window.setTimeout" -///``` -@external(javascript, "./lustre.ffi.mjs", "setup") -pub fn application(init: fn() -> #(model, Effect(msg)), update: Update( - model, - msg, - ), render: Render(model, msg)) -> App(model, msg) - -@target(javascript) -@external(javascript, "./lustre.ffi.mjs", "setup_component") -pub fn component(name: String, init: fn() -> #(model, Effect(msg)), update: Update( - model, - msg, - ), render: Render(model, msg), on_attribute_change: Map(String, Decoder(msg))) -> Result( - Nil, - Error, -) - -// EFFECTS --------------------------------------------------------------------- - -@target(javascript) -/// Once you have created a app with either `basic` or `application`, you -/// need to actually start it! This function will mount your app to the DOM -/// node that matches the query selector you provide. -/// -/// If everything mounted OK, we'll get back a dispatch function that you can -/// call to send actions to your app and trigger an update. -/// -///``` -/// import lustre -/// -/// pub fn main () { -/// let app = lustre.appliation(init, update, render) -/// assert Ok(dispatch) = lustre.start(app, "#root") -/// -/// dispatch(Incr) -/// dispatch(Incr) -/// dispatch(Incr) -/// } -///``` -/// -/// This may not seem super useful at first, but by returning this dispatch -/// function from your `main` (or elsewhere) you can get events into your Lustre -/// app from the outside world. -/// -@external(javascript, "./lustre.ffi.mjs", "start") -pub fn start(app: App(model, msg), selector: String) -> Result( - fn(msg) -> Nil, - Error, -) diff --git a/src/lustre/attribute.gleam b/src/lustre/attribute.gleam deleted file mode 100644 index 459a86e..0000000 --- a/src/lustre/attribute.gleam +++ /dev/null @@ -1,408 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/dynamic.{Dynamic} -import gleam/int -import gleam/list -import gleam/option.{Option} -import gleam/string -import gleam/string_builder.{StringBuilder} - -// TYPES ----------------------------------------------------------------------- - -/// Attributes are attached to specific elements. They're either key/value pairs -/// or event handlers. -/// -pub opaque type Attribute(msg) { - Attribute(String, Dynamic) - Event(String, fn(Dynamic) -> Option(msg)) -} - -// CONSTRUCTORS ---------------------------------------------------------------- - -/// -/// Lustre does some work internally to convert common Gleam values into ones that -/// make sense for JavaScript. Here are the types that are converted: -/// -/// - `List(a)` -> `Array(a)` -/// - `Some(a)` -> `a` -/// - `None` -> `undefined` -/// -pub fn attribute(name: String, value: String) -> Attribute(msg) { - escape("", value) - |> dynamic.from - |> Attribute(name, _) -} - -/// -pub fn property(name: String, value: any) -> Attribute(msg) { - Attribute(name, dynamic.from(value)) -} - -fn escape(escaped: String, content: String) -> String { - case string.pop_grapheme(content) { - Ok(#("<", xs)) -> escape(escaped <> "<", xs) - Ok(#(">", xs)) -> escape(escaped <> ">", xs) - Ok(#("&", xs)) -> escape(escaped <> "&", xs) - Ok(#("\"", xs)) -> escape(escaped <> """, xs) - Ok(#("'", xs)) -> escape(escaped <> "'", xs) - Ok(#(x, xs)) -> escape(escaped <> x, xs) - Error(_) -> escaped <> content - } -} - -/// Attach custom event handlers to an element. A number of helper functions exist -/// in this module to cover the most common events and use-cases, so you should -/// check those out first. -/// -/// If you need to handle an event that isn't covered by the helper functions, -/// then you can use `on` to attach a custom event handler. The callback is given -/// the event object as a `Dynamic`. -/// -/// As a simple example, you can implement `on_click` like so: -/// -/// ```gleam -/// import gleam/option.{Some} -/// import lustre/attribute.{Attribute} -/// import lustre/event -/// -/// pub fn on_click(msg: msg) -> Attribute(msg) { -/// use _ <- event.on("click") -/// Some(msg) -/// } -/// ``` -/// -/// By using `gleam/dynamic` you can decode the event object and pull out all sorts -/// of useful data. This is how `on_input` is implemented: -/// -/// ```gleam -/// import gleam/dynamic -/// import gleam/option.{None, Some} -/// import gleam/result -/// import lustre/attribute.{Attribute} -/// import lustre/event -/// -/// pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) { -/// use event, dispatch <- on("input") -/// let decode = dynamic.field("target", dynamic.field("value", dynamic.string)) -/// -/// case decode(event) { -/// Ok(value) -> Some(msg(value)) -/// Error(_) -> None -/// } -/// } -/// ``` -/// -/// You can take a look at the MDN reference for events -/// [here](https://developer.mozilla.org/en-US/docs/Web/API/Event) to see what -/// you can decode. -/// -/// Unlike the helpers in the rest of this module, it is possible to simply ignore -/// the dispatch function and not dispatch a message at all. In fact, we saw this -/// with the `on_input` example above: if we can't decode the event object, we -/// simply return `None` and emit nothing. -/// -/// Beyond ignoring errors, this can be used to perform side effects we don't need -/// to observe in our main application loop, such as logging... -/// -/// ```gleam -/// import gleam/io -/// import gleam/option.{None} -/// import lustre/attribute.{Attribute} -/// import lustre/event -/// -/// pub fn log_on_click(msg: String) -> Attribute(msg) { -/// use _ <- event.on("click") -/// io.println(msg) -/// None -/// } -/// ``` -/// -pub fn on(name: String, handler: fn(Dynamic) -> Option(msg)) -> Attribute(msg) { - Event("on" <> name, handler) -} - -// MANIPULATIONS --------------------------------------------------------------- - -/// -/// -pub fn map(attr: Attribute(a), f: fn(a) -> b) -> Attribute(b) { - case attr { - Attribute(name, value) -> Attribute(name, value) - Event(on, handler) -> Event(on, fn(e) { option.map(handler(e), f) }) - } -} - -// CONVERSIONS ----------------------------------------------------------------- - -/// -/// -pub fn to_string(attr: Attribute(msg)) -> String { - case attr { - Attribute(name, value) -> { - case dynamic.classify(value) { - "String" -> name <> "=\"" <> dynamic.unsafe_coerce(value) <> "\"" - - // Boolean attributes are determined based on their presence, eg we don't - // want to render `disabled="false"` if the value is `false` we simply - // want to omit the attribute altogether. - "Boolean" -> - case dynamic.unsafe_coerce(value) { - True -> name - False -> "" - } - - // For everything else we'll just make a best-effort serialisation. - _ -> name <> "=\"" <> string.inspect(value) <> "\"" - } - } - Event(on, _) -> "data-lustre-on:" <> on - } -} - -/// -/// -pub fn to_string_builder(attr: Attribute(msg)) -> StringBuilder { - case attr { - Attribute(name, value) -> { - case dynamic.classify(value) { - "String" -> - [name, "=\"", dynamic.unsafe_coerce(value), "\""] - |> string_builder.from_strings - - // Boolean attributes are determined based on their presence, eg we don't - // want to render `disabled="false"` if the value is `false` we simply - // want to omit the attribute altogether. - "Boolean" -> - case dynamic.unsafe_coerce(value) { - True -> string_builder.from_string(name) - False -> string_builder.new() - } - - // For everything else we'll just make a best-effort serialisation. - _ -> - [name, "=\"", string.inspect(value), "\""] - |> string_builder.from_strings - } - } - Event(on, _) -> - ["data-lustre-on:", on] - |> string_builder.from_strings - } -} - -// COMMON ATTRIBUTES ----------------------------------------------------------- - -/// -pub fn style(properties: List(#(String, String))) -> Attribute(msg) { - attribute( - "style", - { - use styles, #(name, value) <- list.fold(properties, "") - styles <> name <> ":" <> value <> ";" - }, - ) -} - -/// -pub fn class(name: String) -> Attribute(msg) { - attribute("class", name) -} - -/// -pub fn classes(names: List(#(String, Bool))) -> Attribute(msg) { - attribute( - "class", - names - |> list.filter_map(fn(class) { - case class.1 { - True -> Ok(class.0) - False -> Error(Nil) - } - }) - |> string.join(" "), - ) -} - -/// -pub fn id(name: String) -> Attribute(msg) { - attribute("id", name) -} - -// INPUTS ---------------------------------------------------------------------- - -/// -pub fn type_(name: String) -> Attribute(msg) { - attribute("type", name) -} - -/// -pub fn value(val: Dynamic) -> Attribute(msg) { - property("value", val) -} - -/// -pub fn checked(is_checked: Bool) -> Attribute(msg) { - property("checked", is_checked) -} - -/// -pub fn placeholder(text: String) -> Attribute(msg) { - attribute("placeholder", text) -} - -/// -pub fn selected(is_selected: Bool) -> Attribute(msg) { - property("selected", is_selected) -} - -// INPUT HELPERS --------------------------------------------------------------- - -/// -pub fn accept(types: List(String)) -> Attribute(msg) { - attribute("accept", string.join(types, " ")) -} - -/// -pub fn accept_charset(types: List(String)) -> Attribute(msg) { - attribute("acceptCharset", string.join(types, " ")) -} - -/// -pub fn msg(uri: String) -> Attribute(msg) { - attribute("msg", uri) -} - -/// -pub fn autocomplete(name: String) -> Attribute(msg) { - attribute("autocomplete", name) -} - -/// -pub fn autofocus(should_autofocus: Bool) -> Attribute(msg) { - property("autoFocus", should_autofocus) -} - -/// -pub fn disabled(is_disabled: Bool) -> Attribute(msg) { - property("disabled", is_disabled) -} - -/// -pub fn name(name: String) -> Attribute(msg) { - attribute("name", name) -} - -/// -pub fn pattern(regex: String) -> Attribute(msg) { - attribute("pattern", regex) -} - -/// -pub fn readonly(is_readonly: Bool) -> Attribute(msg) { - property("readonly", is_readonly) -} - -/// -pub fn required(is_required: Bool) -> Attribute(msg) { - property("required", is_required) -} - -/// -pub fn for(id: String) -> Attribute(msg) { - attribute("for", id) -} - -// INPUT RANGES ---------------------------------------------------------------- - -/// -pub fn max(val: String) -> Attribute(msg) { - attribute("max", val) -} - -/// -pub fn min(val: String) -> Attribute(msg) { - attribute("min", val) -} - -/// -pub fn step(val: String) -> Attribute(msg) { - attribute("step", val) -} - -// INPUT TEXT AREAS ------------------------------------------------------------ - -/// -pub fn cols(val: Int) -> Attribute(msg) { - attribute("cols", int.to_string(val)) -} - -/// -pub fn rows(val: Int) -> Attribute(msg) { - attribute("rows", int.to_string(val)) -} - -/// -pub fn wrap(mode: String) -> Attribute(msg) { - attribute("wrap", mode) -} - -// LINKS AND AREAS ------------------------------------------------------------- - -/// -pub fn href(uri: String) -> Attribute(msg) { - attribute("href", uri) -} - -/// -pub fn target(target: String) -> Attribute(msg) { - attribute("target", target) -} - -/// -pub fn download(filename: String) -> Attribute(msg) { - attribute("download", filename) -} - -/// -pub fn rel(relationship: String) -> Attribute(msg) { - attribute("rel", relationship) -} - -// EMBEDDED CONTENT ------------------------------------------------------------ - -/// -pub fn src(uri: String) -> Attribute(msg) { - attribute("src", uri) -} - -/// -pub fn height(val: Int) -> Attribute(msg) { - property("height", int.to_string(val)) -} - -/// -pub fn width(val: Int) -> Attribute(msg) { - property("width", int.to_string(val)) -} - -/// -pub fn alt(text: String) -> Attribute(msg) { - attribute("alt", text) -} - -// AUDIO AND VIDEO ------------------------------------------------------------- - -/// -pub fn autoplay(should_autoplay: Bool) -> Attribute(msg) { - property("autoplay", should_autoplay) -} - -/// -pub fn controls(visible: Bool) -> Attribute(msg) { - property("controls", visible) -} - -/// -pub fn loop(should_loop: Bool) -> Attribute(msg) { - property("loop", should_loop) -} diff --git a/src/lustre/effect.gleam b/src/lustre/effect.gleam deleted file mode 100644 index 19f54b0..0000000 --- a/src/lustre/effect.gleam +++ /dev/null @@ -1,67 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/list - -// TYPES ----------------------------------------------------------------------- - -/// A `Effect` represents some side effect we want the Lustre runtime to perform. -/// It is parameterised by our app's `action` type because some effects need to -/// get information back into your program. -/// -pub opaque type Effect(action) { - Effect(List(fn(fn(action) -> Nil) -> Nil)) -} - -// CONSTRUCTORS ---------------------------------------------------------------- - -/// Create a `Effect` from some custom side effect. This is mostly useful for -/// package authors, or for integrating other libraries into your Lustre app. -/// -/// We pass in a function that recieves a `dispatch` callback that can be used -/// to send messages to the Lustre runtime. We could, for example, create a `tick` -/// command that uses the `setTimeout` JavaScript API to send a message to the -/// runtime every second: -/// -/// ```gleam -/// import lustre/effect.{Effect} -/// -/// external fn set_interval(callback: fn() -> any, interval: Int) = -/// "" "window.setInterval" -/// -/// pub fn every_second(msg: msg) -> Effect(msg) { -/// use dispatch <- effect.from -/// -/// set_interval(fn() { dispatch(msg) }, 1000) -/// } -/// ``` -/// -pub fn from(effect: fn(fn(action) -> Nil) -> Nil) -> Effect(action) { - Effect([effect]) -} - -/// Typically our app's `update` function needs to return a tuple of -/// `#(model, Effect(action))`. When we don't need to perform any side effects we -/// can just return `none()`! -/// -pub fn none() -> Effect(action) { - Effect([]) -} - -// MANIPULATIONS --------------------------------------------------------------- - -/// -/// -pub fn batch(cmds: List(Effect(action))) -> Effect(action) { - Effect({ - use b, Effect(a) <- list.fold(cmds, []) - list.append(b, a) - }) -} - -pub fn map(effect: Effect(a), f: fn(a) -> b) -> Effect(b) { - let Effect(l) = effect - Effect(list.map( - l, - fn(effect) { fn(dispatch) { effect(fn(a) { dispatch(f(a)) }) } }, - )) -} diff --git a/src/lustre/element.gleam b/src/lustre/element.gleam deleted file mode 100644 index 4e8abee..0000000 --- a/src/lustre/element.gleam +++ /dev/null @@ -1,126 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/list -import gleam/string -import gleam/string_builder.{StringBuilder} -import lustre/attribute.{Attribute} - -// TYPES ----------------------------------------------------------------------- - -/// -/// -pub opaque type Element(msg) { - Text(String) - Element(String, List(Attribute(msg)), List(Element(msg))) - ElementNs(String, List(Attribute(msg)), List(Element(msg)), String) -} - -// CONSTRUCTORS ---------------------------------------------------------------- - -/// -/// -pub fn element( - tag: String, - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - Element(tag, attrs, children) -} - -/// -/// -pub fn namespaced( - namespace: String, - tag: String, - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - ElementNs(tag, attrs, children, namespace) -} - -/// -/// -pub fn text(content: String) -> Element(msg) { - Text(content) -} - -fn escape(escaped: String, content: String) -> String { - case string.pop_grapheme(content) { - Ok(#("<", xs)) -> escape(escaped <> "<", xs) - Ok(#(">", xs)) -> escape(escaped <> ">", xs) - Ok(#("&", xs)) -> escape(escaped <> "&", xs) - Ok(#("\"", xs)) -> escape(escaped <> """, xs) - Ok(#("'", xs)) -> escape(escaped <> "'", xs) - Ok(#(x, xs)) -> escape(escaped <> x, xs) - Error(_) -> escaped <> content - } -} - -// MANIPULATIONS --------------------------------------------------------------- - -/// -/// -pub fn map(element: Element(a), f: fn(a) -> b) -> Element(b) { - case element { - Text(content) -> Text(content) - Element(tag, attrs, children) -> - Element( - tag, - list.map(attrs, attribute.map(_, f)), - list.map(children, map(_, f)), - ) - ElementNs(tag, attrs, children, namespace) -> - ElementNs( - tag, - list.map(attrs, attribute.map(_, f)), - list.map(children, map(_, f)), - namespace, - ) - } -} - -// CONVERSIONS ----------------------------------------------------------------- - -/// -/// -pub fn to_string(element: Element(msg)) -> String { - to_string_builder(element) - |> string_builder.to_string -} - -/// -/// -pub fn to_string_builder(element: Element(msg)) -> StringBuilder { - case element { - Text(content) -> string_builder.from_string(escape("", content)) - Element(tag, attrs, children) -> - string_builder.from_string("<" <> tag) - |> attrs_to_string_builder(attrs) - |> string_builder.append(">") - |> children_to_string_builder(children) - |> string_builder.append(" tag <> ">") - ElementNs(tag, attrs, children, namespace) -> - string_builder.from_string("<" <> tag) - |> attrs_to_string_builder(attrs) - |> string_builder.append(" xmlns=\"" <> namespace <> "\"") - |> string_builder.append(">") - |> children_to_string_builder(children) - |> string_builder.append(" tag <> ">") - } -} - -fn attrs_to_string_builder( - html: StringBuilder, - attrs: List(Attribute(msg)), -) -> StringBuilder { - use html, attr <- list.fold(attrs, html) - string_builder.append_builder(html, attribute.to_string_builder(attr)) -} - -fn children_to_string_builder( - html: StringBuilder, - children: List(Element(msg)), -) -> StringBuilder { - use html, child <- list.fold(children, html) - string_builder.append_builder(html, to_string_builder(child)) -} diff --git a/src/lustre/element/html.gleam b/src/lustre/element/html.gleam deleted file mode 100644 index 9eb4f5e..0000000 --- a/src/lustre/element/html.gleam +++ /dev/null @@ -1,1197 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import lustre/element.{Element, element, namespaced, text} -import lustre/attribute.{Attribute} - -// The doc comments (and order) for functions in this module are taken from the -// MDN Element reference: -// -// https://developer.mozilla.org/en-US/docs/Web/HTML/Element -// - -// HTML ELEMENTS: MAIN ROOT ---------------------------------------------------- - -/// Represents the root (top-level element) of an HTML document, so it is also -/// referred to as the root element. All other elements must be descendants of -/// this element. -/// -pub fn html( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("html", attrs, children) -} - -// HTML ELEMENTS: DOCUMENT METADATA -------------------------------------------- - -/// Specifies the base URL to use for all relative URLs in a document. There can -/// be only one such element in a document. -/// -pub fn base(attrs: List(Attribute(msg))) -> Element(msg) { - element("base", attrs, []) -} - -/// Contains machine-readable information (metadata) about the document, like its -/// title, scripts, and style sheets. -/// -pub fn head(attrs: List(Attribute(msg))) -> Element(msg) { - element("head", attrs, []) -} - -/// Specifies relationships between the current document and an external resource. -/// This element is most commonly used to link to CSS but is also used to establish -/// site icons (both "favicon" style icons and icons for the home screen and apps -/// on mobile devices) among other things. -/// -pub fn link(attrs: List(Attribute(msg))) -> Element(msg) { - element("link", attrs, []) -} - -/// Represents metadata that cannot be represented by other HTML meta-related -/// elements, like , ,