@nordcraft/runtime 1.0.88 → 1.0.90
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/custom-element.main.esm.js +24 -24
- package/dist/custom-element.main.esm.js.map +4 -4
- package/dist/page.main.esm.js +3 -3
- package/dist/page.main.esm.js.map +4 -4
- package/dist/src/components/createComponent.js +37 -36
- package/dist/src/components/createComponent.js.map +1 -1
- package/dist/src/components/createElement.js +0 -2
- package/dist/src/components/createElement.js.map +1 -1
- package/dist/src/components/createNode.js +12 -4
- package/dist/src/components/createNode.js.map +1 -1
- package/dist/src/components/createNode.test.d.ts +1 -0
- package/dist/src/components/createNode.test.js +608 -0
- package/dist/src/components/createNode.test.js.map +1 -0
- package/dist/src/components/meta.d.ts +3 -0
- package/dist/src/components/meta.js +18 -0
- package/dist/src/components/meta.js.map +1 -0
- package/dist/src/components/meta.test.d.ts +1 -0
- package/dist/src/components/meta.test.js +80 -0
- package/dist/src/components/meta.test.js.map +1 -0
- package/dist/src/components/renderComponent.js +2 -4
- package/dist/src/components/renderComponent.js.map +1 -1
- package/dist/src/editor/postMessageToEditor.d.ts +2 -0
- package/dist/src/editor/postMessageToEditor.js +4 -0
- package/dist/src/editor/postMessageToEditor.js.map +1 -0
- package/dist/src/editor-preview.main.js +12 -13
- package/dist/src/editor-preview.main.js.map +1 -1
- package/dist/src/page.main.js +46 -59
- package/dist/src/page.main.js.map +1 -1
- package/dist/src/styles/CustomPropertyStyleSheet.d.ts +0 -1
- package/dist/src/styles/CustomPropertyStyleSheet.js +1 -1
- package/dist/src/styles/CustomPropertyStyleSheet.js.map +1 -1
- package/dist/src/styles/CustomPropertyStyleSheet.test.js +2 -5
- package/dist/src/styles/CustomPropertyStyleSheet.test.js.map +1 -1
- package/dist/src/utils/BatchQueue.d.ts +1 -0
- package/dist/src/utils/BatchQueue.js +2 -1
- package/dist/src/utils/BatchQueue.js.map +1 -1
- package/dist/src/utils/getComponent.d.ts +5 -0
- package/dist/src/utils/getComponent.js +8 -0
- package/dist/src/utils/getComponent.js.map +1 -0
- package/dist/src/utils/getComponent.test.d.ts +1 -0
- package/dist/src/utils/getComponent.test.js +24 -0
- package/dist/src/utils/getComponent.test.js.map +1 -0
- package/dist/src/utils/markSelectedElement.d.ts +1 -0
- package/dist/src/utils/markSelectedElement.js +9 -0
- package/dist/src/utils/markSelectedElement.js.map +1 -0
- package/dist/src/utils/subscribeCustomProperty.d.ts +3 -3
- package/dist/src/utils/subscribeCustomProperty.js +2 -3
- package/dist/src/utils/subscribeCustomProperty.js.map +1 -1
- package/package.json +3 -3
- package/src/components/createComponent.ts +57 -51
- package/src/components/createElement.ts +0 -2
- package/src/components/createNode.test.ts +686 -0
- package/src/components/createNode.ts +17 -6
- package/src/components/meta.test.ts +90 -0
- package/src/components/meta.ts +23 -0
- package/src/components/renderComponent.ts +2 -4
- package/src/editor/postMessageToEditor.ts +5 -0
- package/src/editor-preview.main.ts +12 -15
- package/src/page.main.ts +47 -59
- package/src/styles/CustomPropertyStyleSheet.test.ts +2 -7
- package/src/styles/CustomPropertyStyleSheet.ts +1 -2
- package/src/utils/BatchQueue.ts +2 -2
- package/src/utils/getComponent.test.ts +29 -0
- package/src/utils/getComponent.ts +15 -0
- package/src/utils/markSelectedElement.ts +8 -0
- package/src/utils/subscribeCustomProperty.ts +1 -5
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import type { ComponentData } from '@nordcraft/core/dist/component/component.types'
|
|
2
|
+
import type { ToddleEnv } from '@nordcraft/core/dist/formula/formula'
|
|
3
|
+
import { describe, expect, test } from 'bun:test'
|
|
4
|
+
import '../../happydom'
|
|
5
|
+
import { signal } from '../signal/signal'
|
|
6
|
+
import type { ComponentContext } from '../types'
|
|
7
|
+
import { customPropertiesStylesheet } from '../utils/subscribeCustomProperty'
|
|
8
|
+
import { createNode } from './createNode'
|
|
9
|
+
|
|
10
|
+
describe('createNode()', () => {
|
|
11
|
+
test('it can render basic nodes', () => {
|
|
12
|
+
const parentElement = document.createElement('div')
|
|
13
|
+
const nodes = createNode({
|
|
14
|
+
ctx: {
|
|
15
|
+
isRootComponent: false,
|
|
16
|
+
component: {
|
|
17
|
+
name: 'My Component',
|
|
18
|
+
nodes: {
|
|
19
|
+
'test-node-id': {
|
|
20
|
+
type: 'element',
|
|
21
|
+
tag: 'div',
|
|
22
|
+
children: ['test-node-id.0', 'test-node-id.1'],
|
|
23
|
+
attrs: {},
|
|
24
|
+
events: {},
|
|
25
|
+
},
|
|
26
|
+
'test-node-id.0': {
|
|
27
|
+
type: 'text',
|
|
28
|
+
value: { type: 'value', value: 'Item 1' },
|
|
29
|
+
},
|
|
30
|
+
'test-node-id.1': {
|
|
31
|
+
type: 'text',
|
|
32
|
+
value: { type: 'value', value: 'Item 2' },
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
} as Partial<ComponentContext> as any,
|
|
37
|
+
namespace: 'http://www.w3.org/1999/xhtml',
|
|
38
|
+
dataSignal: signal<ComponentData>({
|
|
39
|
+
Attributes: {},
|
|
40
|
+
Variables: {},
|
|
41
|
+
}),
|
|
42
|
+
path: 'test-node',
|
|
43
|
+
id: 'test-node-id',
|
|
44
|
+
parentElement,
|
|
45
|
+
instance: {},
|
|
46
|
+
})
|
|
47
|
+
expect(nodes.length).toBe(1)
|
|
48
|
+
expect((nodes[0] as Element).outerHTML).toMatchInlineSnapshot(
|
|
49
|
+
`"<div data-node-id="test-node-id" data-id="test-node" data-component="My Component" class="cYXIdv"><span data-node-id="test-node-id.0" data-id="test-node.0" data-component="My Component" data-node-type="text">Item 1</span><span data-node-id="test-node-id.1" data-id="test-node.1" data-component="My Component" data-node-type="text">Item 2</span></div>"`,
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('repeat nodes should keep the same node references when items are shuffled/added/removed', () => {
|
|
54
|
+
// In this test, we update the signal to change the order of the content in the repeat.
|
|
55
|
+
// We use repeatKey, so the nodes should simply be moved in the DOM instead of recreated.
|
|
56
|
+
const parentElement = document.createElement('div')
|
|
57
|
+
document.body.appendChild(parentElement)
|
|
58
|
+
const item1 = { id: 'i1', name: 'Item 1' }
|
|
59
|
+
const item2 = { id: 'i2', name: 'Item 2' }
|
|
60
|
+
const item3 = { id: 'i3', name: 'Item 3' }
|
|
61
|
+
|
|
62
|
+
const dataSignal = signal<ComponentData>({
|
|
63
|
+
Attributes: {},
|
|
64
|
+
Variables: {
|
|
65
|
+
items: [item1, item2, item3],
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const ctx: ComponentContext = {
|
|
70
|
+
isRootComponent: false,
|
|
71
|
+
component: {
|
|
72
|
+
name: 'My Component',
|
|
73
|
+
nodes: {
|
|
74
|
+
'repeat-node-id': {
|
|
75
|
+
type: 'element',
|
|
76
|
+
tag: 'div',
|
|
77
|
+
repeat: { type: 'path', path: ['Variables', 'items'] },
|
|
78
|
+
repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
|
|
79
|
+
children: ['text-node-id'],
|
|
80
|
+
attrs: {},
|
|
81
|
+
events: {},
|
|
82
|
+
customProperties: {
|
|
83
|
+
'--color': {
|
|
84
|
+
syntax: {
|
|
85
|
+
type: 'primitive',
|
|
86
|
+
name: 'color',
|
|
87
|
+
},
|
|
88
|
+
formula: {
|
|
89
|
+
type: 'value',
|
|
90
|
+
value: 'red',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
'text-node-id': {
|
|
96
|
+
type: 'text',
|
|
97
|
+
value: { type: 'path', path: ['ListItem', 'Item', 'name'] },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
root: document,
|
|
102
|
+
formulaCache: {},
|
|
103
|
+
env: { runtime: 'page' } as ToddleEnv,
|
|
104
|
+
} as Partial<ComponentContext> as ComponentContext
|
|
105
|
+
|
|
106
|
+
const nodes = createNode({
|
|
107
|
+
ctx,
|
|
108
|
+
namespace: 'http://www.w3.org/1999/xhtml',
|
|
109
|
+
dataSignal,
|
|
110
|
+
path: '0.0.0',
|
|
111
|
+
id: 'repeat-node-id',
|
|
112
|
+
parentElement,
|
|
113
|
+
instance: {},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Custom properties stylesheet should be created and have the custom property from the node
|
|
117
|
+
const sheet = customPropertiesStylesheet?.getStyleSheet()
|
|
118
|
+
expect(sheet).toBeTruthy()
|
|
119
|
+
expect(sheet?.cssRules.length).toBe(3)
|
|
120
|
+
expect(sheet?.cssRules[0].cssText).toBe(
|
|
121
|
+
'[data-id="0.0.0"] { --color: red; }',
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
parentElement.append(...nodes)
|
|
125
|
+
|
|
126
|
+
expect(parentElement.children.length).toBe(3)
|
|
127
|
+
const element1 = parentElement.children[0]
|
|
128
|
+
const element2 = parentElement.children[1]
|
|
129
|
+
const element3 = parentElement.children[2]
|
|
130
|
+
|
|
131
|
+
expect(element1.textContent).toBe('Item 1')
|
|
132
|
+
expect(element1.getAttribute('data-id')).toBe('0.0.0')
|
|
133
|
+
expect(element2.textContent).toBe('Item 2')
|
|
134
|
+
expect(element2.getAttribute('data-id')).toBe('0.0.0(1)')
|
|
135
|
+
expect(element3.textContent).toBe('Item 3')
|
|
136
|
+
expect(element3.getAttribute('data-id')).toBe('0.0.0(2)')
|
|
137
|
+
|
|
138
|
+
// Shuffle the items: [3, 1, 2]
|
|
139
|
+
dataSignal.update((data) => {
|
|
140
|
+
return {
|
|
141
|
+
...data,
|
|
142
|
+
Variables: {
|
|
143
|
+
...data.Variables,
|
|
144
|
+
items: [item3, item1, item2],
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// After update, elements should be shuffled but the same objects
|
|
150
|
+
expect(parentElement.children.length).toBe(3)
|
|
151
|
+
|
|
152
|
+
// Check text content first
|
|
153
|
+
expect(parentElement.children[0].textContent).toBe('Item 3')
|
|
154
|
+
expect(parentElement.children[1].textContent).toBe('Item 1')
|
|
155
|
+
expect(parentElement.children[2].textContent).toBe('Item 2')
|
|
156
|
+
|
|
157
|
+
// Check identities (do not use test-toBe for DOM nodes is annoying to compare and we are only interested in the reference match)
|
|
158
|
+
expect(parentElement.children[0] === element3).toBeTruthy()
|
|
159
|
+
expect(parentElement.children[0].getAttribute('data-id')).toBe(`0.0.0(2)`)
|
|
160
|
+
expect(parentElement.children[1] === element1).toBeTruthy()
|
|
161
|
+
expect(parentElement.children[1].getAttribute('data-id')).toBe(`0.0.0`)
|
|
162
|
+
expect(parentElement.children[2] === element2).toBeTruthy()
|
|
163
|
+
expect(parentElement.children[2].getAttribute('data-id')).toBe(`0.0.0(1)`)
|
|
164
|
+
|
|
165
|
+
// Remove last item in the list
|
|
166
|
+
dataSignal.update((data) => {
|
|
167
|
+
return {
|
|
168
|
+
...data,
|
|
169
|
+
Variables: {
|
|
170
|
+
...data.Variables,
|
|
171
|
+
items: [item3, item1],
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(parentElement.children.length).toBe(2)
|
|
177
|
+
expect(parentElement.children[0] === element3).toBeTruthy()
|
|
178
|
+
expect(parentElement.children[0].getAttribute('data-id')).toBe(`0.0.0(2)`)
|
|
179
|
+
expect(parentElement.children[1] === element1).toBeTruthy()
|
|
180
|
+
expect(parentElement.children[1].getAttribute('data-id')).toBe(`0.0.0`)
|
|
181
|
+
|
|
182
|
+
// Make sure element 2 is removed from the DOM
|
|
183
|
+
expect(element2.parentElement === null).toBeTruthy()
|
|
184
|
+
|
|
185
|
+
// Should have cleaned the unused custom property from the stylesheet
|
|
186
|
+
expect(sheet?.cssRules.length).toBe(2)
|
|
187
|
+
|
|
188
|
+
// The rules should be correct for the remaining elements
|
|
189
|
+
expect(sheet?.cssRules[0].cssText).toBe(
|
|
190
|
+
'[data-id="0.0.0"] { --color: red; }',
|
|
191
|
+
)
|
|
192
|
+
expect(sheet?.cssRules[1].cssText).toBe(
|
|
193
|
+
'[data-id="0.0.0(2)"] { --color: red; }',
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// Add the same item multiple times
|
|
197
|
+
dataSignal.update((data) => {
|
|
198
|
+
return {
|
|
199
|
+
...data,
|
|
200
|
+
Variables: {
|
|
201
|
+
...data.Variables,
|
|
202
|
+
items: [item3, item1, item3, item3],
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Should have 4 elements and each should have a different data-id, as a duplicate repeatKey should undo any reuse optimizations
|
|
208
|
+
expect(parentElement.children.length).toBe(4)
|
|
209
|
+
// Expect all data-ids to be unique
|
|
210
|
+
const dataIds = new Set<string>()
|
|
211
|
+
for (const child of parentElement.children) {
|
|
212
|
+
const dataId = child.getAttribute('data-id')
|
|
213
|
+
expect(dataId).toBeTruthy()
|
|
214
|
+
expect(
|
|
215
|
+
dataIds.has(dataId!),
|
|
216
|
+
`Duplicate data-id found: ${dataId}, all ids: ${[...parentElement.children].map((c) => c.getAttribute('data-id'))}`,
|
|
217
|
+
).toBeFalsy()
|
|
218
|
+
dataIds.add(dataId!)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Custom properties stylesheet should be cleaned up after all nodes are removed
|
|
222
|
+
dataSignal.destroy()
|
|
223
|
+
expect(sheet?.cssRules.length).toBe(0)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
test('conditional nodes should remove and recreate elements on toggle', () => {
|
|
227
|
+
const parentElement = document.createElement('div')
|
|
228
|
+
document.body.appendChild(parentElement)
|
|
229
|
+
const dataSignal = signal<ComponentData>({
|
|
230
|
+
Attributes: { show: true },
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const ctx = {
|
|
234
|
+
isRootComponent: false,
|
|
235
|
+
component: {
|
|
236
|
+
name: 'My Component',
|
|
237
|
+
nodes: {
|
|
238
|
+
'conditional-node': {
|
|
239
|
+
type: 'element',
|
|
240
|
+
tag: 'div',
|
|
241
|
+
condition: { type: 'path', path: ['Attributes', 'show'] },
|
|
242
|
+
children: ['child-node'],
|
|
243
|
+
attrs: {},
|
|
244
|
+
events: {},
|
|
245
|
+
},
|
|
246
|
+
'child-node': {
|
|
247
|
+
type: 'text',
|
|
248
|
+
value: { type: 'value', value: 'Hello' },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
root: document,
|
|
253
|
+
env: { runtime: 'page' },
|
|
254
|
+
formulaCache: {},
|
|
255
|
+
toddle: {
|
|
256
|
+
getCustomFormula: () => undefined,
|
|
257
|
+
},
|
|
258
|
+
} as any as ComponentContext
|
|
259
|
+
|
|
260
|
+
const nodes = createNode({
|
|
261
|
+
ctx,
|
|
262
|
+
namespace: 'http://www.w3.org/1999/xhtml',
|
|
263
|
+
dataSignal,
|
|
264
|
+
path: 'cond',
|
|
265
|
+
id: 'conditional-node',
|
|
266
|
+
parentElement,
|
|
267
|
+
instance: {},
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
parentElement.append(...nodes)
|
|
271
|
+
expect(parentElement.children.length).toBe(1)
|
|
272
|
+
const firstElement = parentElement.children[0]
|
|
273
|
+
expect(firstElement.textContent).toBe('Hello')
|
|
274
|
+
|
|
275
|
+
// Toggle off
|
|
276
|
+
dataSignal.update((data) => ({
|
|
277
|
+
...data,
|
|
278
|
+
Attributes: { ...data.Attributes, show: false },
|
|
279
|
+
}))
|
|
280
|
+
expect(parentElement.children.length).toBe(0)
|
|
281
|
+
expect(firstElement.parentElement).toBeNull()
|
|
282
|
+
|
|
283
|
+
// Toggle on again
|
|
284
|
+
dataSignal.update((data) => ({
|
|
285
|
+
...data,
|
|
286
|
+
Attributes: { ...data.Attributes, show: true },
|
|
287
|
+
}))
|
|
288
|
+
expect(parentElement.children.length).toBe(1)
|
|
289
|
+
const secondElement = parentElement.children[0]
|
|
290
|
+
expect(secondElement.textContent).toBe('Hello')
|
|
291
|
+
// Identity should be different because conditional nodes recreate on toggle
|
|
292
|
+
expect(secondElement === firstElement).toBeFalsy()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('nested repeats should maintain node references for unchanged items', () => {
|
|
296
|
+
const parentElement = document.createElement('div')
|
|
297
|
+
document.body.appendChild(parentElement)
|
|
298
|
+
|
|
299
|
+
const itemA = {
|
|
300
|
+
id: 'A',
|
|
301
|
+
subItems: [
|
|
302
|
+
{ id: '1', val: 'A1' },
|
|
303
|
+
{ id: '2', val: 'A2' },
|
|
304
|
+
],
|
|
305
|
+
}
|
|
306
|
+
const itemB = {
|
|
307
|
+
id: 'B',
|
|
308
|
+
subItems: [
|
|
309
|
+
{ id: '3', val: 'B3' },
|
|
310
|
+
{ id: '4', val: 'B4' },
|
|
311
|
+
],
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const dataSignal = signal<ComponentData>({
|
|
315
|
+
Attributes: {},
|
|
316
|
+
Variables: {
|
|
317
|
+
items: [itemA, itemB],
|
|
318
|
+
},
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
const ctx = {
|
|
322
|
+
isRootComponent: false,
|
|
323
|
+
component: {
|
|
324
|
+
name: 'My Component',
|
|
325
|
+
nodes: {
|
|
326
|
+
'outer-repeat': {
|
|
327
|
+
type: 'element',
|
|
328
|
+
tag: 'div',
|
|
329
|
+
repeat: { type: 'path', path: ['Variables', 'items'] },
|
|
330
|
+
repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
|
|
331
|
+
children: ['inner-repeat'],
|
|
332
|
+
attrs: {
|
|
333
|
+
class: { type: 'path', path: ['ListItem', 'Item', 'id'] },
|
|
334
|
+
},
|
|
335
|
+
events: {},
|
|
336
|
+
},
|
|
337
|
+
'inner-repeat': {
|
|
338
|
+
type: 'element',
|
|
339
|
+
tag: 'span',
|
|
340
|
+
repeat: { type: 'path', path: ['ListItem', 'Item', 'subItems'] },
|
|
341
|
+
repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
|
|
342
|
+
children: ['text-node'],
|
|
343
|
+
attrs: {},
|
|
344
|
+
events: {},
|
|
345
|
+
},
|
|
346
|
+
'text-node': {
|
|
347
|
+
type: 'text',
|
|
348
|
+
value: { type: 'path', path: ['ListItem', 'Item', 'val'] },
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
root: document,
|
|
353
|
+
env: { runtime: 'page' },
|
|
354
|
+
formulaCache: {},
|
|
355
|
+
toddle: {
|
|
356
|
+
getCustomFormula: () => undefined,
|
|
357
|
+
},
|
|
358
|
+
} as any as ComponentContext
|
|
359
|
+
|
|
360
|
+
const nodes = createNode({
|
|
361
|
+
ctx,
|
|
362
|
+
namespace: 'http://www.w3.org/1999/xhtml',
|
|
363
|
+
dataSignal,
|
|
364
|
+
path: 'nested',
|
|
365
|
+
id: 'outer-repeat',
|
|
366
|
+
parentElement,
|
|
367
|
+
instance: {},
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
parentElement.append(...nodes)
|
|
371
|
+
|
|
372
|
+
const outerA = parentElement.querySelector('.A')!
|
|
373
|
+
const outerB = parentElement.querySelector('.B')!
|
|
374
|
+
const innerA1 = outerA.children[0]
|
|
375
|
+
const innerB3 = outerB.children[0]
|
|
376
|
+
|
|
377
|
+
expect(innerA1.textContent).toBe('A1')
|
|
378
|
+
expect(innerB3.textContent).toBe('B3')
|
|
379
|
+
|
|
380
|
+
// Shuffle outer items
|
|
381
|
+
dataSignal.update((data) => ({
|
|
382
|
+
...data,
|
|
383
|
+
Variables: {
|
|
384
|
+
...data.Variables,
|
|
385
|
+
items: [itemB, itemA],
|
|
386
|
+
},
|
|
387
|
+
}))
|
|
388
|
+
|
|
389
|
+
expect(parentElement.children[0] === outerB).toBeTruthy()
|
|
390
|
+
expect(parentElement.children[1] === outerA).toBeTruthy()
|
|
391
|
+
|
|
392
|
+
// Inner items should still be the same objects
|
|
393
|
+
expect(outerB.children[0] === innerB3).toBeTruthy()
|
|
394
|
+
expect(outerA.children[0] === innerA1).toBeTruthy()
|
|
395
|
+
|
|
396
|
+
// Update subItems of B
|
|
397
|
+
const newItemB = {
|
|
398
|
+
...itemB,
|
|
399
|
+
subItems: [
|
|
400
|
+
{ id: '4', val: 'B4' },
|
|
401
|
+
{ id: '3', val: 'B3' },
|
|
402
|
+
],
|
|
403
|
+
}
|
|
404
|
+
dataSignal.update((data) => ({
|
|
405
|
+
...data,
|
|
406
|
+
Variables: {
|
|
407
|
+
...data.Variables,
|
|
408
|
+
items: [newItemB, itemA],
|
|
409
|
+
},
|
|
410
|
+
}))
|
|
411
|
+
|
|
412
|
+
// Outer B should still be same reference?
|
|
413
|
+
// Wait, createNode reuses by key. newItemB has same ID 'B'.
|
|
414
|
+
expect(parentElement.children[0] === outerB).toBeTruthy()
|
|
415
|
+
|
|
416
|
+
// Inner sequence of B should have changed, but inner elements preserved
|
|
417
|
+
expect(outerB.children[0].textContent).toBe('B4')
|
|
418
|
+
expect(outerB.children[1].textContent).toBe('B3')
|
|
419
|
+
expect(outerB.children[1] === innerB3).toBeTruthy()
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
test('repeat nodes should handle prepending items efficiently', () => {
|
|
423
|
+
const parentElement = document.createElement('div')
|
|
424
|
+
document.body.appendChild(parentElement)
|
|
425
|
+
|
|
426
|
+
const item2 = { id: '2' }
|
|
427
|
+
const item3 = { id: '3' }
|
|
428
|
+
|
|
429
|
+
const dataSignal = signal<ComponentData>({
|
|
430
|
+
Attributes: {},
|
|
431
|
+
Variables: { items: [item2, item3] },
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
const ctx = {
|
|
435
|
+
isRootComponent: false,
|
|
436
|
+
component: {
|
|
437
|
+
name: 'My Component',
|
|
438
|
+
nodes: {
|
|
439
|
+
repeat: {
|
|
440
|
+
type: 'element',
|
|
441
|
+
tag: 'div',
|
|
442
|
+
repeat: { type: 'path', path: ['Variables', 'items'] },
|
|
443
|
+
repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
|
|
444
|
+
children: ['text'],
|
|
445
|
+
attrs: {},
|
|
446
|
+
events: {},
|
|
447
|
+
},
|
|
448
|
+
text: {
|
|
449
|
+
type: 'text',
|
|
450
|
+
value: { type: 'path', path: ['ListItem', 'Item', 'id'] },
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
root: document,
|
|
455
|
+
env: { runtime: 'page' },
|
|
456
|
+
formulaCache: {},
|
|
457
|
+
toddle: { getCustomFormula: () => undefined },
|
|
458
|
+
} as any as ComponentContext
|
|
459
|
+
|
|
460
|
+
const nodes = createNode({
|
|
461
|
+
ctx,
|
|
462
|
+
namespace: 'http://www.w3.org/1999/xhtml',
|
|
463
|
+
dataSignal,
|
|
464
|
+
path: 'prepend',
|
|
465
|
+
id: 'repeat',
|
|
466
|
+
parentElement,
|
|
467
|
+
instance: {},
|
|
468
|
+
})
|
|
469
|
+
parentElement.append(...nodes)
|
|
470
|
+
|
|
471
|
+
const el2 = parentElement.children[0]
|
|
472
|
+
const el3 = parentElement.children[1]
|
|
473
|
+
|
|
474
|
+
// Prepend item 1
|
|
475
|
+
const item1 = { id: '1' }
|
|
476
|
+
dataSignal.update((data) => ({
|
|
477
|
+
...data,
|
|
478
|
+
Variables: { ...data.Variables, items: [item1, item2, item3] },
|
|
479
|
+
}))
|
|
480
|
+
|
|
481
|
+
expect(parentElement.children.length).toBe(3)
|
|
482
|
+
expect(parentElement.children[0].textContent).toBe('1')
|
|
483
|
+
expect(parentElement.children[1] === el2).toBeTruthy()
|
|
484
|
+
expect(parentElement.children[2] === el3).toBeTruthy()
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
test('repeat nodes with duplicate keys should still render all items but without reuse optimizations', () => {
|
|
488
|
+
const parentElement = document.createElement('div')
|
|
489
|
+
document.body.appendChild(parentElement)
|
|
490
|
+
|
|
491
|
+
const item1 = { id: 'i1', name: 'Item 1' }
|
|
492
|
+
const item2 = { id: 'i2', name: 'Item 2' }
|
|
493
|
+
const item3 = { id: 'i3', name: 'Item 3' }
|
|
494
|
+
|
|
495
|
+
const dataSignal = signal<ComponentData>({
|
|
496
|
+
Attributes: {},
|
|
497
|
+
Variables: {
|
|
498
|
+
items: [],
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const ctx = {
|
|
503
|
+
isRootComponent: false,
|
|
504
|
+
component: {
|
|
505
|
+
name: 'My Component',
|
|
506
|
+
nodes: {
|
|
507
|
+
repeat: {
|
|
508
|
+
type: 'element',
|
|
509
|
+
tag: 'div',
|
|
510
|
+
repeat: { type: 'path', path: ['Variables', 'items'] },
|
|
511
|
+
repeatKey: { type: 'path', path: ['ListItem', 'Item', 'id'] },
|
|
512
|
+
children: ['text'],
|
|
513
|
+
attrs: {},
|
|
514
|
+
events: {},
|
|
515
|
+
},
|
|
516
|
+
text: {
|
|
517
|
+
type: 'text',
|
|
518
|
+
value: { type: 'path', path: ['ListItem', 'Item', 'name'] },
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
root: document,
|
|
523
|
+
env: { runtime: 'page' },
|
|
524
|
+
formulaCache: {},
|
|
525
|
+
toddle: { getCustomFormula: () => undefined },
|
|
526
|
+
} as any as ComponentContext
|
|
527
|
+
|
|
528
|
+
const nodes = createNode({
|
|
529
|
+
ctx,
|
|
530
|
+
namespace: 'http://www.w3.org/1999/xhtml',
|
|
531
|
+
dataSignal,
|
|
532
|
+
id: 'repeat',
|
|
533
|
+
path: '0',
|
|
534
|
+
instance: {},
|
|
535
|
+
parentElement,
|
|
536
|
+
})
|
|
537
|
+
parentElement.append(...nodes)
|
|
538
|
+
|
|
539
|
+
// Update with duplicate keys
|
|
540
|
+
dataSignal.update((data) => ({
|
|
541
|
+
...data,
|
|
542
|
+
Variables: { ...data.Variables, items: [item1] },
|
|
543
|
+
}))
|
|
544
|
+
|
|
545
|
+
expect(parentElement.children.length).toBe(1)
|
|
546
|
+
expect(parentElement.children[0].textContent).toBe('Item 1')
|
|
547
|
+
|
|
548
|
+
// Update with all items, including duplicate key
|
|
549
|
+
dataSignal.update((data) => ({
|
|
550
|
+
...data,
|
|
551
|
+
Variables: { ...data.Variables, items: [item1, item2] },
|
|
552
|
+
}))
|
|
553
|
+
|
|
554
|
+
dataSignal.update((data) => ({
|
|
555
|
+
...data,
|
|
556
|
+
Variables: { ...data.Variables, items: [item1, item2, item3] },
|
|
557
|
+
}))
|
|
558
|
+
|
|
559
|
+
expect(parentElement.children.length).toBe(3)
|
|
560
|
+
expect(parentElement.children[0].textContent).toBe('Item 1')
|
|
561
|
+
expect(parentElement.children[1].textContent).toBe('Item 2')
|
|
562
|
+
expect(parentElement.children[2].textContent).toBe('Item 3')
|
|
563
|
+
|
|
564
|
+
expect(parentElement.children[0].getAttribute('data-id')).toBe('0')
|
|
565
|
+
expect(parentElement.children[1].getAttribute('data-id')).toBe('0(1)')
|
|
566
|
+
expect(parentElement.children[2].getAttribute('data-id')).toBe('0(2)')
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
test('it should have correct order of custom properties overrides if a component root has deep instance styling', () => {
|
|
570
|
+
const parentElement = document.createElement('div')
|
|
571
|
+
document.body.appendChild(parentElement)
|
|
572
|
+
|
|
573
|
+
const outermostColor = 'red'
|
|
574
|
+
const middleColor = 'blue'
|
|
575
|
+
const innermostColor = 'green'
|
|
576
|
+
|
|
577
|
+
const ctx: ComponentContext = {
|
|
578
|
+
isRootComponent: false,
|
|
579
|
+
component: {
|
|
580
|
+
name: 'My_Component',
|
|
581
|
+
nodes: {
|
|
582
|
+
root: {
|
|
583
|
+
type: 'component',
|
|
584
|
+
attrs: {},
|
|
585
|
+
events: {},
|
|
586
|
+
children: [],
|
|
587
|
+
name: 'first_wrapper',
|
|
588
|
+
customProperties: {
|
|
589
|
+
'--color': {
|
|
590
|
+
syntax: {
|
|
591
|
+
type: 'primitive',
|
|
592
|
+
name: 'color',
|
|
593
|
+
},
|
|
594
|
+
formula: {
|
|
595
|
+
type: 'value',
|
|
596
|
+
value: 'red',
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
root: document,
|
|
604
|
+
formulaCache: {},
|
|
605
|
+
env: { runtime: 'preview' } as ToddleEnv,
|
|
606
|
+
stores: {
|
|
607
|
+
theme: signal(null),
|
|
608
|
+
},
|
|
609
|
+
components: [
|
|
610
|
+
{
|
|
611
|
+
name: 'first_wrapper',
|
|
612
|
+
nodes: {
|
|
613
|
+
root: {
|
|
614
|
+
type: 'component',
|
|
615
|
+
attrs: {},
|
|
616
|
+
events: {},
|
|
617
|
+
children: [],
|
|
618
|
+
name: 'second_wrapper',
|
|
619
|
+
customProperties: {
|
|
620
|
+
'--color': {
|
|
621
|
+
syntax: {
|
|
622
|
+
type: 'primitive',
|
|
623
|
+
name: 'color',
|
|
624
|
+
},
|
|
625
|
+
formula: {
|
|
626
|
+
type: 'value',
|
|
627
|
+
value: 'blue',
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
name: 'second_wrapper',
|
|
636
|
+
nodes: {
|
|
637
|
+
root: {
|
|
638
|
+
type: 'element',
|
|
639
|
+
tag: 'div',
|
|
640
|
+
children: ['text-node'],
|
|
641
|
+
attrs: {},
|
|
642
|
+
events: {},
|
|
643
|
+
customProperties: {
|
|
644
|
+
'--color': {
|
|
645
|
+
syntax: {
|
|
646
|
+
type: 'primitive',
|
|
647
|
+
name: 'color',
|
|
648
|
+
},
|
|
649
|
+
formula: {
|
|
650
|
+
type: 'value',
|
|
651
|
+
value: 'green',
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
'text-node': {
|
|
657
|
+
type: 'text',
|
|
658
|
+
value: { type: 'value', value: 'Hello' },
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
} as Partial<ComponentContext> as ComponentContext
|
|
664
|
+
|
|
665
|
+
createNode({
|
|
666
|
+
ctx,
|
|
667
|
+
namespace: 'http://www.w3.org/1999/xhtml',
|
|
668
|
+
dataSignal: signal<ComponentData>({
|
|
669
|
+
Attributes: {},
|
|
670
|
+
Variables: {},
|
|
671
|
+
}),
|
|
672
|
+
path: '0',
|
|
673
|
+
id: 'root',
|
|
674
|
+
parentElement,
|
|
675
|
+
instance: {},
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
const sheet = customPropertiesStylesheet?.getStyleSheet()
|
|
679
|
+
// Test that the order makes sense
|
|
680
|
+
expect(Array.from(sheet?.cssRules ?? []).map((r) => r.cssText)).toEqual([
|
|
681
|
+
`[data-id="0"] { --color: ${innermostColor}; }`,
|
|
682
|
+
`[data-id="0"].first_wrapper\\:root { --color: ${middleColor}; }`,
|
|
683
|
+
`[data-id="0"].My_Component\\:root { --color: ${outermostColor}; }`,
|
|
684
|
+
])
|
|
685
|
+
})
|
|
686
|
+
})
|