diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2023-09-19 23:40:03 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2023-09-19 23:40:03 +0100 |
commit | 92e8596b78982885803994b50c6b35f73f7a403e (patch) | |
tree | 13428243987317da540495215ed4d9e3938fb5cb /lib | |
parent | 985a9b0aa469cbe94fb95c433c97e2b321014341 (diff) | |
download | lustre-92e8596b78982885803994b50c6b35f73f7a403e.tar.gz lustre-92e8596b78982885803994b50c6b35f73f7a403e.zip |
:recycle: So long monorepo.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/README.md | 62 | ||||
-rw-r--r-- | lib/gleam.toml | 10 | ||||
-rw-r--r-- | lib/manifest.toml | 9 | ||||
-rw-r--r-- | lib/package-lock.json | 201 | ||||
-rw-r--r-- | lib/package.json | 10 | ||||
-rw-r--r-- | lib/src/lustre.ffi.mjs | 231 | ||||
-rw-r--r-- | lib/src/lustre.gleam | 97 | ||||
-rw-r--r-- | lib/src/lustre/attribute.gleam | 326 | ||||
-rw-r--r-- | lib/src/lustre/effect.gleam | 47 | ||||
-rw-r--r-- | lib/src/lustre/element.gleam | 139 | ||||
-rw-r--r-- | lib/src/lustre/element/html.gleam | 889 | ||||
-rw-r--r-- | lib/src/lustre/element/svg.gleam | 416 | ||||
-rw-r--r-- | lib/src/lustre/event.gleam | 178 | ||||
-rw-r--r-- | lib/src/runtime.ffi.mjs | 239 | ||||
-rw-r--r-- | lib/test/examples/components.gleam | 100 | ||||
-rw-r--r-- | lib/test/examples/components.html | 17 | ||||
-rw-r--r-- | lib/test/examples/counter.gleam | 56 | ||||
-rw-r--r-- | lib/test/examples/counter.html | 17 | ||||
-rw-r--r-- | lib/test/examples/index.html | 27 | ||||
-rw-r--r-- | lib/test/examples/input.gleam | 132 | ||||
-rw-r--r-- | lib/test/examples/input.html | 54 | ||||
-rw-r--r-- | lib/test/examples/nested.gleam | 57 | ||||
-rw-r--r-- | lib/test/examples/nested.html | 17 | ||||
-rw-r--r-- | lib/test/examples/svg.gleam | 107 | ||||
-rw-r--r-- | lib/test/examples/svg.html | 17 |
25 files changed, 0 insertions, 3455 deletions
diff --git a/lib/README.md b/lib/README.md deleted file mode 100644 index 9f393f5..0000000 --- a/lib/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Lustre - -[](https://hex.pm/packages/lustre) - -An Elm-inspired framework for building web apps in Gleam! - -```gleam -import gleam/int -import lustre -import lustre/element.{text} -import lustre/element/html.{div, button, p} -import lustre/event.{on_click} - -pub fn main() { - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start("[data-lustre-app]", Nil) - - Nil -} - -fn init(_) { - 0 -} - -type Msg { - Incr - Decr -} - -fn update(model, msg) { - case msg { - Incr -> model + 1 - Decr -> model - 1 - } -} - -fn view(model) { - let count = int.to_string(model) - - div([], [ - button([on_click(Decr)], [text(" + ")]), - p([], [text(count)]), - button([on_click(Incr)], [text(" - ")]) - ]) -} -``` - -## Documentation - -You can find the official documentation over at [pkg.hayleigh.dev/lustre](https://pkg.hayleigh.dev/lustre). -Note that if you're viewing the documentation published on Hexdocs, you may find -that things are missing! Because of the way Gleam's documentation is generated, -packages and functions that target JavaScript don't get documented. - -## Installation - -Lustre is available on [Hex](https://hex.pm/packages/lustre). You can install -it like any other Hex package: - -```sh -$ gleam add lustre -``` diff --git a/lib/gleam.toml b/lib/gleam.toml deleted file mode 100644 index f301d46..0000000 --- a/lib/gleam.toml +++ /dev/null @@ -1,10 +0,0 @@ -name = "lustre" -version = "3.0.3" - -description = "An Elm-inspired framework for building web apps in Gleam!" -licences = ["MIT"] -links = [{ title = "Buy me a coffee?", href = "https://github.com/sponsors/hayleigh-dot-dev" }] -repository = { type = "github", user = "hayleigh-dot-dev", repo = "lustre/lib" } - -[dependencies] -gleam_stdlib = "~> 0.30" diff --git a/lib/manifest.toml b/lib/manifest.toml deleted file mode 100644 index b711276..0000000 --- a/lib/manifest.toml +++ /dev/null @@ -1,9 +0,0 @@ -# This file was generated by Gleam -# You typically do not need to edit this file - -packages = [ - { name = "gleam_stdlib", version = "0.30.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "03710B3DA047A3683117591707FCA19D32B980229DD8CE8B0603EB5B5144F6C3" }, -] - -[requirements] -gleam_stdlib = { version = "~> 0.30" } diff --git a/lib/package-lock.json b/lib/package-lock.json deleted file mode 100644 index 7a63f7a..0000000 --- a/lib/package-lock.json +++ /dev/null @@ -1,201 +0,0 @@ -{ - "name": "lib", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "devDependencies": { - "vite": "^4.4.2" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.11", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/esbuild": { - "version": "0.18.11", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.11", - "@esbuild/android-arm64": "0.18.11", - "@esbuild/android-x64": "0.18.11", - "@esbuild/darwin-arm64": "0.18.11", - "@esbuild/darwin-x64": "0.18.11", - "@esbuild/freebsd-arm64": "0.18.11", - "@esbuild/freebsd-x64": "0.18.11", - "@esbuild/linux-arm": "0.18.11", - "@esbuild/linux-arm64": "0.18.11", - "@esbuild/linux-ia32": "0.18.11", - "@esbuild/linux-loong64": "0.18.11", - "@esbuild/linux-mips64el": "0.18.11", - "@esbuild/linux-ppc64": "0.18.11", - "@esbuild/linux-riscv64": "0.18.11", - "@esbuild/linux-s390x": "0.18.11", - "@esbuild/linux-x64": "0.18.11", - "@esbuild/netbsd-x64": "0.18.11", - "@esbuild/openbsd-x64": "0.18.11", - "@esbuild/sunos-x64": "0.18.11", - "@esbuild/win32-arm64": "0.18.11", - "@esbuild/win32-ia32": "0.18.11", - "@esbuild/win32-x64": "0.18.11" - } - }, - "node_modules/fsevents": { - "version": "2.3.2", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.6", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss": { - "version": "8.4.25", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "3.26.2", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vite": { - "version": "4.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.24", - "rollup": "^3.25.2" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - } - } -} diff --git a/lib/package.json b/lib/package.json deleted file mode 100644 index fdc091a..0000000 --- a/lib/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "private": true, - "type": "module", - "scripts": { - "dev": "gleam build && vite serve ./test/examples" - }, - "devDependencies": { - "vite": "^4.4.2" - } -} diff --git a/lib/src/lustre.ffi.mjs b/lib/src/lustre.ffi.mjs deleted file mode 100644 index f29e7ea..0000000 --- a/lib/src/lustre.ffi.mjs +++ /dev/null @@ -1,231 +0,0 @@ -import { - AppAlreadyStarted, - AppNotYetStarted, - BadComponentName, - ComponentAlreadyRegistered, - ElementNotFound, - NotABrowser, -} from "./lustre.mjs"; -import { from } from "./lustre/effect.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, flags) { - if (!is_browser()) return new Error(new NotABrowser()); - if (this.#root) return new Error(new AppAlreadyStarted()); - - this.#root = - selector instanceof HTMLElement - ? selector - : document.querySelector(selector); - - if (!this.#root) return new Error(new ElementNotFound()); - - const [next, effects] = this.#init(flags); - - this.#state = next; - this.#effects = effects[0].toArray(); - this.#didUpdate = true; - - window.requestAnimationFrame(() => this.#tick()); - - return new Ok((msg) => this.dispatch(msg)); - } - - 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() { - if (!this.#root) return new Error(new AppNotYetStarted()); - - this.#root.remove(); - this.#root = null; - this.#state = null; - this.#queue = []; - this.#effects = []; - this.#didUpdate = false; - this.#update = () => {}; - this.#view = () => {}; - } - - #tick() { - this.#flush(); - - if (this.#didUpdate) { - const vdom = this.#view(this.#state); - - this.#root = morph(this.#root, vdom, (msg) => this.dispatch(msg)); - this.#didUpdate = false; - } - } - - #flush(times = 0) { - if (!this.#root) return; - if (this.#queue.length) { - while (this.#queue.length) { - const [next, effects] = this.#update(this.#state, this.#queue.shift()); - // If the user returned their model unchanged and not reconstructed then - // we don't need to trigger a re-render. - this.#didUpdate ||= this.#state !== next; - this.#state = next; - this.#effects = this.#effects.concat(effects[0].toArray()); - } - } - - // Each update can produce effects which must now be executed. - while (this.#effects.length) - 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, flags) => app.start(selector, flags); -export const destroy = (app) => app.destroy(); - -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 (!name.includes("-")) return new Error(new BadComponentName()); - if (!is_browser()) return new Error(new NotABrowser()); - 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); - // This is necessary for ✨ reasons ✨. Clearly there's a bug in the - // implementation of either the `App` or the runtime but I con't work it - // out. - // - // If we pass the container to the app directly then the component fails - // to render anything to the ODM. - this.#container.appendChild(document.createElement("div")); - - const dispatch = this.#app.start(this.#container.firstChild); - 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.firstChild); - } - - attributeChangedCallback(name, prev, next) { - if (prev !== next) { - this[name] = next; - } - } - - disconnectedCallback() { - this.#app.destroy(); - } - } - ); - - return new Ok(null); -}; - -// UTLS ------------------------------------------------------------------------ - -export const is_browser = () => window && window.document; -export const is_registered = (name) => !!customElements.get(name); diff --git a/lib/src/lustre.gleam b/lib/src/lustre.gleam deleted file mode 100644 index 4f8249a..0000000 --- a/lib/src/lustre.gleam +++ /dev/null @@ -1,97 +0,0 @@ -//// To read the full documentation for this module, please visit -//// [https://pkg.hayleigh.dev/lustre/api/lustre](https://pkg.hayleigh.dev/lustre/api/lustre) - -// IMPORTS --------------------------------------------------------------------- - -import gleam/dynamic.{Decoder} -import gleam/map.{Map} -import lustre/effect.{Effect} -import lustre/element.{Element} - -// TYPES ----------------------------------------------------------------------- - -/// -pub type App(flags, model, msg) - -pub type Error { - AppAlreadyStarted - AppNotYetStarted - BadComponentName - ComponentAlreadyRegistered - ElementNotFound - NotABrowser -} - -// CONSTRUCTORS ---------------------------------------------------------------- - -@target(javascript) -/// -pub fn element(element: Element(msg)) -> App(Nil, Nil, msg) { - let init = fn(_) { #(Nil, effect.none()) } - let update = fn(_, _) { #(Nil, effect.none()) } - let view = fn(_) { element } - - application(init, update, view) -} - -@target(javascript) -/// -pub fn simple( - init: fn(flags) -> model, - update: fn(model, msg) -> model, - view: fn(model) -> Element(msg), -) -> App(flags, model, msg) { - let init = fn(flags) { #(init(flags), effect.none()) } - let update = fn(model, msg) { #(update(model, msg), effect.none()) } - - application(init, update, view) -} - -@target(javascript) -/// -@external(javascript, "./lustre.ffi.mjs", "setup") -pub fn application( - init: fn(flags) -> #(model, Effect(msg)), - update: fn(model, msg) -> #(model, Effect(msg)), - view: fn(model) -> Element(msg), -) -> App(flags, model, msg) - -@target(javascript) -@external(javascript, "./lustre.ffi.mjs", "setup_component") -pub fn component( - name: String, - init: fn() -> #(model, Effect(msg)), - update: fn(model, msg) -> #(model, Effect(msg)), - view: fn(model) -> Element(msg), - on_attribute_change: Map(String, Decoder(msg)), -) -> Result(Nil, Error) - -// EFFECTS --------------------------------------------------------------------- - -@target(javascript) -/// -@external(javascript, "./lustre.ffi.mjs", "start") -pub fn start( - app: App(flags, model, msg), - selector: String, - flags: flags, -) -> Result(fn(msg) -> Nil, Error) - -@target(javascript) -/// -@external(javascript, "./lustre.ffi.mjs", "destroy") -pub fn destroy(app: App(flags, model, msg)) -> Result(Nil, Error) - -// UTILS ----------------------------------------------------------------------- - -/// -@external(javascript, "./lustre.ffi.mjs", "is_browser") -pub fn is_browser() -> Bool { - False -} - -/// -@external(javascript, "./lustre.ffi.mjs", "is_registered") -pub fn is_registered(_name: String) -> Bool { - False -} diff --git a/lib/src/lustre/attribute.gleam b/lib/src/lustre/attribute.gleam deleted file mode 100644 index 0c79692..0000000 --- a/lib/src/lustre/attribute.gleam +++ /dev/null @@ -1,326 +0,0 @@ -//// To read the full documentation for this module, please visit -//// [https://pkg.hayleigh.dev/lustre/api/lustre/attribute](https://pkg.hayleigh.dev/lustre/api/lustre/attribute) - -// IMPORTS --------------------------------------------------------------------- - -import gleam/dynamic.{Dynamic} -import gleam/function -import gleam/int -import gleam/list -import gleam/result -import gleam/string -import gleam/string_builder.{StringBuilder} - -// TYPES ----------------------------------------------------------------------- - -/// -pub opaque type Attribute(msg) { - Attribute(String, Dynamic, as_property: Bool) - Event(String, fn(Dynamic) -> Result(msg, Nil)) -} - -// CONSTRUCTORS ---------------------------------------------------------------- - -/// -pub fn attribute(name: String, value: String) -> Attribute(msg) { - Attribute(name, dynamic.from(value), as_property: False) -} - -/// -pub fn property(name: String, value: any) -> Attribute(msg) { - Attribute(name, dynamic.from(value), as_property: True) -} - -/// -pub fn on( - name: String, - handler: fn(Dynamic) -> Result(msg, error), -) -> Attribute(msg) { - Event("on" <> name, function.compose(handler, result.replace_error(_, Nil))) -} - -// MANIPULATIONS --------------------------------------------------------------- - -/// -pub fn map(attr: Attribute(a), f: fn(a) -> b) -> Attribute(b) { - case attr { - Attribute(name, value, as_property) -> Attribute(name, value, as_property) - Event(on, handler) -> Event(on, fn(e) { result.map(handler(e), f) }) - } -} - -// CONVERSIONS ----------------------------------------------------------------- - -/// -/// -pub fn to_string(attr: Attribute(msg)) -> String { - case attr { - Attribute(name, value, as_property: False) -> { - 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) <> "\"" - } - } - Attribute(_, _, as_property: True) -> "" - Event(on, _) -> "data-lustre-on:" <> on - } -} - -/// -pub fn to_string_builder(attr: Attribute(msg)) -> StringBuilder { - case attr { - Attribute(name, value, as_property: False) -> { - 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 - } - } - Attribute(_, _, as_property: True) -> string_builder.new() - 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/lib/src/lustre/effect.gleam b/lib/src/lustre/effect.gleam deleted file mode 100644 index 964ddd2..0000000 --- a/lib/src/lustre/effect.gleam +++ /dev/null @@ -1,47 +0,0 @@ -//// To read the full documentation for this module, please visit -//// [https://pkg.hayleigh.dev/lustre/api/lustre/effect](https://pkg.hayleigh.dev/lustre/api/lustre/effect) - -// IMPORTS --------------------------------------------------------------------- - -import gleam/list - -// TYPES ----------------------------------------------------------------------- - -/// -pub opaque type Effect(msg) { - Effect(List(fn(fn(msg) -> Nil) -> Nil)) -} - -// CONSTRUCTORS ---------------------------------------------------------------- - -/// -pub fn from(effect: fn(fn(msg) -> Nil) -> Nil) -> Effect(msg) { - Effect([effect]) -} - -/// Typically our app's `update` function needs to return a tuple of -/// `#(model, Effect(msg))`. When we don't need to perform any side effects we -/// can just return `none()`! -/// -pub fn none() -> Effect(msg) { - Effect([]) -} - -// MANIPULATIONS --------------------------------------------------------------- - -/// -pub fn batch(effects: List(Effect(msg))) -> Effect(msg) { - Effect({ - use b, Effect(a) <- list.fold(effects, []) - 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/lib/src/lustre/element.gleam b/lib/src/lustre/element.gleam deleted file mode 100644 index 4608db7..0000000 --- a/lib/src/lustre/element.gleam +++ /dev/null @@ -1,139 +0,0 @@ -//// To read the full documentation for this module, please visit -//// [https://pkg.hayleigh.dev/lustre/api/lustre/element](https://pkg.hayleigh.dev/lustre/api/lustre/element) - -// 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("area" as tag, attrs, _) - | Element("base" as tag, attrs, _) - | Element("br" as tag, attrs, _) - | Element("col" as tag, attrs, _) - | Element("embed" as tag, attrs, _) - | Element("hr" as tag, attrs, _) - | Element("img" as tag, attrs, _) - | Element("input" as tag, attrs, _) - | Element("link" as tag, attrs, _) - | Element("meta" as tag, attrs, _) - | Element("param" as tag, attrs, _) - | Element("source" as tag, attrs, _) - | Element("track" as tag, attrs, _) - | Element("wbr" as tag, attrs, _) -> - string_builder.from_string("<" <> tag) - |> attrs_to_string_builder(attrs) - |> string_builder.append(">") - 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/lib/src/lustre/element/html.gleam b/lib/src/lustre/element/html.gleam deleted file mode 100644 index a69a5fd..0000000 --- a/lib/src/lustre/element/html.gleam +++ /dev/null @@ -1,889 +0,0 @@ -//// To read the full documentation for this module, please visit -//// [https://pkg.hayleigh.dev/lustre/api/lustre/element/html](https://pkg.hayleigh.dev/lustre/api/lustre/element/html) - -// IMPORTS --------------------------------------------------------------------- - -import lustre/element.{Element, element, namespaced, text} -import lustre/attribute.{Attribute} - -// HTML ELEMENTS: MAIN ROOT ---------------------------------------------------- - -/// -pub fn html( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("html", attrs, children) -} - -// HTML ELEMENTS: DOCUMENT METADATA -------------------------------------------- - -/// -pub fn base(attrs: List(Attribute(msg))) -> Element(msg) { - element("base", attrs, []) -} - -/// -pub fn head( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("head", attrs, children) -} - -/// -pub fn link(attrs: List(Attribute(msg))) -> Element(msg) { - element("link", attrs, []) -} - -/// -pub fn meta(attrs: List(Attribute(msg))) -> Element(msg) { - element("meta", attrs, []) -} - -/// -pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg) { - element("style", attrs, [text(css)]) -} - -/// -pub fn title(attrs: List(Attribute(msg)), content: String) -> Element(msg) { - element("title", attrs, [text(content)]) -} - -// HTML ELEMENTS: SECTIONING ROOT ----------------------------------------------- - -/// -pub fn body( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("body", attrs, children) -} - -// HTML ELEMENTS: CONTENT SECTIONING ------------------------------------------- - -/// -pub fn address( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("address", attrs, children) -} - -/// -pub fn article( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("article", attrs, children) -} - -/// -pub fn aside( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("aside", attrs, children) -} - -/// -pub fn footer( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("footer", attrs, children) -} - -/// -pub fn header( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("header", attrs, children) -} - -/// -pub fn h1( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("h1", attrs, children) -} - -/// -pub fn h2( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("h2", attrs, children) -} - -/// -pub fn h3( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("h3", attrs, children) -} - -/// -pub fn h4( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("h4", attrs, children) -} - -/// -pub fn h5( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("h5", attrs, children) -} - -/// -pub fn h6( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("h6", attrs, children) -} - -/// -pub fn hgroup( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("hgroup", attrs, children) -} - -/// -pub fn main( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("main", attrs, children) -} - -/// -pub fn nav( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("nav", attrs, children) -} - -/// -pub fn section( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("section", attrs, children) -} - -/// -pub fn search( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("search", attrs, children) -} - -// HTML ELEMENTS: TEXT CONTENT ------------------------------------------------- - -/// -pub fn blockquote( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("blockquote", attrs, children) -} - -/// -pub fn dd( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("dd", attrs, children) -} - -/// -pub fn div( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("div", attrs, children) -} - -/// -pub fn dl( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("dl", attrs, children) -} - -/// -pub fn dt( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("dt", attrs, children) -} - -/// -pub fn figcaption( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("figcaption", attrs, children) -} - -/// -pub fn figure( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("figure", attrs, children) -} - -/// -pub fn hr(attrs: List(Attribute(msg))) -> Element(msg) { - element("hr", attrs, []) -} - -/// -pub fn li( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("li", attrs, children) -} - -/// -pub fn menu( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("menu", attrs, children) -} - -/// -pub fn ol( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("ol", attrs, children) -} - -/// -pub fn p( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("p", attrs, children) -} - -/// -pub fn pre( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("pre", attrs, children) -} - -/// -pub fn ul( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("ul", attrs, children) -} - -// HTML ELEMENTS: INLINE TEXT SEMANTICS ---------------------------------------- - -/// -pub fn a( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("a", attrs, children) -} - -/// -pub fn abbr( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("abbr", attrs, children) -} - -/// -pub fn b( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("b", attrs, children) -} - -/// -pub fn bdi( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("bdi", attrs, children) -} - -/// -pub fn bdo( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("bdo", attrs, children) -} - -/// -pub fn br(attrs: List(Attribute(msg))) -> Element(msg) { - element("br", attrs, []) -} - -/// -pub fn cite( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("cite", attrs, children) -} - -/// -pub fn code( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("code", attrs, children) -} - -/// -pub fn data( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("data", attrs, children) -} - -/// -pub fn dfn( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("dfn", attrs, children) -} - -/// -pub fn em( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("em", attrs, children) -} - -/// -pub fn i( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("i", attrs, children) -} - -/// -pub fn kbd( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("kbd", attrs, children) -} - -/// -pub fn mark( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("mark", attrs, children) -} - -/// -pub fn q( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("q", attrs, children) -} - -/// -pub fn rp( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("rp", attrs, children) -} - -/// -pub fn rt( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("rt", attrs, children) -} - -/// -pub fn ruby( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("ruby", attrs, children) -} - -/// -pub fn s( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("s", attrs, children) -} - -/// -pub fn samp( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("samp", attrs, children) -} - -/// -pub fn small( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("small", attrs, children) -} - -/// -pub fn span( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("span", attrs, children) -} - -/// -pub fn strong( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("strong", attrs, children) -} - -/// -pub fn sub( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("sub", attrs, children) -} - -/// -pub fn sup( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("sup", attrs, children) -} - -/// -pub fn time( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("time", attrs, children) -} - -/// -pub fn u( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("u", attrs, children) -} - -/// -pub fn var( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("var", attrs, children) -} - -/// -pub fn wbr(attrs: List(Attribute(msg))) -> Element(msg) { - element("wbr", attrs, []) -} - -// HTML ELEMENTS: IMAGE AND MULTIMEDIA ----------------------------------------- - -/// -pub fn area(attrs: List(Attribute(msg))) -> Element(msg) { - element("area", attrs, []) -} - -/// -pub fn audio( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("audio", attrs, children) -} - -/// -pub fn img(attrs: List(Attribute(msg))) -> Element(msg) { - element("img", attrs, []) -} - -/// Used with <area> elements to define an image map (a clickable link area). -/// -pub fn map( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("map", attrs, children) -} - -/// -pub fn track(attrs: List(Attribute(msg))) -> Element(msg) { - element("track", attrs, []) -} - -/// -pub fn video( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("video", attrs, children) -} - -// HTML ELEMENTS: EMBEDDED CONTENT --------------------------------------------- - -/// -pub fn embed(attrs: List(Attribute(msg))) -> Element(msg) { - element("embed", attrs, []) -} - -/// -pub fn iframe(attrs: List(Attribute(msg))) -> Element(msg) { - element("iframe", attrs, []) -} - -/// -pub fn object(attrs: List(Attribute(msg))) -> Element(msg) { - element("object", attrs, []) -} - -/// -pub fn picture( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("picture", attrs, children) -} - -/// -pub fn portal(attrs: List(Attribute(msg))) -> Element(msg) { - element("portal", attrs, []) -} - -/// -pub fn source(attrs: List(Attribute(msg))) -> Element(msg) { - element("source", attrs, []) -} - -// HTML ELEMENTS: SVG AND MATHML ----------------------------------------------- - -/// -pub fn svg( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced("http://www.w3.org/2000/svg", "svg", attrs, children) -} - -/// -pub fn math( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("math", attrs, children) -} - -// HTML ELEMENTS: SCRIPTING ---------------------------------------------------- - -/// -pub fn canvas(attrs: List(Attribute(msg))) -> Element(msg) { - element("canvas", attrs, []) -} - -/// -pub fn noscript( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element("noscript", attrs, children) -} - -/// -pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg) { - element("script", attrs, [text(js)]) -} - -// HTML ELEMENTS: DEMARCATING EDITS --------------------------------------------- - -/// -pub fn del( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("del", attrs, children) -} - -/// -pub fn ins( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("ins", attrs, children) -} - -// HTML ELEMENTS: TABLE CONTENT ------------------------------------------------ - -/// -pub fn caption( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("caption", attrs, children) -} - -/// -pub fn col(attrs: List(Attribute(msg))) -> Element(msg) { - element.element("col", attrs, []) -} - -/// -pub fn colgroup( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("colgroup", attrs, children) -} - -/// -pub fn table( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("table", attrs, children) -} - -/// -pub fn tbody( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("tbody", attrs, children) -} - -/// -pub fn td( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("td", attrs, children) -} - -/// -pub fn tfoot( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("tfoot", attrs, children) -} - -/// -pub fn th( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("th", attrs, children) -} - -/// -pub fn thead( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("thead", attrs, children) -} - -/// -pub fn tr( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("tr", attrs, children) -} - -// HTML ELEMENTS: FORMS -------------------------------------------------------- - -/// -pub fn button( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("button", attrs, children) -} - -/// -pub fn datalist( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("datalist", attrs, children) -} - -/// -pub fn fieldset( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("fieldset", attrs, children) -} - -/// -pub fn form( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("form", attrs, children) -} - -/// -pub fn input(attrs: List(Attribute(msg))) -> Element(msg) { - element.element("input", attrs, []) -} - -/// -pub fn label( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("label", attrs, children) -} - -/// -pub fn legend( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("legend", attrs, children) -} - -/// -pub fn meter( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("meter", attrs, children) -} - -/// -pub fn optgroup( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("optgroup", attrs, children) -} - -/// -pub fn option(attrs: List(Attribute(msg))) -> Element(msg) { - element.element("option", attrs, []) -} - -/// -pub fn output( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("output", attrs, children) -} - -/// -pub fn progress( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("progress", attrs, children) -} - -/// -pub fn select( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("select", attrs, children) -} - -/// -pub fn textarea(attrs: List(Attribute(msg))) -> Element(msg) { - element.element("textarea", attrs, []) -} - -// HTML ELEMENTS: INTERACTIVE ELEMENTS ----------------------------------------- - -/// -pub fn details( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("details", attrs, children) -} - -/// -pub fn dialog( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("dialog", attrs, children) -} - -/// -pub fn summary( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("summary", attrs, children) -} - -// HTML ELEMENTS: WEB COMPONENTS ----------------------------------------------- - -/// -pub fn slot(attrs: List(Attribute(msg))) -> Element(msg) { - element.element("slot", attrs, []) -} - -/// -pub fn template( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - element.element("template", attrs, children) -} diff --git a/lib/src/lustre/element/svg.gleam b/lib/src/lustre/element/svg.gleam deleted file mode 100644 index a1fc0ce..0000000 --- a/lib/src/lustre/element/svg.gleam +++ /dev/null @@ -1,416 +0,0 @@ -//// To read the full documentation for this module, please visit -//// [https://pkg.hayleigh.dev/lustre/api/lustre/element/svg](https://pkg.hayleigh.dev/lustre/api/lustre/element/svg) - -// IMPORTS --------------------------------------------------------------------- - -import lustre/element.{Element, namespaced, text as inline_text} -import lustre/attribute.{Attribute} - -// CONSTANTS ------------------------------------------------------------------- - -const namespace = "http://www.w3.org/2000/svg" - -// 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/SVG/Element -// - -// SVG ELEMENTS: ANIMATION ELEMENTS -------------------------------------------- - -/// -pub fn animate(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "animate", attrs, []) -} - -/// -pub fn animate_motion(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "animateMotion", attrs, []) -} - -/// -pub fn animate_transform(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "animateTransform", attrs, []) -} - -/// -pub fn mpath(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "mpath", attrs, []) -} - -/// -pub fn set(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "set", attrs, []) -} - -// SVG ELEMENTS: BASIC SHAPES -------------------------------------------------- - -/// -pub fn circle(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "circle", attrs, []) -} - -/// -pub fn ellipse(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "ellipse", attrs, []) -} - -/// -pub fn line(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "line", attrs, []) -} - -/// -pub fn polygon(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "polygon", attrs, []) -} - -/// -pub fn polyline(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "polyline", attrs, []) -} - -/// -pub fn rect(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "rect", attrs, []) -} - -// SVG ELEMENTS: CONTAINER ELEMENTS -------------------------------------------- - -/// -pub fn a( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "a", attrs, children) -} - -/// -pub fn defs( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "defs", attrs, children) -} - -/// -pub fn g( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "g", attrs, children) -} - -/// -pub fn marker( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "marker", attrs, children) -} - -/// -pub fn mask( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "mask", attrs, children) -} - -/// -pub fn missing_glyph( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "missing-glyph", attrs, children) -} - -/// -pub fn pattern( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "pattern", attrs, children) -} - -/// -pub fn svg( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "svg", attrs, children) -} - -/// -pub fn switch( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "switch", attrs, children) -} - -/// -pub fn symbol( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "symbol", attrs, children) -} - -// SVG ELEMENTS: DESCRIPTIVE ELEMENTS ------------------------------------------ - -/// -pub fn desc( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "desc", attrs, children) -} - -/// -pub fn metadata( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "metadata", attrs, children) -} - -/// -pub fn title( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "title", attrs, children) -} - -// SVG ELEMENTS: FILTER EFFECTS ------------------------------------------------ - -/// -pub fn fe_blend(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feBlend", attrs, []) -} - -/// -pub fn fe_color_matrix(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feColorMatrix", attrs, []) -} - -/// -pub fn fe_component_transfer(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feComponentTransfer", attrs, []) -} - -/// -pub fn fe_composite(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feComposite", attrs, []) -} - -/// -pub fn fe_convolve_matrix(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feConvolveMatrix", attrs, []) -} - -/// -pub fn fe_diffuse_lighting( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "feDiffuseLighting", attrs, children) -} - -/// -pub fn fe_displacement_map(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feDisplacementMap", attrs, []) -} - -/// -pub fn fe_drop_shadow(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feDropShadow", attrs, []) -} - -/// -pub fn fe_flood(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feFlood", attrs, []) -} - -/// -pub fn fe_func_a(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feFuncA", attrs, []) -} - -/// -pub fn fe_func_b(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feFuncB", attrs, []) -} - -/// -pub fn fe_func_g(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feFuncG", attrs, []) -} - -/// -pub fn fe_func_r(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feFuncR", attrs, []) -} - -/// -pub fn fe_gaussian_blur(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feGaussianBlur", attrs, []) -} - -/// -pub fn fe_image(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feImage", attrs, []) -} - -/// -pub fn fe_merge( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "feMerge", attrs, children) -} - -/// -pub fn fe_merge_node(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feMergeNode", attrs, []) -} - -/// -pub fn fe_morphology(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feMorphology", attrs, []) -} - -/// -pub fn fe_offset(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feOffset", attrs, []) -} - -/// -pub fn fe_specular_lighting( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "feSpecularLighting", attrs, children) -} - -/// -pub fn fe_tile( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "feTile", attrs, children) -} - -/// -pub fn fe_turbulence(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feTurbulence", attrs, []) -} - -// SVG ELEMENTS: GRADIENT ELEMENTS --------------------------------------------- - -/// -pub fn linear_gradient( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "linearGradient", attrs, children) -} - -/// -pub fn radial_gradient( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "radialGradient", attrs, children) -} - -/// -pub fn stop(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "stop", attrs, []) -} - -// SVG ELEMENTS: GRAPHICAL ELEMENTS -------------------------------------------- - -/// -pub fn image(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "image", attrs, []) -} - -/// -pub fn path(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "path", attrs, []) -} - -/// -pub fn text(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "text", attrs, []) -} - -/// -pub fn use_(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "use", attrs, []) -} - -// SVG ELEMENTS: LIGHTING ELEMENTS --------------------------------------------- - -/// -pub fn fe_distant_light(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feDistantLight", attrs, []) -} - -/// -pub fn fe_point_light(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "fePointLight", attrs, []) -} - -/// -pub fn fe_spot_light(attrs: List(Attribute(msg))) -> Element(msg) { - namespaced(namespace, "feSpotLight", attrs, []) -} - -// SVG ELEMENTS: NEVER-RENDERED ELEMENTS --------------------------------------- - -/// -pub fn clip_path( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "clipPath", attrs, children) -} - -/// -pub fn script(attrs: List(Attribute(msg)), js: String) -> Element(msg) { - namespaced(namespace, "script", attrs, [inline_text(js)]) -} - -/// -pub fn style(attrs: List(Attribute(msg)), css: String) -> Element(msg) { - namespaced(namespace, "style", attrs, [inline_text(css)]) -} - -// SVG ELEMENTS: RENDERABLE ELEMENTS ------------------------------------------- - -/// -pub fn foreign_object( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "foreignObject", attrs, children) -} - -/// -pub fn text_path( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "textPath", attrs, children) -} - -/// -pub fn tspan( - attrs: List(Attribute(msg)), - children: List(Element(msg)), -) -> Element(msg) { - namespaced(namespace, "tspan", attrs, children) -} diff --git a/lib/src/lustre/event.gleam b/lib/src/lustre/event.gleam deleted file mode 100644 index 7913ef5..0000000 --- a/lib/src/lustre/event.gleam +++ /dev/null @@ -1,178 +0,0 @@ -//// To read the full documentation for this module, please visit -//// [https://pkg.hayleigh.dev/lustre/api/lustre/event](https://pkg.hayleigh.dev/lustre/api/lustre/event) - -// IMPORTS --------------------------------------------------------------------- - -import gleam/dynamic.{DecodeError, Dynamic} -import gleam/result -import lustre/attribute.{Attribute} -import lustre/effect.{Effect} - -// TYPES ----------------------------------------------------------------------- - -type Decoded(a) = - Result(a, List(DecodeError)) - -// EFFECTS --------------------------------------------------------------------- - -@target(javascript) -/// -@external(javascript, "../lustre.ffi.mjs", "emit") -pub fn emit(event: String, data: any) -> Effect(msg) - -// CUSTOM EVENTS --------------------------------------------------------------- - -/// -pub fn on( - name: String, - handler: fn(Dynamic) -> Result(msg, error), -) -> Attribute(msg) { - attribute.on(name, handler) -} - -// MOUSE EVENTS ---------------------------------------------------------------- - -/// -pub fn on_click(msg: msg) -> Attribute(msg) { - use _ <- on("click") - Ok(msg) -} - -/// -pub fn on_mouse_down(msg: msg) -> Attribute(msg) { - use _ <- on("mousedown") - Ok(msg) -} - -/// -pub fn on_mouse_up(msg: msg) -> Attribute(msg) { - use _ <- on("mouseup") - Ok(msg) -} - -/// -pub fn on_mouse_enter(msg: msg) -> Attribute(msg) { - use _ <- on("mouseenter") - Ok(msg) -} - -/// -pub fn on_mouse_leave(msg: msg) -> Attribute(msg) { - use _ <- on("mouseleave") - Ok(msg) -} - -/// -pub fn on_mouse_over(msg: msg) -> Attribute(msg) { - use _ <- on("mouseover") - Ok(msg) -} - -/// -pub fn on_mouse_out(msg: msg) -> Attribute(msg) { - use _ <- on("mouseout") - Ok(msg) -} - -// KEYBOARD EVENTS ------------------------------------------------------------- - -/// Listens for key presses on an element, and dispatches a message with the -/// current key being pressed. -/// -pub fn on_keypress(msg: fn(String) -> msg) -> Attribute(msg) { - use event <- on("keypress") - - event - |> dynamic.field("key", dynamic.string) - |> result.map(msg) -} - -/// Listens for key dow events on an element, and dispatches a message with the -/// current key being pressed. -/// -pub fn on_keydown(msg: fn(String) -> msg) -> Attribute(msg) { - use event <- on("keydown") - - event - |> dynamic.field("key", dynamic.string) - |> result.map(msg) -} - -/// Listens for key up events on an element, and dispatches a message with the -/// current key being released. -/// -pub fn on_keyup(msg: fn(String) -> msg) -> Attribute(msg) { - use event <- on("keyup") - - event - |> dynamic.field("key", dynamic.string) - |> result.map(msg) -} - -// FORM EVENTS ----------------------------------------------------------------- - -/// -pub fn on_input(msg: fn(String) -> msg) -> Attribute(msg) { - use event <- on("input") - - value(event) - |> result.map(msg) -} - -pub fn on_check(msg: fn(Bool) -> msg) -> Attribute(msg) { - use event <- on("change") - - checked(event) - |> result.map(msg) -} - -pub fn on_submit(msg: msg) -> Attribute(msg) { - use event <- on("submit") - let _ = prevent_default(event) - - Ok(msg) -} - -// FOCUS EVENTS ---------------------------------------------------------------- - -pub fn on_focus(msg: msg) -> Attribute(msg) { - use _ <- on("focus") - Ok(msg) -} - -pub fn on_blur(msg: msg) -> Attribute(msg) { - use _ <- on("blur") - Ok(msg) -} - -// DECODERS -------------------------------------------------------------------- - -/// -pub fn value(event: Dynamic) -> Decoded(String) { - event - |> dynamic.field("target", dynamic.field("value", dynamic.string)) -} - -/// -pub fn checked(event: Dynamic) -> Decoded(Bool) { - event - |> dynamic.field("target", dynamic.field("checked", dynamic.bool)) -} - -/// -pub fn mouse_position(event: Dynamic) -> Decoded(#(Float, Float)) { - use x <- result.then(dynamic.field("clientX", dynamic.float)(event)) - use y <- result.then(dynamic.field("clientY", dynamic.float)(event)) - - Ok(#(x, y)) -} - -// UTILS ----------------------------------------------------------------------- - -@target(javascript) -@external(javascript, "../lustre.ffi.mjs", "prevent_default") -pub fn prevent_default(event: Dynamic) -> Nil - -@target(javascript) -@external(javascript, "../lustre.ffi.mjs", "stop_propagation") -pub fn stop_propagation(event: Dynamic) -> Nil diff --git a/lib/src/runtime.ffi.mjs b/lib/src/runtime.ffi.mjs deleted file mode 100644 index dd02f8b..0000000 --- a/lib/src/runtime.ffi.mjs +++ /dev/null @@ -1,239 +0,0 @@ -import { Empty } from "./gleam.mjs"; -import { map as result_map } from "../gleam_stdlib/gleam/result.mjs"; - -export function morph(prev, curr, dispatch, parent) { - if (curr[3]) { - return prev?.nodeType === 1 && - prev.nodeName === curr[0].toUpperCase() && - prev.namespaceURI === curr[3] - ? morphElement(prev, curr, curr[3], dispatch, parent) - : createElement(prev, curr, curr[3], dispatch, parent); - } - - if (curr[2]) { - return prev?.nodeType === 1 && prev.nodeName === curr[0].toUpperCase() - ? morphElement(prev, curr, null, dispatch, parent) - : createElement(prev, curr, null, dispatch, parent); - } - - if (curr[0] && typeof curr[0] === "string") { - return prev?.nodeType === 3 - ? morphText(prev, curr) - : createText(prev, curr); - } - - return document.createComment( - [ - "[internal lustre error] I couldn't work out how to render this element. This", - "function should only be called internally by lustre's runtime: if you think", - "this is an error, please open an issue at", - "https://github.com/hayleigh-dot-dev/gleam-lustre/issues/new", - ].join(" ") - ); -} - -// ELEMENTS -------------------------------------------------------------------- - -function createElement(prev, curr, ns, dispatch, parent = null) { - const el = ns - ? document.createElementNS(ns, curr[0]) - : document.createElement(curr[0]); - - el.$lustre = {}; - - let attr = curr[1]; - while (attr.head) { - morphAttr( - el, - attr.head[0], - attr.head[0] === "class" && el.className - ? `${el.className} ${attr.head[1]}` - : attr.head[1], - dispatch - ); - - attr = attr.tail; - } - - if (customElements.get(curr[0])) { - el._slot = curr[2]; - } else if (curr[0] === "slot") { - let child = new Empty(); - let parentWithSlot = parent; - - while (parentWithSlot) { - if (parentWithSlot._slot) { - child = parentWithSlot._slot; - break; - } else { - parentWithSlot = parentWithSlot.parentNode; - } - } - - while (child.head) { - el.appendChild(morph(null, child.head, dispatch, el)); - child = child.tail; - } - } else { - let child = curr[2]; - while (child.head) { - el.appendChild(morph(null, child.head, dispatch, el)); - child = child.tail; - } - - if (prev) prev.replaceWith(el); - } - - return el; -} - -function morphElement(prev, curr, ns, dispatch, parent) { - const prevAttrs = prev.attributes; - const currAttrs = new Map(); - - let currAttr = curr[1]; - while (currAttr.head) { - currAttrs.set( - currAttr.head[0], - currAttr.head[0] === "class" && currAttrs.has("class") - ? `${currAttrs.get("class")} ${currAttr.head[1]}` - : currAttr.head[1] - ); - - currAttr = currAttr.tail; - } - - for (const { name, value: prevValue } of prevAttrs) { - if (!currAttrs.has(name)) { - prev.removeAttribute(name); - } else { - const value = currAttrs.get(name); - - if (value !== prevValue) { - morphAttr(prev, name, value, dispatch); - currAttrs.delete(name); - } - } - } - - for (const [name, value] of currAttrs) { - morphAttr(prev, name, value, dispatch); - } - - if (customElements.get(curr[0])) { - prev._slot = curr[2]; - } else if (curr[0] === "slot") { - let prevChild = prev.firstChild; - let currChild = new Empty(); - let parentWithSlot = parent; - - while (parentWithSlot) { - if (parentWithSlot._slot) { - currChild = parentWithSlot._slot; - break; - } else { - parentWithSlot = parentWithSlot.parentNode; - } - } - - while (prevChild) { - if (currChild.head) { - morph(prevChild, currChild.head, dispatch, prev); - currChild = currChild.tail; - } - - prevChild = prevChild.nextSibling; - } - - while (currChild.head) { - prev.appendChild(morph(null, currChild.head, dispatch, prev)); - currChild = currChild.tail; - } - } else { - let prevChild = prev.firstChild; - let currChild = curr[2]; - - while (prevChild) { - if (currChild.head) { - const next = prevChild.nextSibling; - morph(prevChild, currChild.head, dispatch, prev); - currChild = currChild.tail; - prevChild = next; - } else { - const next = prevChild.nextSibling; - prevChild.remove(); - prevChild = next; - } - } - - while (currChild.head) { - prev.appendChild(morph(null, currChild.head, dispatch, prev)); - currChild = currChild.tail; - } - } - - return prev; -} - -// ATTRIBUTES ------------------------------------------------------------------ - -function morphAttr(el, name, value, dispatch) { - switch (typeof value) { - case "string": - if (el.getAttribute(name) !== value) el.setAttribute(name, value); - if (value === "") el.removeAttribute(name); - if (name === "value" && el.value !== value) el.value = value; - break; - - // Event listeners need to be handled slightly differently because we need - // to be able to support custom events. We - case name.startsWith("on") && "function": { - if (el.$lustre[name] === value) break; - - const event = name.slice(2).toLowerCase(); - const handler = (e) => result_map(value(e), dispatch); - - if (el.$lustre[`${name}Handler`]) { - el.removeEventListener(event, el.$lustre[`${name}Handler`]); - } - - el.addEventListener(event, handler); - - el.$lustre[name] = value; - el.$lustre[`${name}Handler`] = handler; - - break; - } - - default: - el[name] = value; - } -} - -// TEXT ------------------------------------------------------------------------ - -function createText(prev, curr) { - if (!curr[0]) { - prev?.remove(); - return null; - } - - const el = document.createTextNode(curr[0]); - - if (prev) prev.replaceWith(el); - return el; -} - -function morphText(prev, curr) { - const prevValue = prev.nodeValue; - const currValue = curr[0]; - - if (!currValue) { - prev?.remove(); - return null; - } - - if (prevValue !== currValue) prev.nodeValue = currValue; - - return prev; -} diff --git a/lib/test/examples/components.gleam b/lib/test/examples/components.gleam deleted file mode 100644 index b87cf84..0000000 --- a/lib/test/examples/components.gleam +++ /dev/null @@ -1,100 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/dynamic -import gleam/function -import gleam/int -import gleam/list -import gleam/map -import gleam/result -import lustre -import lustre/attribute -import lustre/effect -import lustre/element.{element, text} -import lustre/element/html.{button, div, li, ol, p, slot} -import lustre/event - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - let assert Ok(_) = - lustre.component( - "custom-counter", - counter_init, - counter_update, - counter_view, - map.from_list([ - #( - "count", - fn(attr) { - dynamic.int(attr) - |> result.map(GotCount) - }, - ), - ]), - ) - - // A `simple` lustre application doesn't produce `Effect`s. These are best to - // start with if you're just getting started with lustre or you know you don't - // need the runtime to manage any side effects. - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil) - - Nil -} - -fn init(_) { - [] -} - -fn update(history, msg) { - case msg { - "reset" -> [] - _ -> [msg, ..history] - } -} - -fn view(history) { - let on_custom_click = event.on("custom-click", function.constant(Ok("click"))) - - div( - [], - [ - button([event.on_click("reset")], [text("Reset")]), - ol([], list.map(history, fn(msg) { li([], [text(msg)]) })), - element( - "custom-counter", - [on_custom_click, attribute.property("count", list.length(history))], - [ol([], list.map(history, fn(msg) { li([], [text(msg)]) }))], - ), - ], - ) -} - -// COUNTER --------------------------------------------------------------------- - -fn counter_init() { - #(0, effect.none()) -} - -type CounterMsg { - GotCount(Int) - Clicked -} - -fn counter_update(count, msg) { - case msg { - GotCount(count) -> #(count, effect.none()) - Clicked -> #(count, event.emit("custom-click", Nil)) - } -} - -fn counter_view(count) { - div( - [], - [ - button([event.on_click(Clicked)], [text("Click me!")]), - p([], [text("Count: "), text(int.to_string(count))]), - slot([]), - ], - ) -} diff --git a/lib/test/examples/components.html b/lib/test/examples/components.html deleted file mode 100644 index f4b4530..0000000 --- a/lib/test/examples/components.html +++ /dev/null @@ -1,17 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>lustre | components</title> - - <script type="module"> - import { main } from "../../build/dev/javascript/lustre/examples/components.mjs"; - - document.addEventListener("DOMContentLoaded", main); - </script> - </head> - <body> - <div data-lustre-app></div> - </body> -</html> diff --git a/lib/test/examples/counter.gleam b/lib/test/examples/counter.gleam deleted file mode 100644 index 4faf00c..0000000 --- a/lib/test/examples/counter.gleam +++ /dev/null @@ -1,56 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/int -import lustre -import lustre/element.{Element, text} -import lustre/element/html.{button, div, p} -import lustre/event - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - // A `simple` lustre application doesn't produce `Effect`s. These are best to - // start with if you're just getting started with lustre or you know you don't - // need the runtime to manage any side effects. - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -pub type Model = - Int - -pub fn init(_) -> Model { - 0 -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - Incr - Decr - Reset -} - -pub fn update(model: Model, msg: Msg) -> Model { - case msg { - Incr -> model + 1 - Decr -> model - 1 - Reset -> 0 - } -} - -// VIEW ------------------------------------------------------------------------ - -pub fn view(model: Model) -> Element(Msg) { - div( - [], - [ - button([event.on_click(Incr)], [text("+")]), - button([event.on_click(Decr)], [text("-")]), - button([event.on_click(Reset)], [text("Reset")]), - p([], [text(int.to_string(model))]), - ], - ) -} diff --git a/lib/test/examples/counter.html b/lib/test/examples/counter.html deleted file mode 100644 index 2b120fd..0000000 --- a/lib/test/examples/counter.html +++ /dev/null @@ -1,17 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>lustre | counter</title> - - <script type="module"> - import { main } from "../../build/dev/javascript/lustre/examples/counter.mjs"; - - document.addEventListener("DOMContentLoaded", main); - </script> - </head> - <body> - <div data-lustre-app></div> - </body> -</html> diff --git a/lib/test/examples/index.html b/lib/test/examples/index.html deleted file mode 100644 index 70d4196..0000000 --- a/lib/test/examples/index.html +++ /dev/null @@ -1,27 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>lustre | examples</title> - </head> - <body> - <menu> - <li> - <a href="input.html">input</a> - </li> - <li> - <a href="counter.html">counter</a> - </li> - <li> - <a href="nested.html">nested</a> - </li> - <li> - <a href="svg.html">svg</a> - </li> - <li> - <a href="components.html">components</a> - </li> - </menu> - </body> -</html> diff --git a/lib/test/examples/input.gleam b/lib/test/examples/input.gleam deleted file mode 100644 index ff4d794..0000000 --- a/lib/test/examples/input.gleam +++ /dev/null @@ -1,132 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/dynamic -import gleam/string -import lustre -import lustre/attribute.{attribute} -import lustre/element.{Element, text} -import lustre/element/html.{div, input, label, pre} -import lustre/event - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - // A `simple` lustre application doesn't produce `Effect`s. These are best to - // start with if you're just getting started with lustre or you know you don't - // need the runtime to manage any side effects. - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil) - - Nil -} - -// MODEL ----------------------------------------------------------------------- - -type Model { - Model(email: String, password: String, remember_me: Bool) -} - -fn init(_) -> Model { - Model(email: "", password: "", remember_me: False) -} - -// UPDATE ---------------------------------------------------------------------- - -type Msg { - Typed(Input, String) - Toggled(Control, Bool) -} - -type Input { - Email - Password -} - -type Control { - RememberMe -} - -fn update(model: Model, msg: Msg) -> Model { - case msg { - Typed(Email, email) -> Model(..model, email: email) - Typed(Password, password) -> Model(..model, password: password) - Toggled(RememberMe, remember_me) -> Model(..model, remember_me: remember_me) - } -} - -// RENDER ---------------------------------------------------------------------- - -fn view(model: Model) -> Element(Msg) { - div( - [attribute.class("container")], - [ - card([ - email_input(model.email), - password_input(model.password), - remember_checkbox(model.remember_me), - pre( - [attribute.class("debug")], - [ - string.inspect(model) - |> string.replace("(", "(\n ") - |> string.replace(", ", ",\n ") - |> string.replace(")", "\n)") - |> text, - ], - ), - ]), - ], - ) -} - -fn card(content: List(Element(a))) -> Element(a) { - div([attribute.class("card")], [div([], content)]) -} - -fn email_input(value: String) -> Element(Msg) { - render_input(Email, "email", "email-input", value, "Email address") -} - -fn password_input(value: String) -> Element(Msg) { - render_input(Password, "password", "password-input", value, "Password") -} - -fn render_input( - field: Input, - type_: String, - id: String, - value: String, - label_: String, -) -> Element(Msg) { - div( - [attribute.class("input")], - [ - label([attribute.for(id)], [text(label_)]), - input([ - attribute.id(id), - attribute.name(id), - attribute.type_(type_), - attribute.required(True), - attribute.value(dynamic.from(value)), - event.on_input(fn(value) { Typed(field, value) }), - ]), - ], - ) -} - -fn remember_checkbox(checked: Bool) -> Element(Msg) { - div( - [attribute.class("flex items-center")], - [ - input([ - attribute.id("remember-me"), - attribute.name("remember-me"), - attribute.type_("checkbox"), - attribute.checked(checked), - attribute.class("checkbox"), - event.on_click(Toggled(RememberMe, !checked)), - ]), - label([attribute.for("remember-me")], [text("Remember me")]), - ], - ) -} diff --git a/lib/test/examples/input.html b/lib/test/examples/input.html deleted file mode 100644 index 3bd6463..0000000 --- a/lib/test/examples/input.html +++ /dev/null @@ -1,54 +0,0 @@ -<!DOCTYPE html> -<html lang="en" class="h-full bg-gray-50"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>lustre | forms</title> - - <script src="https://cdn.tailwindcss.com"></script> - <style type="text/tailwindcss"> - @layer components { - .container { - @apply flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8; - } - - .card { - @apply mt-10 sm:mx-auto sm:w-full sm:max-w-[480px]; - } - - .card > div { - @apply bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12 space-y-6; - } - - .debug { - @apply text-sm text-gray-400; - } - - .input label { - @apply block text-sm font-medium leading-6 text-gray-900; - } - - .input input { - @apply block w-full rounded-md border-0 p-1.5 mt-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-pink-600 sm:text-sm sm:leading-6; - } - - .checkbox { - @apply h-4 w-4 rounded border-gray-300 text-pink-600 focus:ring-pink-600; - } - - .checkbox + label { - @apply ml-3 block text-sm leading-6 text-gray-900; - } - } - </style> - - <script type="module"> - import { main } from "../../build/dev/javascript/lustre/examples/input.mjs"; - - document.addEventListener("DOMContentLoaded", main); - </script> - </head> - <body class="h-full"> - <div data-lustre-app></div> - </body> -</html> diff --git a/lib/test/examples/nested.gleam b/lib/test/examples/nested.gleam deleted file mode 100644 index 91c2da4..0000000 --- a/lib/test/examples/nested.gleam +++ /dev/null @@ -1,57 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import examples/counter -import gleam/list -import gleam/map.{Map} -import gleam/pair -import lustre -import lustre/element.{Element} -import lustre/element/html.{div} - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - // A `simple` lustre application doesn't produce `Effect`s. These are best to - // start with if you're just getting started with lustre or you know you don't - // need the runtime to manage any side effects. - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil) - - Nil -} - -// MODEL ----------------------------------------------------------------------- - -type Model = - Map(Int, counter.Model) - -fn init(_) -> Model { - use counters, id <- list.fold(list.range(1, 10), map.new()) - - map.insert(counters, id, counter.init(Nil)) -} - -// UPDATE ---------------------------------------------------------------------- - -type Msg = - #(Int, counter.Msg) - -fn update(model: Model, msg: Msg) -> Model { - let #(id, counter_msg) = msg - let assert Ok(counter) = map.get(model, id) - - map.insert(model, id, counter.update(counter, counter_msg)) -} - -// RENDER ---------------------------------------------------------------------- - -fn view(model: Model) -> Element(Msg) { - let counters = { - use rest, id, counter <- map.fold(model, []) - let el = element.map(counter.view(counter), pair.new(id, _)) - - [el, ..rest] - } - - div([], counters) -} diff --git a/lib/test/examples/nested.html b/lib/test/examples/nested.html deleted file mode 100644 index 420b159..0000000 --- a/lib/test/examples/nested.html +++ /dev/null @@ -1,17 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>lustre | nested</title> - - <script type="module"> - import { main } from "../../build/dev/javascript/lustre/examples/nested.mjs"; - - document.addEventListener("DOMContentLoaded", main); - </script> - </head> - <body> - <div data-lustre-app></div> - </body> -</html> diff --git a/lib/test/examples/svg.gleam b/lib/test/examples/svg.gleam deleted file mode 100644 index e895f1a..0000000 --- a/lib/test/examples/svg.gleam +++ /dev/null @@ -1,107 +0,0 @@ -// IMPORTS --------------------------------------------------------------------- - -import gleam/int -import lustre -import lustre/attribute.{attribute} -import lustre/element.{Element, text} -import lustre/element/html.{button, div, p, svg} -import lustre/element/svg.{path} -import lustre/event - -// MAIN ------------------------------------------------------------------------ - -pub fn main() { - // A `simple` lustre application doesn't produce `Effect`s. These are best to - // start with if you're just getting started with lustre or you know you don't - // need the runtime to manage any side effects. - let app = lustre.simple(init, update, view) - let assert Ok(_) = lustre.start(app, "[data-lustre-app]", Nil) -} - -// MODEL ----------------------------------------------------------------------- - -pub type Model = - Int - -pub fn init(_) -> Model { - 0 -} - -// UPDATE ---------------------------------------------------------------------- - -pub opaque type Msg { - Incr - Decr - Reset -} - -pub fn update(model: Model, msg: Msg) -> Model { - case msg { - Incr -> model + 1 - Decr -> model - 1 - Reset -> 0 - } -} - -// VIEW ------------------------------------------------------------------------ - -pub fn view(model: Model) -> Element(Msg) { - div( - [], - [ - button( - [event.on_click(Incr)], - [plus([attribute.style([#("color", "red")])])], - ), - button([event.on_click(Decr)], [minus([])]), - button([event.on_click(Reset)], [text("Reset")]), - p([], [text(int.to_string(model))]), - ], - ) -} - -fn plus(attrs) { - svg( - [ - attribute("width", "15"), - attribute("height", "15"), - attribute("viewBox", "0 0 15 15"), - attribute("fill", "none"), - ..attrs - ], - [ - path([ - attribute( - "d", - "M8 2.75C8 2.47386 7.77614 2.25 7.5 2.25C7.22386 2.25 7 2.47386 7 2.75V7H2.75C2.47386 7 2.25 7.22386 2.25 7.5C2.25 7.77614 2.47386 8 2.75 8H7V12.25C7 12.5261 7.22386 12.75 7.5 12.75C7.77614 12.75 8 12.5261 8 12.25V8H12.25C12.5261 8 12.75 7.77614 12.75 7.5C12.75 7.22386 12.5261 7 12.25 7H8V2.75Z", - ), - attribute("fill", "currentColor"), - attribute("fill-rule", "evenodd"), - attribute("clip-rule", "evenodd"), - ]), - ], - ) -} - -fn minus(attrs) { - svg( - [ - attribute("width", "15"), - attribute("height", "15"), - attribute("viewBox", "0 0 15 15"), - attribute("fill", "none"), - ..attrs - ], - [ - path([ - attribute( - "d", - "M2.25 7.5C2.25 7.22386 2.47386 7 2.75 7H12.25C12.5261 7 12.75 7.22386 12.75 7.5C12.75 7.77614 12.5261 8 12.25 8H2.75C2.47386 8 2.25 7.77614 2.25 7.5Z", - ), - attribute("fill", "currentColor"), - attribute("fill-rule", "evenodd"), - attribute("clip-rule", "evenodd"), - ]), - ], - ) -} diff --git a/lib/test/examples/svg.html b/lib/test/examples/svg.html deleted file mode 100644 index 12af526..0000000 --- a/lib/test/examples/svg.html +++ /dev/null @@ -1,17 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>lustre | svg</title> - - <script type="module"> - import { main } from "../../build/dev/javascript/lustre/examples/svg.mjs"; - - document.addEventListener("DOMContentLoaded", main); - </script> - </head> - <body> - <div data-lustre-app></div> - </body> -</html> |