diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2022-03-20 10:58:23 +0000 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2022-03-20 10:58:23 +0000 |
commit | 170a249f1bc5caae55ae5a3469f0fc6beef63568 (patch) | |
tree | 13a65659aa2aef065d1acf59af20a109df83eaa4 | |
parent | 06e3827589771da31bf033d304da4ba3a40c9cde (diff) | |
download | lustre-170a249f1bc5caae55ae5a3469f0fc6beef63568.tar.gz lustre-170a249f1bc5caae55ae5a3469f0fc6beef63568.zip |
:construction: Morphdom is dead, long live React.
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | package-lock.json | 116 | ||||
-rw-r--r-- | package.json | 9 | ||||
-rw-r--r-- | src/index.html | 17 | ||||
-rw-r--r-- | src/js/test.js | 5 | ||||
-rw-r--r-- | src/lustre.gleam | 46 | ||||
-rw-r--r-- | src/lustre/attribute.gleam | 21 | ||||
-rw-r--r-- | src/lustre/dom/html.ffi.mjs | 14 | ||||
-rw-r--r-- | src/lustre/dom/html.gleam | 54 | ||||
-rw-r--r-- | src/lustre/dom/html/attr.ffi.mjs | 6 | ||||
-rw-r--r-- | src/lustre/dom/html/attr.gleam | 61 | ||||
-rw-r--r-- | src/lustre/dom/svg.gleam | 32 | ||||
-rw-r--r-- | src/lustre/dom/svg/attr.gleam | 92 | ||||
-rw-r--r-- | src/lustre/element.gleam | 493 | ||||
-rw-r--r-- | src/lustre/event.gleam | 24 | ||||
-rw-r--r-- | src/lustre/ffi.mjs | 65 | ||||
-rw-r--r-- | src/lustre/test.gleam | 11 |
17 files changed, 764 insertions, 326 deletions
@@ -1,19 +1,15 @@ -# gleam_lustre +# Lustre -A Gleam project +> A playground for building create web apps – powered by Gleam! -## Quick start +## Development -```sh -gleam run # Run the project -gleam test # Run the tests -gleam shell # Run an Erlang shell -``` - -## Installation +First, make sure you have both Gleam and Node.js installed, then: -If available on Hex this package can be added to your Gleam project. - -```sh -gleam add gleam_lustre +```bash +npm i +npm start ``` + +This sets up `chokidar` to watch our gleam source code and runs the compiler +whenever we make a change. diff --git a/package-lock.json b/package-lock.json index 6dc6798..ac83f5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,10 +5,12 @@ "requires": true, "packages": { "": { + "name": "gleam-lustre", "version": "1.0.0", - "license": "ISC", + "license": "MIT", "dependencies": { - "morphdom": "^2.6.1" + "react": "^17.0.2", + "react-dom": "^17.0.2" }, "devDependencies": { "chokidar-cli": "^3.0.0", @@ -2172,8 +2174,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -2274,6 +2275,17 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -2286,11 +2298,6 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "node_modules/morphdom": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz", - "integrity": "sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA==" - }, "node_modules/msgpackr": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.5.4.tgz", @@ -2392,6 +2399,14 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "dev": true }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ordered-binary": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.2.4.tgz", @@ -3019,6 +3034,31 @@ "node": ">= 0.6.0" } }, + "node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + }, + "peerDependencies": { + "react": "17.0.2" + } + }, "node_modules/react-refresh": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", @@ -3102,6 +3142,15 @@ } ] }, + "node_modules/scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -4883,8 +4932,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -4972,6 +5020,14 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -4984,11 +5040,6 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "morphdom": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz", - "integrity": "sha512-Y8YRbAEP3eKykroIBWrjcfMw7mmwJfjhqdpSvoqinu8Y702nAwikpXcNFDiIkyvfCLxLM9Wu95RZqo4a9jFBaA==" - }, "msgpackr": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.5.4.tgz", @@ -5066,6 +5117,11 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "dev": true }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "ordered-binary": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.2.4.tgz", @@ -5474,6 +5530,25 @@ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", "dev": true }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, "react-refresh": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", @@ -5528,6 +5603,15 @@ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", diff --git a/package.json b/package.json index a89771a..7d66ddd 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,6 @@ }, "author": "", "license": "MIT", - "dependencies": { - "morphdom": "^2.6.1" - }, "devDependencies": { "chokidar-cli": "^3.0.0", "concurrently": "^7.0.0", @@ -27,5 +24,9 @@ }, "alias": { "lustre": "./build/dev/javascript/lustre/dist/lustre" + }, + "dependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2" } -}
\ No newline at end of file +} diff --git a/src/index.html b/src/index.html deleted file mode 100644 index e807f2a..0000000 --- a/src/index.html +++ /dev/null @@ -1,17 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - -<head> - <meta charset="UTF-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Document</title> - - <script type="module" src="./js/test.js"></script> -</head> - -<body> - -</body> - -</html>
\ No newline at end of file diff --git a/src/js/test.js b/src/js/test.js deleted file mode 100644 index ef7923a..0000000 --- a/src/js/test.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as Lustre from 'lustre/test.mjs' - -document.documentElement.append( - Lustre.html_test() -)
\ No newline at end of file diff --git a/src/lustre.gleam b/src/lustre.gleam new file mode 100644 index 0000000..2058da6 --- /dev/null +++ b/src/lustre.gleam @@ -0,0 +1,46 @@ +//// + + +// IMPORTS --------------------------------------------------------------------- +import lustre/element +import lustre/attribute + + +// TYPES ----------------------------------------------------------------------- +/// +pub opaque type Program(state, action) { + Program( + init: state, + update: Update(state, action), + view: View(state, action) + ) +} + + +/// +pub type Element(action) = element.Element(action) +/// +pub type Attribute(action) = attribute.Attribute(action) + + +/// +type Update(state, action) = fn (state, action) -> state +type View(state, action) = fn (state) -> Element(action) + + +// CONSTRUCTORS ---------------------------------------------------------------- +/// +pub fn create (init: state, update: Update(state, action), view: View(state, action)) -> Program(state, action) { + Program(init, update, view) +} + + +/// +pub external fn start (program: Program(state, action), selector: String) -> Nil + = "./lustre/ffi.mjs" "mount" + + +// CONVERSIONS ----------------------------------------------------------------- +/// +pub external fn to_element (program: Program(state, action)) -> Element(a) + = "./lustre/ffi.mjs" "Program" diff --git a/src/lustre/attribute.gleam b/src/lustre/attribute.gleam new file mode 100644 index 0000000..c294b00 --- /dev/null +++ b/src/lustre/attribute.gleam @@ -0,0 +1,21 @@ +import gleam/dynamic.{ Dynamic } + +pub opaque type Attribute(action) { + Attribute(name: String, value: String) + Property(name: String, value: Dynamic) + Event(on: String, handler: fn (Dynamic) -> action) +} + +// CONSTRUCTORS ---------------------------------------------------------------- + +pub fn attribute (name: String, value: String) -> Attribute(action) { + Attribute(name, value) +} + +pub fn property (name: String, value: Dynamic) -> Attribute(action) { + Property(name, value) +} + +pub fn event (on: String, handler: fn (Dynamic) -> action) -> Attribute(action) { + Event(on, handler) +}
\ No newline at end of file diff --git a/src/lustre/dom/html.ffi.mjs b/src/lustre/dom/html.ffi.mjs deleted file mode 100644 index 2cbe58d..0000000 --- a/src/lustre/dom/html.ffi.mjs +++ /dev/null @@ -1,14 +0,0 @@ -export const createNode = (tag, namespace, attrs, children) => { - const el = namespace == '' - ? document.createElement(tag) - : document.createElementNS(namespace, tag) - - for (const attr of attrs) el.setAttributeNode(attr) - for (const node of children) el.append(node) - - return el -} - -export const createText = (content) => { - return document.createTextNode(content) -}
\ No newline at end of file diff --git a/src/lustre/dom/html.gleam b/src/lustre/dom/html.gleam deleted file mode 100644 index 12ac25a..0000000 --- a/src/lustre/dom/html.gleam +++ /dev/null @@ -1,54 +0,0 @@ -import lustre/dom/html/attr.{Attr} - -// TYPES ----------------------------------------------------------------------- - -pub external type Html - -// GENERIC CONSTRUCTORS -------------------------------------------------------- - -external fn create_node (String, String, List(Attr), List(Html)) -> Html = "./html.ffi.mjs" "createNode" - -external fn create_text (String) -> Html = - "./html.ffi.mjs" "createText" - -pub fn node (tag: String, attrs: List(Attr), children: List(Html)) -> Html { - create_node(tag, "", attrs, children) -} - -pub fn node_ns (tag: String, namespace: String, attrs: List(Attr), children: List(Html)) -> Html { - create_node(tag, namespace, attrs, children) -} - -pub fn text (content: String) -> Html { - create_text(content) -} - -// COMMON CONSTRUCTORS --------------------------------------------------------- - -pub fn div (attrs: List(Attr), children: List(Html)) -> Html { - node("div", attrs, children) -} - -pub fn p (attrs: List(Attr), children: List(Html)) -> Html { - node("p", attrs, children) -} - -pub fn span (attrs: List(Attr), children: List(Html)) -> Html { - node("span", attrs, children) -} - -pub fn button (attrs: List(Attr), children: List(Html)) -> Html { - node("button", attrs, children) -} - -// INPUT CONSTRUCTORS ---------------------------------------------------------- - -pub fn input (attrs: List(Attr)) -> Html { - node("input", attrs, []) -} - -// GRAPHICS CONSTRUCTORS ------------------------------------------------------- - -pub fn img (attrs: List(Attr)) -> Html { - node("img", attrs, []) -}
\ No newline at end of file diff --git a/src/lustre/dom/html/attr.ffi.mjs b/src/lustre/dom/html/attr.ffi.mjs deleted file mode 100644 index b75c37a..0000000 --- a/src/lustre/dom/html/attr.ffi.mjs +++ /dev/null @@ -1,6 +0,0 @@ -export const createAttr = (name, value) => { - const attr = document.createAttribute(name) - attr.value = value - - return attr -}
\ No newline at end of file diff --git a/src/lustre/dom/html/attr.gleam b/src/lustre/dom/html/attr.gleam deleted file mode 100644 index ac86cdf..0000000 --- a/src/lustre/dom/html/attr.gleam +++ /dev/null @@ -1,61 +0,0 @@ -import gleam/float -import gleam/int - -// TYPES ----------------------------------------------------------------------- - -pub external type Attr - -// GENERIC CONSTRUCTORS -------------------------------------------------------- - -external fn create_attr (String, String) -> Attr = - "./attr.ffi.mjs" "createAttr" - -pub fn from_string (name: String, val: String) -> Attr { - create_attr(name, val) -} - -pub fn from_float (name: String, val: Float) -> Attr { - create_attr(name, float.to_string(val)) -} - -pub fn from_int (name: String, val: Int) -> Attr { - create_attr(name, int.to_string(val)) -} - -pub fn from_bool (name: String, val: Bool) -> Attr { - create_attr(name, case val { - True -> - "true" - - False -> - "false" - }) -} - -// COMMON ATTRIBUTES ----------------------------------------------------------- - -pub fn style (styles: String) -> Attr { - from_string("style", styles) -} - -pub fn class (class: String) -> Attr { - from_string("class", class) -} - -pub fn id (id: String) -> Attr { - from_string("id", id) -} - -// INPUT ATTRIBUTES ------------------------------------------------------------ - -pub fn type_ (type_: String) -> Attr { - from_string("type", type_) -} - -pub fn value (value: String) -> Attr { - from_string("value", value) -} - -pub fn checked (checked: Bool) -> Attr { - from_bool("checked", checked) -} diff --git a/src/lustre/dom/svg.gleam b/src/lustre/dom/svg.gleam deleted file mode 100644 index e356e42..0000000 --- a/src/lustre/dom/svg.gleam +++ /dev/null @@ -1,32 +0,0 @@ -import lustre/dom/html.{Html} -import lustre/dom/html/attr.{Attr} - -// CONSTANTS ------------------------------------------------------------------- - -const svg_namespace = "http://www.w3.org/2000/svg" - -// COMMON CONSTRUCTORS --------------------------------------------------------- - -pub fn svg (attrs: List(Attr), children: List(Html)) -> Html { - html.node_ns("svg", svg_namespace, attrs, children) -} - -pub fn rect (attrs: List(Attr), children: List(Html)) -> Html { - html.node_ns("rect", svg_namespace, attrs, children) -} - -pub fn circle (attrs: List(Attr), children: List(Html)) -> Html { - html.node_ns("circle", svg_namespace, attrs, children) -} - -pub fn ellipse (attrs: List(Attr), children: List(Html)) -> Html { - html.node_ns("ellipse", svg_namespace, attrs, children) -} - -pub fn line (attrs: List(Attr), children: List(Html)) -> Html { - html.node_ns("line", svg_namespace, attrs, children) -} - -pub fn text (attrs: List(Attr), children: List(Html)) -> Html { - html.node_ns("text", svg_namespace, attrs, children) -} diff --git a/src/lustre/dom/svg/attr.gleam b/src/lustre/dom/svg/attr.gleam deleted file mode 100644 index c2ce5cc..0000000 --- a/src/lustre/dom/svg/attr.gleam +++ /dev/null @@ -1,92 +0,0 @@ -import gleam/float -import gleam/list -import gleam/string -import lustre/dom/html/attr - -// TYPES ----------------------------------------------------------------------- - -pub type Attr = attr.Attr - -// COMMON CONSTRUCTORS --------------------------------------------------------- - -pub fn x (x: Float) -> Attr { - attr.from_float("x", x) -} - -pub fn x1 (x1: Float) -> Attr { - attr.from_float("x1", x1) -} - -pub fn x2 (x2: Float) -> Attr { - attr.from_float("x2", x2) -} - -pub fn y (y: Float) -> Attr { - attr.from_float("y", y) -} - -pub fn y1 (y1: Float) -> Attr { - attr.from_float("y1", y1) -} - -pub fn y2 (y2: Float) -> Attr { - attr.from_float("y2", y2) -} - -pub fn width (width: Float) -> Attr { - attr.from_float("width", width) -} - -pub fn height (height: Float) -> Attr { - attr.from_float("height", height) -} - -pub fn viewbox (min_x: Float, min_y: Float, width: Float, height: Float) -> Attr { - attr.from_string("viewbox", [ min_x, min_y, width, height ] |> list.map(float.to_string) |> string.join(" ")) -} - -// STYLING CONSTRUCTORS -------------------------------------------------------- - -pub fn rgb (r: Float, g: Float, b: Float) -> String { - rgba(r, g, b, 1.0) -} - -pub fn rgba (r: Float, g: Float, b: Float, a: Float) -> String { - let r = clamp(r, between: 0.0, and: 255.0) |> float.to_string - let g = clamp(g, between: 0.0, and: 255.0) |> float.to_string - let b = clamp(b, between: 0.0, and: 255.0) |> float.to_string - let a = clamp(a, between: 0.0, and: 1.0) |> float.to_string - - string.concat([ "rgba(", r, ",", g, ",", b, ",", a, ")" ]) -} - -pub fn fill (color: String) -> Attr { - attr.from_string("fill", color) -} - -pub fn stroke (color: String) -> Attr { - attr.from_string("stroke", color) -} - -pub fn stroke_width (width: Float) -> Attr { - attr.from_float("stroke-width", width) -} - -pub fn opacity (opacity: Float) -> Attr { - attr.from_float("opacity", clamp(opacity, between: 0.0, and: 1.0)) -} - -// UTILS ----------------------------------------------------------------------- - -fn clamp (val: Float, between min: Float, and max: Float) -> Float { - case val { - _ if val <. min -> - min - - _else if val >. max -> - max - - _else -> - val - } -}
\ No newline at end of file diff --git a/src/lustre/element.gleam b/src/lustre/element.gleam new file mode 100644 index 0000000..aa0d605 --- /dev/null +++ b/src/lustre/element.gleam @@ -0,0 +1,493 @@ +//// + +// IMPORTS --------------------------------------------------------------------- +import lustre/attribute.{ Attribute, attribute } + +// TYPES ----------------------------------------------------------------------- + +/// +pub external type Element(action) + +// CONSTRUCTORS ---------------------------------------------------------------- + +/// +pub external fn element (tag: String, attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) + = "./ffi.mjs" "element" + +pub external fn fragment (children: List(Element(action))) -> Element(action) + = "./ffi.mjs" "fragment" + +pub external fn text (content: String) -> Element(action) + = "./ffi.mjs" "text" + + +// CONSTRUCTING NODES ---------------------------------------------------------- +// This list and grouping of nodes has been taken from the MDN reference at: +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element + +// MAIN ROOT: +pub fn html (attributes: List(Attribute(action)), head: Element(action), body: Element(action)) -> Element(action) { + element("html", attributes, [ head, body ]) +} + + +// DOCUMENT METADATA: +pub fn base (attributes: List(Attribute(action))) -> Element(action) { + element("base", attributes, []) +} + +pub fn head (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("head", attributes, children) +} + +pub fn meta (attributes: List(Attribute(action))) -> Element(action) { + element("meta", attributes, []) +} + +pub fn style (attributes: List(Attribute(action)), css: String) -> Element(action) { + element("style", attributes, [ text(css) ]) +} + +pub fn title (attributes: List(Attribute(action)), name: String) -> Element(action) { + element("title", attributes, [ text(name) ]) +} + +// SECTIONING ROOT: +pub fn body (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("body", attributes, children) +} + + +// CONTENT SECTIONING: +pub fn address (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("address", attributes, children) +} + +pub fn article (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("article", attributes, children) +} + +pub fn aside (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("aside", attributes, children) +} + +pub fn footer (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("footer", attributes, children) +} + +pub fn header (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("header", attributes, children) +} + +pub fn h1 (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("h1", attributes, children) +} + +pub fn h2 (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("h2", attributes, children) +} + +pub fn h3 (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("h3", attributes, children) +} + +pub fn h4 (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("h4", attributes, children) +} + +pub fn h5 (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("h5", attributes, children) +} + +pub fn h6 (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("h6", attributes, children) +} + +pub fn main (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("main", attributes, children) +} + +pub fn nav (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("nav", attributes, children) +} + +pub fn section (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("section", attributes, children) +} + + +// TEXT CONTENT: +pub fn blockquote (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("blockquote", attributes, children) +} + +pub fn dd (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("dd", attributes, children) +} + +pub fn div (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("div", attributes, children) +} + +pub fn dl (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("dl", attributes, children) +} + +pub fn dt (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("dt", attributes, children) +} + +pub fn figcaption (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("figcaption", attributes, children) +} + +pub fn figure (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("figure", attributes, children) +} + +pub fn hr (attributes: List(Attribute(action))) -> Element(action) { + element("hr", attributes, []) +} + +pub fn li (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("li", attributes, children) +} + +pub fn menu (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("menu", attributes, children) +} + +pub fn ol (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("ol", attributes, children) +} + +pub fn p (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("p", attributes, children) +} + +pub fn pre (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("pre", attributes, children) +} + +pub fn ul (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("ul", attributes, children) +} + + +// INLINE TEXT SEMANTICS: +pub fn a (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("a", attributes, children) +} + +pub fn abbr (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("abbr", attributes, children) +} + +pub fn b (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("b", attributes, children) +} + +pub fn bdi (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("bdi", attributes, children) +} + +pub fn bdo (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("bdo", attributes, children) +} + +pub fn br (attributes: List(Attribute(action))) -> Element(action) { + element("br", attributes, []) +} + +pub fn cite (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("cite", attributes, children) +} + +pub fn code (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("code", attributes, children) +} + +pub fn dfn (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("dfn", attributes, children) +} + +pub fn em (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("em", attributes, children) +} + +pub fn i (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("i", attributes, children) +} + +pub fn kbd (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("kbd", attributes, children) +} + +pub fn mark (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("mark", attributes, children) +} + +pub fn rp (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("rp", attributes, children) +} + +pub fn rt (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("rt", attributes, children) +} + +pub fn ruby (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("ruby", attributes, children) +} + +pub fn s (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("s", attributes, children) +} + +pub fn samp (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("samp", attributes, children) +} + +pub fn small (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("small", attributes, children) +} + +pub fn span (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("span", attributes, children) +} + +pub fn strong (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("strong", attributes, children) +} + +pub fn sub (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("sub", attributes, children) +} + +pub fn sup (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("sup", attributes, children) +} + +pub fn time (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("time", attributes, children) +} + +pub fn u (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("u", attributes, children) +} + +pub fn var_ (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("var", attributes, children) +} + +pub fn wbr (attributes: List(Attribute(action))) -> Element(action) { + element("wbr", attributes, []) +} + + +// IMAGE AND MULTIMEDIA: +pub fn area (attributes: List(Attribute(action))) -> Element(action) { + element("area", attributes, []) +} + +pub fn audio (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("audio", attributes, children) +} + +pub fn img (attributes: List(Attribute(action))) -> Element(action) { + element("img", attributes, []) +} + +pub fn map (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("map", attributes, children) +} + +pub fn track (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("track", attributes, children) +} + +pub fn video (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("video", attributes, children) +} + + +// EMBEDDED CONTENT: +pub fn embed (attributes: List(Attribute(action))) -> Element(action) { + element("embed", attributes, []) +} + +pub fn iframe (attributes: List(Attribute(action))) -> Element(action) { + element("iframe", attributes, []) +} + +pub fn object (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("object", attributes, children) +} + +pub fn param (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("param", attributes, children) +} + +pub fn picture (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("picture", attributes, children) +} + +pub fn portal (attributes: List(Attribute(action))) -> Element(action) { + element("portal", attributes, []) +} + +pub fn source (attributes: List(Attribute(action))) -> Element(action) { + element("source", attributes, []) +} + + +// SVG AND MATHML: +pub fn svg (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("svg", [attribute("xmlns", "http://www.w3.org/2000/svg"), ..attributes], children) +} + +pub fn mathml (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("mathml", [attribute("xmlns", "http://www.w3.org/1998/Math/MathML"), ..attributes], children) +} + +// SCRIPTING: +pub fn canvas (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("canvas", attributes, children) +} + +pub fn noscript (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("noscript", attributes, children) +} + + +// DEMARCATING EDITS: +pub fn del (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("del", attributes, children) +} + +pub fn ins (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("ins", attributes, children) +} + + +// TABLE CONTENT: +pub fn caption (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("caption", attributes, children) +} + +pub fn col (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("col", attributes, children) +} + +pub fn colgroup (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("colgroup", attributes, children) +} + +pub fn table (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("table", attributes, children) +} + +pub fn tbody (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("tbody", attributes, children) +} + +pub fn td (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("td", attributes, children) +} + +pub fn tfoot (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("tfoot", attributes, children) +} + +pub fn th (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("th", attributes, children) +} + +pub fn thead (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("thead", attributes, children) +} + +pub fn tr (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("tr", attributes, children) +} + + +// FORMS: +pub fn button (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("button", attributes, children) +} + +pub fn datalist (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("datalist", attributes, children) +} + +pub fn fieldset (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("fieldset", attributes, children) +} + +pub fn form (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("form", attributes, children) +} + +pub fn input (attributes: List(Attribute(action))) -> Element(action) { + element("input", attributes, []) +} + +pub fn label (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("label", attributes, children) +} + +pub fn legend (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("legend", attributes, children) +} + +pub fn meter (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("meter", attributes, children) +} + +pub fn optgroup (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("optgroup", attributes, children) +} + +pub fn option (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("option", attributes, children) +} + +pub fn output (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("output", attributes, children) +} + +pub fn progress (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("progress", attributes, children) +} + +pub fn select (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("select", attributes, children) +} + +pub fn textarea (attributes: List(Attribute(action))) -> Element(action) { + element("textarea", attributes, []) +} + + +// INTERACTIVE ELEMENTS: +pub fn details (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("details", attributes, children) +} + +pub fn dialog (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("dialog", attributes, children) +} + +pub fn summary (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("summary", attributes, children) +} + + +// WEB COMPONENTS: +pub fn slot (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("slot", attributes, children) +} + +pub fn template (attributes: List(Attribute(action)), children: List(Element(action))) -> Element(action) { + element("template", attributes, children) +} diff --git a/src/lustre/event.gleam b/src/lustre/event.gleam new file mode 100644 index 0000000..23c9f49 --- /dev/null +++ b/src/lustre/event.gleam @@ -0,0 +1,24 @@ +import gleam/dynamic.{ Dynamic } +import lustre/attribute.{ Attribute } + +pub fn on (event: String, handler: fn (Dynamic) -> action) -> Attribute(action) { + attribute.event(event, handler) +} + +// + +pub fn on_click (handler: action) -> Attribute(action) { + on("onClick", fn (_) { handler }) +} + +pub fn on_input (handler: fn (String) -> action) -> Attribute(action) { + let decoder = dynamic.field("target", dynamic.field("value", dynamic.string)) + + on("onInput", fn (e) { + // If this fails then there's probably some sort of hideous browser bug + // that is way beyond the concern of our Lustre apps! + assert Ok(value) = decoder(e) + + handler(value) + }) +}
\ No newline at end of file diff --git a/src/lustre/ffi.mjs b/src/lustre/ffi.mjs new file mode 100644 index 0000000..9d27c5e --- /dev/null +++ b/src/lustre/ffi.mjs @@ -0,0 +1,65 @@ +import * as React from 'react' +import * as ReactDOM from 'react-dom' + +export const Program = ({ init, update, view }) => () => { + return React.createElement(() => { + const [state, dispatch] = React.useReducer(update, init) + + return view(state)(dispatch) + }) +} + +export const mount = (program, selector) => { + const root = document.querySelector(selector) + const App = Program(program) + + ReactDOM.render(App(), root) +} + +// ----------------------------------------------------------------------------- + +export const element = (tag, attributes, children) => (dispatch) => { + const props = attributes.toArray().map(attr => { + switch (attr.constructor.name) { + case "Attribute": + case "Property": + return [attr.name, attr.value] + + case "Event": + return [attr.on, (e) => dispatch(attr.handler(e))] + + default: + throw new Error(`Unknown attribute type: ${attr.constructor.name}`) + } + }) + + return React.createElement(tag, + // React expects an object of "props" where the keys are attribute names + // like "class" or "onClick" and their corresponding values. Lustre's API + // works with lists, though. + // + // The snippet above converts our Gleam list of attributes to a JavaScript + // array of key/value pairs. This below turns that array into an object. + Object.fromEntries(props), + + // Recursively pass down the dispatch function to all children. Text nodes + // – constructed below – aren't functions + ...children.toArray().map(child => typeof child === 'function' + ? child(dispatch) + : child + ) + ) +} + +export const fragment = (children) => (dispatch) => { + return React.createElement(React.Fragment, null, + ...children.toArray().map(child => typeof child === 'function' + ? child(dispatch) + : child + ) + ) +} + +// This is just an identity function! We need it because we want to trick Gleam +// into converting a `String` into an `Element(action)` . +export const text = (content) => content diff --git a/src/lustre/test.gleam b/src/lustre/test.gleam deleted file mode 100644 index f8f913b..0000000 --- a/src/lustre/test.gleam +++ /dev/null @@ -1,11 +0,0 @@ -import lustre/dom/html -import lustre/dom/html/attr - -pub fn html_test () { - html.div([ attr.class("foo") ], [ - html.p([], [ - html.text("testing") - ]), - html.input([ attr.value("hello") ]) - ]) -} |