aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHayleigh Thompson <me@hayleigh.dev>2024-03-11 05:48:53 +0100
committerHayleigh Thompson <me@hayleigh.dev>2024-03-12 08:04:11 +0100
commitc0042287511c6aaa3447f9ffc38a095add939a25 (patch)
tree3a1c2629299bedc8babf35a35cfad697639467e8
parent1e6354995552ff44d1527874c36ccb7de3b45f28 (diff)
downloadlustre-c0042287511c6aaa3447f9ffc38a095add939a25.tar.gz
lustre-c0042287511c6aaa3447f9ffc38a095add939a25.zip
:recycle: Add a 'Map' vdom node for more efficient msg mapping.
-rw-r--r--src/client-runtime.ffi.mjs8
-rw-r--r--src/lustre/element.gleam21
-rw-r--r--src/lustre/internals/patch.gleam9
-rw-r--r--src/lustre/internals/vdom.gleam16
-rw-r--r--src/vdom.ffi.mjs9
5 files changed, 42 insertions, 21 deletions
diff --git a/src/client-runtime.ffi.mjs b/src/client-runtime.ffi.mjs
index 12c8627..ab80fbf 100644
--- a/src/client-runtime.ffi.mjs
+++ b/src/client-runtime.ffi.mjs
@@ -71,8 +71,12 @@ export class LustreClientApplication {
const vdom = this.#view(this.#model);
this.#didUpdate = false;
- this.#root = morph(this.#root, vdom, (msg) => {
- this.send(new Dispatch(msg));
+ this.#root = morph(this.#root, vdom, (handler) => (e) => {
+ const result = handler(e);
+
+ if (result.isOk()) {
+ this.send(new Dispatch(result[0]));
+ }
});
}
diff --git a/src/lustre/element.gleam b/src/lustre/element.gleam
index d82d5c9..c491aab 100644
--- a/src/lustre/element.gleam
+++ b/src/lustre/element.gleam
@@ -17,7 +17,7 @@ import gleam/list
import gleam/string
import gleam/string_builder.{type StringBuilder}
import lustre/attribute.{type Attribute, attribute}
-import lustre/internals/vdom.{Element, Text}
+import lustre/internals/vdom.{Element, Map, Text}
// TYPES -----------------------------------------------------------------------
@@ -201,15 +201,18 @@ fn escape(escaped: String, content: String) -> String {
pub fn map(element: Element(a), f: fn(a) -> b) -> Element(b) {
case element {
Text(content) -> Text(content)
+ Map(subtree) -> Map(fn() { map(subtree(), f) })
Element(namespace, tag, attrs, children, self_closing, void) ->
- Element(
- namespace,
- tag,
- list.map(attrs, attribute.map(_, f)),
- list.map(children, map(_, f)),
- self_closing,
- void,
- )
+ Map(fn() {
+ Element(
+ namespace: namespace,
+ tag: tag,
+ attrs: list.map(attrs, attribute.map(_, f)),
+ children: list.map(children, map(_, f)),
+ self_closing: self_closing,
+ void: void,
+ )
+ })
}
}
diff --git a/src/lustre/internals/patch.gleam b/src/lustre/internals/patch.gleam
index 7a2073a..a0fff47 100644
--- a/src/lustre/internals/patch.gleam
+++ b/src/lustre/internals/patch.gleam
@@ -11,7 +11,7 @@ import gleam/set.{type Set}
import gleam/string
import lustre/internals/constants
import lustre/internals/vdom.{
- type Attribute, type Element, Attribute, Element, Event, Text,
+ type Attribute, type Element, Attribute, Element, Event, Map, Text,
}
// TYPES -----------------------------------------------------------------------
@@ -68,6 +68,10 @@ fn do_elements(
Some(old), Some(new) -> {
case old, new {
+ Map(old_subtree), Map(new_subtree) ->
+ do_elements(diff, Some(old_subtree()), Some(new_subtree()), key)
+ Map(subtree), _ -> do_elements(diff, Some(subtree()), Some(new), key)
+ _, Map(subtree) -> do_elements(diff, Some(old), Some(subtree()), key)
Text(old), Text(new) if old == new -> diff
// We have two text nodes but their text content is not the same. We could
// be *really* granular here and compute a diff of the text content itself
@@ -130,7 +134,7 @@ fn do_elements(
}
// When we have two elements, but their namespaces or their tags differ,
- // there is nothing to diff. We mark the new element as created and
+ // there is nothing to diff. We mark the new element as created and
// extract any event handlers.
Element(_, _, _, _, _, _), Element(_, _, _, _, _, _) as new ->
ElementDiff(
@@ -344,6 +348,7 @@ fn fold_event_handlers(
) -> Dict(String, fn(Dynamic) -> Result(msg, Nil)) {
case element {
Text(_) -> handlers
+ Map(subtree) -> fold_event_handlers(handlers, subtree(), key)
Element(_, _, attrs, children, _, _) -> {
let handlers =
list.fold(attrs, handlers, fn(handlers, attr) {
diff --git a/src/lustre/internals/vdom.gleam b/src/lustre/internals/vdom.gleam
index 6ba06f5..1bbf9fc 100644
--- a/src/lustre/internals/vdom.gleam
+++ b/src/lustre/internals/vdom.gleam
@@ -1,7 +1,7 @@
// IMPORTS ---------------------------------------------------------------------
import gleam/dict.{type Dict}
-import gleam/dynamic.{type Dynamic}
+import gleam/dynamic.{type Decoder, type Dynamic}
import gleam/int
import gleam/json.{type Json}
import gleam/list
@@ -20,11 +20,14 @@ pub type Element(msg) {
self_closing: Bool,
void: Bool,
)
+ // The lambda here defers the creation of the mapped subtree until it is necessary.
+ // This means we pay the cost of mapping multiple times only *once* during rendering.
+ Map(subtree: fn() -> Element(msg))
}
pub type Attribute(msg) {
Attribute(String, Dynamic, as_property: Bool)
- Event(String, fn(Dynamic) -> Result(msg, Nil))
+ Event(String, Decoder(msg))
}
// QUERIES ---------------------------------------------------------------------
@@ -42,6 +45,7 @@ fn do_handlers(
) -> Dict(String, fn(Dynamic) -> Result(msg, Nil)) {
case element {
Text(_) -> handlers
+ Map(subtree) -> do_handlers(subtree(), handlers, key)
Element(_, _, attrs, children, _, _) -> {
let handlers =
list.fold(attrs, handlers, fn(handlers, attr) {
@@ -68,7 +72,7 @@ pub fn element_to_json(element: Element(msg)) -> Json {
fn do_element_to_json(element: Element(msg), key: String) -> Json {
case element {
Text(content) -> json.object([#("content", json.string(content))])
-
+ Map(subtree) -> do_element_to_json(subtree(), key)
Element(namespace, tag, attrs, children, self_closing, void) -> {
let attrs =
json.preprocessed_array({
@@ -173,6 +177,8 @@ fn do_element_to_string_builder(
Text(content) if raw_text -> string_builder.from_string(content)
Text(content) -> string_builder.from_string(escape("", content))
+ Map(subtree) -> do_element_to_string_builder(subtree(), raw_text)
+
Element(namespace, tag, attrs, _, self_closing, _) if self_closing -> {
let html = string_builder.from_string("<" <> tag)
let #(attrs, _) =
@@ -331,7 +337,7 @@ fn attribute_to_string_parts(
// For everything else, we care whether or not the attribute is actually
// a property. Properties are *Javascript* values that aren't necessarily
- // reflected in the DOM.
+ // reflected in the DOM.
_ if as_property -> Error(Nil)
_ -> Ok(#(name, string.inspect(value)))
}
@@ -342,7 +348,7 @@ fn attribute_to_string_parts(
pub fn attribute_to_event_handler(
attribute: Attribute(msg),
-) -> Result(#(String, fn(Dynamic) -> Result(msg, Nil)), Nil) {
+) -> Result(#(String, Decoder(msg)), Nil) {
case attribute {
Attribute(_, _, _) -> Error(Nil)
Event(name, handler) -> {
diff --git a/src/vdom.ffi.mjs b/src/vdom.ffi.mjs
index 3a6e5a1..0e5cd70 100644
--- a/src/vdom.ffi.mjs
+++ b/src/vdom.ffi.mjs
@@ -1,7 +1,10 @@
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?.subtree) {
+ return morph(prev, curr.subtree(), dispatch, parent);
+ }
+
// The current node is an `Element` and the previous DOM node is also a DOM
// element.
if (curr?.tag && prev?.nodeType === 1) {
@@ -296,7 +299,7 @@ function morphAttr(el, name, value, dispatch) {
if (el.hasAttribute(name)) break;
const event = name.slice(15).toLowerCase();
- const handler = (e) => dispatch(serverEventHandler(e));
+ const handler = dispatch(serverEventHandler);
if (el.$lustre[`${name}Handler`]) {
el.removeEventListener(event, el.$lustre[`${name}Handler`]);
@@ -328,7 +331,7 @@ function morphAttr(el, name, value, dispatch) {
if (el.$lustre[name] === value) break;
const event = name.slice(2).toLowerCase();
- const handler = (e) => result_map(value(e), dispatch);
+ const handler = dispatch(value);
if (el.$lustre[`${name}Handler`]) {
el.removeEventListener(event, el.$lustre[`${name}Handler`]);