diff options
author | Hayleigh Thompson <me@hayleigh.dev> | 2024-03-11 05:48:53 +0100 |
---|---|---|
committer | Hayleigh Thompson <me@hayleigh.dev> | 2024-03-12 08:04:11 +0100 |
commit | c0042287511c6aaa3447f9ffc38a095add939a25 (patch) | |
tree | 3a1c2629299bedc8babf35a35cfad697639467e8 | |
parent | 1e6354995552ff44d1527874c36ccb7de3b45f28 (diff) | |
download | lustre-c0042287511c6aaa3447f9ffc38a095add939a25.tar.gz lustre-c0042287511c6aaa3447f9ffc38a095add939a25.zip |
:recycle: Add a 'Map' vdom node for more efficient msg mapping.
-rw-r--r-- | src/client-runtime.ffi.mjs | 8 | ||||
-rw-r--r-- | src/lustre/element.gleam | 21 | ||||
-rw-r--r-- | src/lustre/internals/patch.gleam | 9 | ||||
-rw-r--r-- | src/lustre/internals/vdom.gleam | 16 | ||||
-rw-r--r-- | src/vdom.ffi.mjs | 9 |
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`]); |