@livetemplate/client 0.8.25 → 0.8.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dom/link-interceptor.d.ts +11 -2
- package/dist/dom/link-interceptor.d.ts.map +1 -1
- package/dist/dom/link-interceptor.js +87 -4
- package/dist/dom/link-interceptor.js.map +1 -1
- package/dist/livetemplate-client.browser.js +4 -4
- package/dist/livetemplate-client.browser.js.map +3 -3
- package/dist/livetemplate-client.d.ts +21 -0
- package/dist/livetemplate-client.d.ts.map +1 -1
- package/dist/livetemplate-client.js +282 -49
- package/dist/livetemplate-client.js.map +1 -1
- package/dist/tests/conditional-slot-transition.test.d.ts +15 -0
- package/dist/tests/conditional-slot-transition.test.d.ts.map +1 -0
- package/dist/tests/conditional-slot-transition.test.js +98 -0
- package/dist/tests/conditional-slot-transition.test.js.map +1 -0
- package/dist/tests/navigate.test.d.ts +18 -0
- package/dist/tests/navigate.test.d.ts.map +1 -0
- package/dist/tests/navigate.test.js +219 -0
- package/dist/tests/navigate.test.js.map +1 -0
- package/dist/tests/navigation.test.js +31 -5
- package/dist/tests/navigation.test.js.map +1 -1
- package/dist/tests/preserve.test.d.ts +17 -0
- package/dist/tests/preserve.test.d.ts.map +1 -0
- package/dist/tests/preserve.test.js +193 -0
- package/dist/tests/preserve.test.js.map +1 -0
- package/dist/tests/script-duplication.test.d.ts +15 -0
- package/dist/tests/script-duplication.test.d.ts.map +1 -0
- package/dist/tests/script-duplication.test.js +87 -0
- package/dist/tests/script-duplication.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lvt-preserve attribute tests.
|
|
4
|
+
*
|
|
5
|
+
* lvt-preserve tells the morphdom diff engine "don't touch this element".
|
|
6
|
+
* It's the generic escape hatch for interactive elements whose state
|
|
7
|
+
* lives on the client side — <details open>, <dialog open>, checkbox
|
|
8
|
+
* state, scroll positions, third-party widgets, etc. Without it, any
|
|
9
|
+
* server-driven update that doesn't include the client-managed state
|
|
10
|
+
* clobbers it on the next diff cycle.
|
|
11
|
+
*
|
|
12
|
+
* Equivalent attributes in other frameworks:
|
|
13
|
+
* - Phoenix LiveView: phx-update="ignore"
|
|
14
|
+
* - Hotwire Turbo: data-turbo-permanent
|
|
15
|
+
* - HTMX: hx-preserve="true"
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const livetemplate_client_1 = require("../livetemplate-client");
|
|
19
|
+
describe("lvt-preserve attribute", () => {
|
|
20
|
+
let client;
|
|
21
|
+
let wrapper;
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
client = new livetemplate_client_1.LiveTemplateClient();
|
|
24
|
+
// Minimal LiveTemplate-style wrapper the updateDOM path expects.
|
|
25
|
+
// Built via createElement so no innerHTML is needed in the test setup.
|
|
26
|
+
document.body.replaceChildren();
|
|
27
|
+
wrapper = document.createElement("div");
|
|
28
|
+
wrapper.setAttribute("data-lvt-id", "test-preserve");
|
|
29
|
+
document.body.appendChild(wrapper);
|
|
30
|
+
});
|
|
31
|
+
it("preserves an element's open attribute across updates", () => {
|
|
32
|
+
const initialTree = {
|
|
33
|
+
s: [
|
|
34
|
+
`<details lvt-preserve class="picker"><summary>Sessions</summary><div class="list">`,
|
|
35
|
+
`</div></details>`,
|
|
36
|
+
],
|
|
37
|
+
0: "one two",
|
|
38
|
+
};
|
|
39
|
+
client.updateDOM(wrapper, initialTree);
|
|
40
|
+
const details = wrapper.querySelector("details");
|
|
41
|
+
expect(details).not.toBeNull();
|
|
42
|
+
expect(details.open).toBe(false);
|
|
43
|
+
// Simulate the user tapping the summary to expand the details.
|
|
44
|
+
details.setAttribute("open", "");
|
|
45
|
+
expect(details.open).toBe(true);
|
|
46
|
+
// Apply an update that does NOT contain the open attribute. Without
|
|
47
|
+
// lvt-preserve, morphdom would diff the incoming <details> against
|
|
48
|
+
// the DOM and remove the open attribute to match the server.
|
|
49
|
+
const updateTree = { 0: "one two three" };
|
|
50
|
+
client.updateDOM(wrapper, updateTree);
|
|
51
|
+
const detailsAfter = wrapper.querySelector("details");
|
|
52
|
+
expect(detailsAfter).not.toBeNull();
|
|
53
|
+
expect(detailsAfter.open).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("does not preserve elements without lvt-preserve (control)", () => {
|
|
56
|
+
// Same shape, no lvt-preserve. Verify the open state IS clobbered
|
|
57
|
+
// here — confirming the preservation guarantee in the test above
|
|
58
|
+
// is actually doing work and not just always passing.
|
|
59
|
+
const initialTree = {
|
|
60
|
+
s: [
|
|
61
|
+
`<details class="picker"><summary>Sessions</summary><div class="list">`,
|
|
62
|
+
`</div></details>`,
|
|
63
|
+
],
|
|
64
|
+
0: "one two",
|
|
65
|
+
};
|
|
66
|
+
client.updateDOM(wrapper, initialTree);
|
|
67
|
+
const details = wrapper.querySelector("details");
|
|
68
|
+
details.setAttribute("open", "");
|
|
69
|
+
expect(details.open).toBe(true);
|
|
70
|
+
const updateTree = { 0: "one two three" };
|
|
71
|
+
client.updateDOM(wrapper, updateTree);
|
|
72
|
+
const detailsAfter = wrapper.querySelector("details");
|
|
73
|
+
// Without lvt-preserve, morphdom diff removes the user's open attr.
|
|
74
|
+
expect(detailsAfter.open).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
it("lvt-preserve-attrs keeps own attributes but still diffs children", () => {
|
|
77
|
+
// This is the subtler "collapsible picker" case: the <details>
|
|
78
|
+
// element's `open` attribute is user-toggled and must survive
|
|
79
|
+
// server updates, but the <a> cards inside ARE server-authored
|
|
80
|
+
// and their state (e.g. the `current` class marker for the
|
|
81
|
+
// selected item) must reflect the latest tree.
|
|
82
|
+
const initialTree = {
|
|
83
|
+
s: [
|
|
84
|
+
`<details lvt-preserve-attrs class="picker"><summary>Pick</summary>`,
|
|
85
|
+
`</details>`,
|
|
86
|
+
],
|
|
87
|
+
0: `<a class="card">one</a><a class="card">two</a>`,
|
|
88
|
+
};
|
|
89
|
+
client.updateDOM(wrapper, initialTree);
|
|
90
|
+
const details = wrapper.querySelector("details");
|
|
91
|
+
expect(details).not.toBeNull();
|
|
92
|
+
expect(details.open).toBe(false);
|
|
93
|
+
// User opens the picker.
|
|
94
|
+
details.setAttribute("open", "");
|
|
95
|
+
expect(details.open).toBe(true);
|
|
96
|
+
// Server pushes an update where "two" is now the current card.
|
|
97
|
+
const updateTree = {
|
|
98
|
+
0: `<a class="card">one</a><a class="card current">two</a>`,
|
|
99
|
+
};
|
|
100
|
+
client.updateDOM(wrapper, updateTree);
|
|
101
|
+
const detailsAfter = wrapper.querySelector("details");
|
|
102
|
+
// User's open state preserved.
|
|
103
|
+
expect(detailsAfter.open).toBe(true);
|
|
104
|
+
// But the children DID update — the second card now has "current".
|
|
105
|
+
const cards = wrapper.querySelectorAll("a.card");
|
|
106
|
+
expect(cards.length).toBe(2);
|
|
107
|
+
expect(cards[0].classList.contains("current")).toBe(false);
|
|
108
|
+
expect(cards[1].classList.contains("current")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it("server can remove lvt-preserve by omitting it in the next full template", () => {
|
|
111
|
+
// lvt-preserve is checked on toEl (the incoming server version), not
|
|
112
|
+
// fromEl (the current DOM). This means the server retains authority:
|
|
113
|
+
// a later render that omits lvt-preserve lets morphdom resume updating
|
|
114
|
+
// the element. Checking fromEl would make the attribute sticky forever.
|
|
115
|
+
const initialTree = {
|
|
116
|
+
s: [`<div lvt-preserve class="widget">`, `</div>`],
|
|
117
|
+
0: "server-initial",
|
|
118
|
+
};
|
|
119
|
+
client.updateDOM(wrapper, initialTree);
|
|
120
|
+
const widget = wrapper.querySelector(".widget");
|
|
121
|
+
// Simulate the widget mutating its own DOM.
|
|
122
|
+
widget.textContent = "client-modified";
|
|
123
|
+
// Server sends a NEW template that removes lvt-preserve.
|
|
124
|
+
const removedTree = {
|
|
125
|
+
s: [`<div class="widget">`, `</div>`],
|
|
126
|
+
0: "server-updated",
|
|
127
|
+
};
|
|
128
|
+
client.updateDOM(wrapper, removedTree);
|
|
129
|
+
// Now that lvt-preserve is gone from the template, morphdom should
|
|
130
|
+
// have applied the server's update, overwriting the client state.
|
|
131
|
+
const widgetAfter = wrapper.querySelector(".widget");
|
|
132
|
+
expect(widgetAfter).not.toBeNull();
|
|
133
|
+
expect(widgetAfter.textContent).toBe("server-updated");
|
|
134
|
+
expect(widgetAfter.hasAttribute("lvt-preserve")).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
it("server can remove lvt-preserve-attrs by omitting it in a later update", () => {
|
|
137
|
+
// The attribute-copy loop must NOT copy the lvt-preserve-attrs control
|
|
138
|
+
// attribute itself back onto toEl. If it did, the server could never
|
|
139
|
+
// remove the attribute in a future render (it would always be re-added
|
|
140
|
+
// by the copy loop before morphdom sees the diff).
|
|
141
|
+
const initialTree = {
|
|
142
|
+
s: [
|
|
143
|
+
`<details lvt-preserve-attrs class="picker"><summary>Pick</summary>`,
|
|
144
|
+
`</details>`,
|
|
145
|
+
],
|
|
146
|
+
0: `<a class="card">item</a>`,
|
|
147
|
+
};
|
|
148
|
+
client.updateDOM(wrapper, initialTree);
|
|
149
|
+
const details = wrapper.querySelector("details");
|
|
150
|
+
expect(details.hasAttribute("lvt-preserve-attrs")).toBe(true);
|
|
151
|
+
// Server pushes an update WITHOUT lvt-preserve-attrs — it is opting
|
|
152
|
+
// the element back out of attribute preservation.
|
|
153
|
+
const updateTree = {
|
|
154
|
+
s: [
|
|
155
|
+
`<details class="picker"><summary>Pick</summary>`,
|
|
156
|
+
`</details>`,
|
|
157
|
+
],
|
|
158
|
+
0: `<a class="card">item</a>`,
|
|
159
|
+
};
|
|
160
|
+
client.updateDOM(wrapper, updateTree);
|
|
161
|
+
const detailsAfter = wrapper.querySelector("details");
|
|
162
|
+
expect(detailsAfter).not.toBeNull();
|
|
163
|
+
// The control attribute must be gone — the server has opted out.
|
|
164
|
+
expect(detailsAfter.hasAttribute("lvt-preserve-attrs")).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
it("preserves the element's children as well", () => {
|
|
167
|
+
// lvt-preserve is a full-element bail-out: attributes, children,
|
|
168
|
+
// everything stays as-is. Useful for third-party widgets that
|
|
169
|
+
// mutate their own DOM.
|
|
170
|
+
const initialTree = {
|
|
171
|
+
s: [`<div lvt-preserve class="widget">`, `</div>`],
|
|
172
|
+
0: "initial content",
|
|
173
|
+
};
|
|
174
|
+
client.updateDOM(wrapper, initialTree);
|
|
175
|
+
const widget = wrapper.querySelector(".widget");
|
|
176
|
+
expect(widget.textContent).toBe("initial content");
|
|
177
|
+
// Simulate a third-party widget mutating its own children via
|
|
178
|
+
// safe DOM methods (the livetemplate contract applies regardless
|
|
179
|
+
// of how the client-side mutation happened).
|
|
180
|
+
widget.replaceChildren();
|
|
181
|
+
const span = document.createElement("span");
|
|
182
|
+
span.textContent = "widget-modified";
|
|
183
|
+
widget.appendChild(span);
|
|
184
|
+
// Server sends an update that would otherwise replace the content.
|
|
185
|
+
const updateTree = { 0: "server-updated content" };
|
|
186
|
+
client.updateDOM(wrapper, updateTree);
|
|
187
|
+
const widgetAfter = wrapper.querySelector(".widget");
|
|
188
|
+
// The widget's own mutation survives — morphdom never touched it.
|
|
189
|
+
expect(widgetAfter.textContent).toBe("widget-modified");
|
|
190
|
+
expect(widgetAfter.querySelector("span")).not.toBeNull();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
//# sourceMappingURL=preserve.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preserve.test.js","sourceRoot":"","sources":["../../tests/preserve.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the inline <script> content duplication bug.
|
|
3
|
+
*
|
|
4
|
+
* When the reconstructed HTML contains an inline <script> tag and
|
|
5
|
+
* updateDOM sets tempWrapper.innerHTML, browsers parse the script
|
|
6
|
+
* content specially and can create phantom duplicate DOM nodes after
|
|
7
|
+
* the script boundary. morphdom then sees doubled elements and
|
|
8
|
+
* patches them into the live DOM.
|
|
9
|
+
*
|
|
10
|
+
* This test operates at the updateDOM level (tree + morphdom) to
|
|
11
|
+
* reproduce the exact conditions: a template with a <script> block
|
|
12
|
+
* followed by more HTML elements.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=script-duplication.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-duplication.test.d.ts","sourceRoot":"","sources":["../../tests/script-duplication.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Regression test for the inline <script> content duplication bug.
|
|
4
|
+
*
|
|
5
|
+
* When the reconstructed HTML contains an inline <script> tag and
|
|
6
|
+
* updateDOM sets tempWrapper.innerHTML, browsers parse the script
|
|
7
|
+
* content specially and can create phantom duplicate DOM nodes after
|
|
8
|
+
* the script boundary. morphdom then sees doubled elements and
|
|
9
|
+
* patches them into the live DOM.
|
|
10
|
+
*
|
|
11
|
+
* This test operates at the updateDOM level (tree + morphdom) to
|
|
12
|
+
* reproduce the exact conditions: a template with a <script> block
|
|
13
|
+
* followed by more HTML elements.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const livetemplate_client_1 = require("../livetemplate-client");
|
|
17
|
+
describe("inline script duplication", () => {
|
|
18
|
+
let client;
|
|
19
|
+
let wrapper;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
client = new livetemplate_client_1.LiveTemplateClient();
|
|
22
|
+
document.body.replaceChildren();
|
|
23
|
+
wrapper = document.createElement("div");
|
|
24
|
+
wrapper.setAttribute("data-lvt-id", "script-dup-test");
|
|
25
|
+
document.body.appendChild(wrapper);
|
|
26
|
+
});
|
|
27
|
+
it("elements after an inline script are NOT duplicated", () => {
|
|
28
|
+
// Initial tree: content + script + more content after.
|
|
29
|
+
// The tree shape mirrors what devbox-dash's claude.tmpl produces
|
|
30
|
+
// when chat messages are followed by a scroll-to-bottom script
|
|
31
|
+
// and then a key-grid div.
|
|
32
|
+
const tree = {
|
|
33
|
+
s: [
|
|
34
|
+
'<div class="chat">messages here</div>',
|
|
35
|
+
'<script>(function(){ /* scroll */ })();</script>',
|
|
36
|
+
'<div class="after-script">',
|
|
37
|
+
"</div>",
|
|
38
|
+
],
|
|
39
|
+
0: "", // between chat and script (slot 0)
|
|
40
|
+
1: "", // between script and after-script (slot 1)
|
|
41
|
+
2: "unique-content-that-should-appear-once", // inside after-script
|
|
42
|
+
};
|
|
43
|
+
client.updateDOM(wrapper, tree);
|
|
44
|
+
// Count: the div.after-script should appear exactly ONCE.
|
|
45
|
+
const afterScriptDivs = wrapper.querySelectorAll(".after-script");
|
|
46
|
+
expect(afterScriptDivs.length).toBe(1);
|
|
47
|
+
// The unique content should appear once.
|
|
48
|
+
const textOccurrences = (wrapper.textContent || "").split("unique-content-that-should-appear-once").length - 1;
|
|
49
|
+
expect(textOccurrences).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
it("uppercase <SCRIPT> tag also routes through DOMParser (case-insensitive)", () => {
|
|
52
|
+
// HTML tag names are case-insensitive; <SCRIPT> must be treated the
|
|
53
|
+
// same as <script> so that the DOMParser path (which prevents phantom
|
|
54
|
+
// duplication) is used regardless of casing.
|
|
55
|
+
const tree = {
|
|
56
|
+
s: [
|
|
57
|
+
'<div class="before">before</div>',
|
|
58
|
+
'<SCRIPT>var y = 2;</SCRIPT>',
|
|
59
|
+
'<div class="after">after</div>',
|
|
60
|
+
],
|
|
61
|
+
0: "",
|
|
62
|
+
1: "",
|
|
63
|
+
};
|
|
64
|
+
client.updateDOM(wrapper, tree);
|
|
65
|
+
expect(wrapper.querySelectorAll(".after").length).toBe(1);
|
|
66
|
+
expect(wrapper.querySelectorAll(".before").length).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
it("a second updateDOM does not cause further duplication", () => {
|
|
69
|
+
const tree = {
|
|
70
|
+
s: [
|
|
71
|
+
'<div class="before">',
|
|
72
|
+
'</div><script>var x = 1;</script><div class="after">',
|
|
73
|
+
"</div>",
|
|
74
|
+
],
|
|
75
|
+
0: "initial",
|
|
76
|
+
1: "initial-after",
|
|
77
|
+
};
|
|
78
|
+
client.updateDOM(wrapper, tree);
|
|
79
|
+
expect(wrapper.querySelectorAll(".after").length).toBe(1);
|
|
80
|
+
// Apply an update that changes the dynamic slot but keeps the same structure.
|
|
81
|
+
const update = { 0: "updated", 1: "updated-after" };
|
|
82
|
+
client.updateDOM(wrapper, update);
|
|
83
|
+
expect(wrapper.querySelectorAll(".after").length).toBe(1);
|
|
84
|
+
expect(wrapper.textContent).toContain("updated-after");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=script-duplication.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-duplication.test.js","sourceRoot":"","sources":["../../tests/script-duplication.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED