Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ request to fix it.
- [lustre] Fixed a bug where the internal `isLustreNode` function would incorrectly identify nodes as Lustre nodes when they were not in some cases.
- [lustre/component] Fixed a bug where a component's Shadow Root was incorrectly closed by default.
- [lustre/element] Fixed a bug where a top-level fragment would not be hydrated correctly.
- [lustre/element/keyed] Fixed a bug where keyed elements were not virtualised correctly.

## [v5.2.1] - 2025-06-23

Expand Down
10 changes: 0 additions & 10 deletions src/lustre/internals/mutable_map.ffi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,6 @@ export function empty() {
return null;
}

export function from_list(list) {
const map = new Map();

for (list; list.tail; list = list.tail) {
map.set(list.head[0], list.head[1]);
}

return map;
}

export function get(map, key) {
const value = map?.get(key);

Expand Down
23 changes: 14 additions & 9 deletions src/lustre/vdom/virtualise.ffi.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Empty, NonEmpty } from "../../gleam.mjs";
import { element, namespaced, fragment, text, none } from "../element.mjs";
import { NonEmpty } from "../../gleam.mjs";
import { text, none } from "../element.mjs";
import { element, namespaced, fragment } from '../element/keyed.mjs';
import { attribute } from "../attribute.mjs";
import { to_keyed } from "./vnode.mjs";
import { empty_list } from "../internals/constants.mjs";
import { initialiseMetadata } from "./reconciler.ffi.mjs";

Expand Down Expand Up @@ -34,7 +34,7 @@ export const virtualise = (root) => {
// a single virtualisable child, so we assume the view function returned that element.
if (virtualisableRootChildren === 1) {
const children = virtualiseChildNodes(root);
return children.head;
return children.head[1];
}

// any other number of virtualisable children > 1, the view function had to
Expand Down Expand Up @@ -64,14 +64,13 @@ const canVirtualiseNode = (node) => {
}
}

const virtualiseNode = (parent, node, index) => {
const virtualiseNode = (parent, node, key, index) => {
if (!canVirtualiseNode(node)) {
return null;
}

switch (node.nodeType) {
case ELEMENT_NODE: {
const key = node.getAttribute("data-lustre-key");
initialiseMetadata(parent, node, index, key);

if (key) {
Expand All @@ -94,7 +93,7 @@ const virtualiseNode = (parent, node, index) => {
? element(tag, attributes, children)
: namespaced(namespace, tag, attributes, children);

return key ? to_keyed(key, vnode) : vnode;
return vnode;
}

case TEXT_NODE:
Expand Down Expand Up @@ -150,10 +149,16 @@ const virtualiseChildNodes = (node, index = 0) => {
let ptr = null;

while (child) {
const vnode = virtualiseNode(node, child, index);
const key = child.nodeType === ELEMENT_NODE ? child.getAttribute('data-lustre-key') : null;
if (key != null) {
child.removeAttribute('data-lustre-key');
}

const vnode = virtualiseNode(node, child, key, index);

const next = child.nextSibling;
if (vnode) {
const list_node = new NonEmpty(vnode, null);
const list_node = new NonEmpty([key ?? '', vnode], null);
if (ptr) {
ptr = ptr.tail = list_node;
} else {
Expand Down
23 changes: 23 additions & 0 deletions test/integration/virtualise_test.ffi.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { register, unregister } from './happy-dom.ffi.mjs'
import { virtualise as do_virtualise } from '../lustre/vdom/virtualise.ffi.mjs';

export function virtualise(html, callback) {
return runInBrowserContext(() => {
document.body.innerHTML = html;
return callback(do_virtualise(document.body));
});
}

async function runInBrowserContext(callback) {
register({
width: 1920,
height: 1080,
url: "https://localhost:1234",
});

try {
return await callback();
} finally {
await unregister();
}
}
69 changes: 69 additions & 0 deletions test/integration/virtualise_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@target(javascript)
import lustre/element.{type Element}
@target(javascript)
import lustre/element/html
@target(javascript)
import lustre/element/keyed
@target(javascript)
import lustre_test

@target(javascript)
pub fn virtualise_none_test() {
use <- lustre_test.test_filter("virtualise_none_test")
test_virtualise(element.none())
}

@target(javascript)
pub fn virtualise_empty_div_test() {
use <- lustre_test.test_filter("virtualise_empty_div_test")
test_virtualise(html.div([], []))
}

@target(javascript)
pub fn virtualise_fragment_root_test() {
use <- lustre_test.test_filter("virtualise_fragment_root_test")
test_virtualise(element.fragment([html.div([], []), html.div([], [])]))
}

@target(javascript)
pub fn virtualise_text_test() {
use <- lustre_test.test_filter("virtualise_text_test")
test_virtualise(html.text("Hello, Joe!"))
}

@target(javascript)
pub fn virtualise_tree_test() {
use <- lustre_test.test_filter("virtualise_tree_test")
let html =
html.div([], [
html.h1([], [html.text("Welcome!")]),
html.p([], [html.text("...")]),
])

test_virtualise(html)
}

@target(javascript)
pub fn virtualise_keyed_test() {
use <- lustre_test.test_filter("virtualise_keyed_test")

let html =
keyed.div([], [
#("a", html.h1([], [html.text("Welcome!")])),
#("b", html.p([], [html.text("...")])),
])

test_virtualise(html)
}

@target(javascript)
fn test_virtualise(vdom: Element(msg)) {
use virtualised <- virtualise(element.to_string(vdom))
assert virtualised == vdom
}

// FFI -------------------------------------------------------------------------

@target(javascript)
@external(javascript, "./virtualise_test.ffi.mjs", "virtualise")
fn virtualise(html: String, callback: fn(Element(msg)) -> Nil) -> Nil