aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2022-03-20 10:58:23 +0000
committerHayleigh Thompson <me@hayleigh.dev>2022-03-20 10:58:23 +0000
commit170a249f1bc5caae55ae5a3469f0fc6beef63568 (patch)
tree13a65659aa2aef065d1acf59af20a109df83eaa4
parent06e3827589771da31bf033d304da4ba3a40c9cde (diff)
downloadlustre-170a249f1bc5caae55ae5a3469f0fc6beef63568.tar.gz
lustre-170a249f1bc5caae55ae5a3469f0fc6beef63568.zip
:construction: Morphdom is dead, long live React.
-rw-r--r--README.md24
-rw-r--r--package-lock.json116
-rw-r--r--package.json9
-rw-r--r--src/index.html17
-rw-r--r--src/js/test.js5
-rw-r--r--src/lustre.gleam46
-rw-r--r--src/lustre/attribute.gleam21
-rw-r--r--src/lustre/dom/html.ffi.mjs14
-rw-r--r--src/lustre/dom/html.gleam54
-rw-r--r--src/lustre/dom/html/attr.ffi.mjs6
-rw-r--r--src/lustre/dom/html/attr.gleam61
-rw-r--r--src/lustre/dom/svg.gleam32
-rw-r--r--src/lustre/dom/svg/attr.gleam92
-rw-r--r--src/lustre/element.gleam493
-rw-r--r--src/lustre/event.gleam24
-rw-r--r--src/lustre/ffi.mjs65
-rw-r--r--src/lustre/test.gleam11
17 files changed, 764 insertions, 326 deletions
diff --git a/README.md b/README.md
index 8d9e5f5..d339fd1 100644
--- a/README.md
+++ b/README.md
@@ -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") ])
- ])
-}