@node-projects/web-component-designer-visualization-addons 0.1.152 → 0.3.0
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/components/BindingsEditor.js +2 -2
- package/dist/components/BindingsEditorHelp.d.ts +1 -1
- package/dist/components/BindingsEditorHelp.js +4 -0
- package/dist/components/EventAssignment.d.ts +1 -0
- package/dist/components/EventAssignment.js +5 -2
- package/dist/helpers/BindingsHelper.d.ts +25 -13
- package/dist/helpers/BindingsHelper.js +602 -605
- package/dist/index-min.js +52 -52
- package/dist/services/VisualizationBindingsRefactorService.js +20 -23
- package/package.json +1 -1
- package/tests/BindingsHelper.test.ts +527 -0
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
const sourcePrefixes = [
|
|
2
|
+
{ prefix: '?@', itemType: 'attribute' },
|
|
3
|
+
{ prefix: '#@', itemType: 'attribute' },
|
|
4
|
+
{ prefix: '?$', itemType: 'signalObject' },
|
|
5
|
+
{ prefix: '#$', itemType: 'signalObject' },
|
|
6
|
+
{ prefix: '??', itemType: 'property' },
|
|
7
|
+
{ prefix: '##', itemType: 'property' },
|
|
8
|
+
{ prefix: '?', itemType: 'property' },
|
|
9
|
+
{ prefix: '#', itemType: 'property' }
|
|
10
|
+
];
|
|
11
|
+
function parseSource(source) {
|
|
12
|
+
const match = sourcePrefixes.find(x => source.startsWith(x.prefix));
|
|
13
|
+
return match
|
|
14
|
+
? { name: source.substring(match.prefix.length), ...match }
|
|
15
|
+
: { name: source, prefix: '', itemType: 'signal' };
|
|
16
|
+
}
|
|
1
17
|
export class VisualizationBindingsRefactorService {
|
|
2
18
|
getRefactorings(designItems) {
|
|
3
19
|
let refactorings = [];
|
|
@@ -6,33 +22,14 @@ export class VisualizationBindingsRefactorService {
|
|
|
6
22
|
if (bindings) {
|
|
7
23
|
for (let b of bindings) {
|
|
8
24
|
for (let s of b.bindableObjectNames) {
|
|
9
|
-
let itemType = 'signal';
|
|
10
|
-
let prefix = "";
|
|
11
25
|
if (s.includes(':')) {
|
|
12
26
|
let nm = s.split(':')[0];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
sng = sng.substring(1);
|
|
16
|
-
prefix = '?';
|
|
17
|
-
itemType = 'property';
|
|
18
|
-
if (sng.startsWith('?')) {
|
|
19
|
-
sng = sng.substring(1);
|
|
20
|
-
prefix = '??';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
refactorings.push({ service: this, name: sng, itemType: itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName + ' - ' + nm + ':', shortName: nm, prefix: prefix });
|
|
27
|
+
const source = parseSource(s.substring(nm.length + 1));
|
|
28
|
+
refactorings.push({ service: this, name: source.name, itemType: source.itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName + ' - ' + nm + ':', shortName: nm, prefix: source.prefix });
|
|
24
29
|
}
|
|
25
30
|
else {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
prefix = '?';
|
|
29
|
-
itemType = 'property';
|
|
30
|
-
if (s.startsWith('?')) {
|
|
31
|
-
s = s.substring(1);
|
|
32
|
-
prefix = '??';
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
refactorings.push({ service: this, name: s, itemType: itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName, prefix: prefix });
|
|
31
|
+
const source = parseSource(s);
|
|
32
|
+
refactorings.push({ service: this, name: source.name, itemType: source.itemType, designItem: d, type: 'binding', sourceObject: b, display: b.target + '/' + b.targetName, prefix: source.prefix });
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "web-component-designer addon for visualizations",
|
|
3
3
|
"name": "@node-projects/web-component-designer-visualization-addons",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"author": "jochen.kuehner@gmx.de",
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
|
|
3
|
+
import { beforeAll, describe, expect, test } from '@jest/globals';
|
|
4
|
+
import type { SpecialValueHandler } from '../src/helpers/BindingsHelper.js';
|
|
5
|
+
import type { VisualizationHandler, State } from '../src/interfaces/VisualizationHandler.js';
|
|
6
|
+
import { BindingTarget } from '@node-projects/web-component-designer/dist/elements/item/BindingTarget.js';
|
|
7
|
+
|
|
8
|
+
let BindingsHelper: typeof import('../src/helpers/BindingsHelper.js').BindingsHelper;
|
|
9
|
+
let parseBindingString: typeof import('../src/helpers/BindingsHelper.js').parseBindingString;
|
|
10
|
+
let VisualizationBindingsRefactorService: typeof import('../src/services/VisualizationBindingsRefactorService.js').VisualizationBindingsRefactorService;
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
if (!CSSStyleSheet.prototype.replaceSync)
|
|
14
|
+
CSSStyleSheet.prototype.replaceSync = () => { };
|
|
15
|
+
({ BindingsHelper, parseBindingString } = await import('../src/helpers/BindingsHelper.js'));
|
|
16
|
+
({ VisualizationBindingsRefactorService } = await import('../src/services/VisualizationBindingsRefactorService.js'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
class TestVisualizationHandler implements VisualizationHandler {
|
|
20
|
+
values = new Map<string, any>();
|
|
21
|
+
callbacks = new Map<string, Set<(id: string, state: State) => void>>();
|
|
22
|
+
subscribeCalls: string[] = [];
|
|
23
|
+
unsubscribeCalls: string[] = [];
|
|
24
|
+
getStateCalls: string[] = [];
|
|
25
|
+
setStateCalls: { id: string, value: any, ack?: boolean }[] = [];
|
|
26
|
+
objects = new Map<string, any>();
|
|
27
|
+
getObjectCalls: string[] = [];
|
|
28
|
+
|
|
29
|
+
getNormalizedSignalName(id: string, relativeSignalPath = '') {
|
|
30
|
+
return relativeSignalPath + id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getState(id: string): Promise<State> {
|
|
34
|
+
this.getStateCalls.push(id);
|
|
35
|
+
return { val: this.values.get(id) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async setState(id: string, value: any, ack?: boolean): Promise<void> {
|
|
39
|
+
this.setStateCalls.push({ id, value, ack });
|
|
40
|
+
this.values.set(id, value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
subscribeState(id: string, callback: (id: string, state: State) => void) {
|
|
44
|
+
this.subscribeCalls.push(id);
|
|
45
|
+
let callbacks = this.callbacks.get(id);
|
|
46
|
+
if (!callbacks) {
|
|
47
|
+
callbacks = new Set();
|
|
48
|
+
this.callbacks.set(id, callbacks);
|
|
49
|
+
}
|
|
50
|
+
callbacks.add(callback);
|
|
51
|
+
return callback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
unsubscribeState(id: string, callback: (id: string, state: State) => void) {
|
|
55
|
+
this.unsubscribeCalls.push(id);
|
|
56
|
+
this.callbacks.get(id)?.delete(callback);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
emit(id: string, value: any) {
|
|
60
|
+
this.values.set(id, value);
|
|
61
|
+
for (const callback of this.callbacks.get(id) ?? [])
|
|
62
|
+
callback(id, { val: value });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getHistoricData() {
|
|
66
|
+
return { values: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getObject(id: string): Promise<any> {
|
|
70
|
+
this.getObjectCalls.push(id);
|
|
71
|
+
return this.objects.get(id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getAllNames() {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getSignalInformation(): any {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const flushMicrotasks = async () => {
|
|
84
|
+
await Promise.resolve();
|
|
85
|
+
await Promise.resolve();
|
|
86
|
+
await Promise.resolve();
|
|
87
|
+
await Promise.resolve();
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
describe('BindingsHelper runtime', () => {
|
|
91
|
+
test('shares subscriptions and initial reads for the same signal', async () => {
|
|
92
|
+
const handler = new TestVisualizationHandler();
|
|
93
|
+
handler.values.set('shared', 7);
|
|
94
|
+
const helper = new BindingsHelper(handler);
|
|
95
|
+
const first = document.createElement('div');
|
|
96
|
+
const second = document.createElement('div');
|
|
97
|
+
|
|
98
|
+
const disposeFirst = helper.applyBinding(first, ['value', { signal: 'shared', target: BindingTarget.property }], '', null);
|
|
99
|
+
const disposeSecond = helper.applyBinding(second, ['value', { signal: 'shared', target: BindingTarget.property }], '', null);
|
|
100
|
+
await flushMicrotasks();
|
|
101
|
+
|
|
102
|
+
expect(handler.subscribeCalls).toEqual(['shared']);
|
|
103
|
+
expect(handler.getStateCalls).toEqual(['shared']);
|
|
104
|
+
expect(first['value']).toBe(7);
|
|
105
|
+
expect(second['value']).toBe(7);
|
|
106
|
+
|
|
107
|
+
disposeFirst();
|
|
108
|
+
expect(handler.unsubscribeCalls).toEqual([]);
|
|
109
|
+
disposeSecond();
|
|
110
|
+
expect(handler.unsubscribeCalls).toEqual(['shared']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('switches only the changed dynamic source', async () => {
|
|
114
|
+
const handler = new TestVisualizationHandler();
|
|
115
|
+
handler.values.set('first', 1);
|
|
116
|
+
handler.values.set('second', 2);
|
|
117
|
+
handler.values.set('fixed', 10);
|
|
118
|
+
const helper = new BindingsHelper(handler);
|
|
119
|
+
const root = document.createElement('div');
|
|
120
|
+
const element = document.createElement('div');
|
|
121
|
+
root['signalName'] = 'first';
|
|
122
|
+
|
|
123
|
+
const dispose = helper.applyBinding(element, ['value', {
|
|
124
|
+
signal: 'dynamic:?signalName;fixed:fixed',
|
|
125
|
+
expression: 'dynamic + fixed',
|
|
126
|
+
twoWay: true,
|
|
127
|
+
events: ['change'],
|
|
128
|
+
target: BindingTarget.property
|
|
129
|
+
}], '', root);
|
|
130
|
+
await flushMicrotasks();
|
|
131
|
+
expect(element['value']).toBe(11);
|
|
132
|
+
|
|
133
|
+
root['signalName'] = 'second';
|
|
134
|
+
root.dispatchEvent(new Event('signal-name-changed'));
|
|
135
|
+
await flushMicrotasks();
|
|
136
|
+
|
|
137
|
+
expect(element['value']).toBe(12);
|
|
138
|
+
expect(handler.subscribeCalls).toEqual(['first', 'fixed', 'second']);
|
|
139
|
+
expect(handler.unsubscribeCalls).toEqual(['first']);
|
|
140
|
+
|
|
141
|
+
root.dispatchEvent(new Event('signal-name-changed'));
|
|
142
|
+
await flushMicrotasks();
|
|
143
|
+
expect(handler.subscribeCalls).toEqual(['first', 'fixed', 'second']);
|
|
144
|
+
expect(handler.unsubscribeCalls).toEqual(['first']);
|
|
145
|
+
|
|
146
|
+
element['value'] = 20;
|
|
147
|
+
element.dispatchEvent(new Event('change'));
|
|
148
|
+
expect(handler.setStateCalls).toEqual([{ id: 'second', value: 20, ack: undefined }]);
|
|
149
|
+
|
|
150
|
+
dispose();
|
|
151
|
+
expect(handler.unsubscribeCalls).toEqual(['first', 'fixed', 'second']);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('switches an indirect property source without rebuilding its binding', async () => {
|
|
155
|
+
const handler = new TestVisualizationHandler();
|
|
156
|
+
handler.values.set('selector-one', 'one');
|
|
157
|
+
handler.values.set('selector-two', 'two');
|
|
158
|
+
handler.values.set('device.one', 1);
|
|
159
|
+
handler.values.set('device.two', 2);
|
|
160
|
+
const helper = new BindingsHelper(handler);
|
|
161
|
+
const root = document.createElement('div');
|
|
162
|
+
const element = document.createElement('div');
|
|
163
|
+
root['selectorSignal'] = 'selector-one';
|
|
164
|
+
|
|
165
|
+
const dispose = helper.applyBinding(element, ['value', {
|
|
166
|
+
signal: 'device.{?selectorSignal}',
|
|
167
|
+
target: BindingTarget.property
|
|
168
|
+
}], '', root);
|
|
169
|
+
await flushMicrotasks();
|
|
170
|
+
expect(element['value']).toBe(1);
|
|
171
|
+
|
|
172
|
+
root['selectorSignal'] = 'selector-two';
|
|
173
|
+
root.dispatchEvent(new Event('selector-signal-changed'));
|
|
174
|
+
await flushMicrotasks();
|
|
175
|
+
expect(element['value']).toBe(2);
|
|
176
|
+
expect(handler.subscribeCalls).toEqual(['selector-one', 'device.one', 'selector-two', 'device.two']);
|
|
177
|
+
expect(handler.unsubscribeCalls).toEqual(['selector-one', 'device.one']);
|
|
178
|
+
dispose();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('binds screen and target attributes and observes their changes', async () => {
|
|
182
|
+
const handler = new TestVisualizationHandler();
|
|
183
|
+
const helper = new BindingsHelper(handler);
|
|
184
|
+
const root = document.createElement('div');
|
|
185
|
+
const element = document.createElement('input');
|
|
186
|
+
root.setAttribute('screen-value', 'one');
|
|
187
|
+
element.setAttribute('target-value', 'two');
|
|
188
|
+
|
|
189
|
+
const disposeScreen = helper.applyBinding(element, ['screenValue', {
|
|
190
|
+
signal: '?@screen-value',
|
|
191
|
+
twoWay: true,
|
|
192
|
+
events: ['change'],
|
|
193
|
+
target: BindingTarget.property
|
|
194
|
+
}], '', root);
|
|
195
|
+
const disposeTarget = helper.applyBinding(element, ['targetValue', {
|
|
196
|
+
signal: '#@target-value',
|
|
197
|
+
target: BindingTarget.property
|
|
198
|
+
}], '', root);
|
|
199
|
+
|
|
200
|
+
expect(element['screenValue']).toBe('one');
|
|
201
|
+
expect(element['targetValue']).toBe('two');
|
|
202
|
+
expect(handler.subscribeCalls).toEqual([]);
|
|
203
|
+
|
|
204
|
+
root.setAttribute('screen-value', 'three');
|
|
205
|
+
element.setAttribute('target-value', 'four');
|
|
206
|
+
await flushMicrotasks();
|
|
207
|
+
expect(element['screenValue']).toBe('three');
|
|
208
|
+
expect(element['targetValue']).toBe('four');
|
|
209
|
+
|
|
210
|
+
element['screenValue'] = 'five';
|
|
211
|
+
element.dispatchEvent(new Event('change'));
|
|
212
|
+
expect(root.getAttribute('screen-value')).toBe('five');
|
|
213
|
+
|
|
214
|
+
disposeScreen();
|
|
215
|
+
disposeTarget();
|
|
216
|
+
root.setAttribute('screen-value', 'ignored');
|
|
217
|
+
element.setAttribute('target-value', 'ignored');
|
|
218
|
+
await flushMicrotasks();
|
|
219
|
+
expect(element['screenValue']).toBe('five');
|
|
220
|
+
expect(element['targetValue']).toBe('four');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('binds signal objects stored in screen and target properties', async () => {
|
|
224
|
+
const handler = new TestVisualizationHandler();
|
|
225
|
+
handler.objects.set('named-object', { id: 'named-object' });
|
|
226
|
+
const helper = new BindingsHelper(handler);
|
|
227
|
+
const root = document.createElement('div');
|
|
228
|
+
const screenElement = document.createElement('div');
|
|
229
|
+
const targetElement = document.createElement('div');
|
|
230
|
+
const namedElement = document.createElement('div');
|
|
231
|
+
root['signalObject'] = { id: 'screen-object' };
|
|
232
|
+
targetElement['signalObject'] = { id: 'target-object' };
|
|
233
|
+
|
|
234
|
+
const disposeScreen = helper.applyBinding(screenElement, ['value', {
|
|
235
|
+
signal: '?$signalObject',
|
|
236
|
+
target: BindingTarget.property
|
|
237
|
+
}], '', root);
|
|
238
|
+
const disposeTarget = helper.applyBinding(targetElement, ['value', {
|
|
239
|
+
signal: '#$signalObject',
|
|
240
|
+
target: BindingTarget.property
|
|
241
|
+
}], '', root);
|
|
242
|
+
const disposeNamed = helper.applyBinding(namedElement, ['value', {
|
|
243
|
+
signal: '$named-object',
|
|
244
|
+
target: BindingTarget.property
|
|
245
|
+
}], '', root);
|
|
246
|
+
await flushMicrotasks();
|
|
247
|
+
expect(screenElement['value']).toEqual({ id: 'screen-object' });
|
|
248
|
+
expect(targetElement['value']).toEqual({ id: 'target-object' });
|
|
249
|
+
expect(namedElement['value']).toEqual({ id: 'named-object' });
|
|
250
|
+
|
|
251
|
+
root['signalObject'] = { id: 'next-object' };
|
|
252
|
+
root.dispatchEvent(new Event('signal-object-changed'));
|
|
253
|
+
await flushMicrotasks();
|
|
254
|
+
expect(screenElement['value']).toEqual({ id: 'next-object' });
|
|
255
|
+
expect(handler.getObjectCalls).toEqual(['named-object']);
|
|
256
|
+
|
|
257
|
+
disposeScreen();
|
|
258
|
+
disposeTarget();
|
|
259
|
+
disposeNamed();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('uses screen and target attributes in indirect signal paths', async () => {
|
|
263
|
+
const handler = new TestVisualizationHandler();
|
|
264
|
+
handler.values.set('device.one', 1);
|
|
265
|
+
handler.values.set('device.two', 2);
|
|
266
|
+
handler.values.set('device.three', 3);
|
|
267
|
+
handler.values.set('device.four', 4);
|
|
268
|
+
const helper = new BindingsHelper(handler);
|
|
269
|
+
const root = document.createElement('div');
|
|
270
|
+
const screenElement = document.createElement('div');
|
|
271
|
+
const targetElement = document.createElement('div');
|
|
272
|
+
root.setAttribute('device-id', 'one');
|
|
273
|
+
targetElement.setAttribute('device-id', 'three');
|
|
274
|
+
|
|
275
|
+
const disposeScreen = helper.applyBinding(screenElement, ['value', {
|
|
276
|
+
signal: 'device.{?@device-id}',
|
|
277
|
+
target: BindingTarget.property
|
|
278
|
+
}], '', root);
|
|
279
|
+
const disposeTarget = helper.applyBinding(targetElement, ['value', {
|
|
280
|
+
signal: 'device.{#@device-id}',
|
|
281
|
+
target: BindingTarget.property
|
|
282
|
+
}], '', root);
|
|
283
|
+
await flushMicrotasks();
|
|
284
|
+
expect(screenElement['value']).toBe(1);
|
|
285
|
+
expect(targetElement['value']).toBe(3);
|
|
286
|
+
|
|
287
|
+
root.setAttribute('device-id', 'two');
|
|
288
|
+
targetElement.setAttribute('device-id', 'four');
|
|
289
|
+
await flushMicrotasks();
|
|
290
|
+
expect(screenElement['value']).toBe(2);
|
|
291
|
+
expect(targetElement['value']).toBe(4);
|
|
292
|
+
expect(handler.unsubscribeCalls).toEqual(['device.one', 'device.three']);
|
|
293
|
+
|
|
294
|
+
disposeScreen();
|
|
295
|
+
disposeTarget();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('keeps serialization output stable while sharing target-specific logic', () => {
|
|
299
|
+
const helper = new BindingsHelper(new TestVisualizationHandler());
|
|
300
|
+
const element = document.createElement('div');
|
|
301
|
+
|
|
302
|
+
expect(helper.serializeBinding(element, 'value', { signal: 'source', target: BindingTarget.property }))
|
|
303
|
+
.toEqual(['bind-prop:value', 'source']);
|
|
304
|
+
expect(helper.serializeBinding(element, 'dataValue', { signal: 'source', target: BindingTarget.attribute }))
|
|
305
|
+
.toEqual(['bind-attr:data-value', 'source']);
|
|
306
|
+
expect(helper.serializeBinding(element, 'isActive', { signal: 'source', target: BindingTarget.class }))
|
|
307
|
+
.toEqual(['bind-class:is-active', 'source']);
|
|
308
|
+
expect(helper.serializeBinding(element, 'backgroundColor', { signal: 'source', target: BindingTarget.css }))
|
|
309
|
+
.toEqual(['bind-css:background-color', 'source']);
|
|
310
|
+
expect(helper.serializeBinding(element, '--mainColor', { signal: 'source', target: BindingTarget.cssvar }))
|
|
311
|
+
.toEqual(['bind-cssvar:main.color', 'source']);
|
|
312
|
+
expect(helper.serializeBinding(element, 'value', {
|
|
313
|
+
signal: 'source',
|
|
314
|
+
converter: { true: 'yes' },
|
|
315
|
+
target: BindingTarget.property
|
|
316
|
+
})).toEqual(['bind-prop:value', '{"signal":"source","converter":{"true":"yes"}}']);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('coalesces bursts and skips identical primitive DOM and write-back updates', async () => {
|
|
320
|
+
const handler = new TestVisualizationHandler();
|
|
321
|
+
handler.values.set('sensor', 1);
|
|
322
|
+
const helper = new BindingsHelper(handler);
|
|
323
|
+
const element = document.createElement('div');
|
|
324
|
+
let writes = 0;
|
|
325
|
+
let renderedValue: any;
|
|
326
|
+
Object.defineProperty(element, 'value', {
|
|
327
|
+
configurable: true,
|
|
328
|
+
get: () => renderedValue,
|
|
329
|
+
set: value => {
|
|
330
|
+
writes++;
|
|
331
|
+
renderedValue = value;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const dispose = helper.applyBinding(element, ['value', {
|
|
336
|
+
signal: 'sensor',
|
|
337
|
+
writeBackSignal: 'mirror',
|
|
338
|
+
target: BindingTarget.property
|
|
339
|
+
}], '', null);
|
|
340
|
+
await flushMicrotasks();
|
|
341
|
+
expect(writes).toBe(1);
|
|
342
|
+
expect(handler.setStateCalls).toEqual([{ id: 'mirror', value: 1, ack: true }]);
|
|
343
|
+
|
|
344
|
+
handler.emit('sensor', 1);
|
|
345
|
+
handler.emit('sensor', 1);
|
|
346
|
+
await flushMicrotasks();
|
|
347
|
+
expect(writes).toBe(1);
|
|
348
|
+
|
|
349
|
+
handler.emit('sensor', 2);
|
|
350
|
+
handler.emit('sensor', 3);
|
|
351
|
+
await flushMicrotasks();
|
|
352
|
+
expect(writes).toBe(2);
|
|
353
|
+
expect(renderedValue).toBe(3);
|
|
354
|
+
expect(handler.setStateCalls).toHaveLength(2);
|
|
355
|
+
expect(handler.setStateCalls[1]).toEqual({ id: 'mirror', value: 3, ack: true });
|
|
356
|
+
dispose();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('routes all target types through the shared target writer', async () => {
|
|
360
|
+
const handler = new TestVisualizationHandler();
|
|
361
|
+
handler.values.set('shared', 1);
|
|
362
|
+
const helper = new BindingsHelper(handler);
|
|
363
|
+
const propertyElement = document.createElement('div');
|
|
364
|
+
const attributeElement = document.createElement('div');
|
|
365
|
+
const classElement = document.createElement('div');
|
|
366
|
+
const cssElement = document.createElement('div');
|
|
367
|
+
const cssVarElement = document.createElement('div');
|
|
368
|
+
const visibleElement = document.createElement('div');
|
|
369
|
+
const disposers = [
|
|
370
|
+
helper.applyBinding(propertyElement, ['value', { signal: 'shared', target: BindingTarget.property }], '', null),
|
|
371
|
+
helper.applyBinding(attributeElement, ['data-value', { signal: 'shared', target: BindingTarget.attribute }], '', null),
|
|
372
|
+
helper.applyBinding(classElement, ['active', { signal: 'shared', target: BindingTarget.class }], '', null),
|
|
373
|
+
helper.applyBinding(cssElement, ['opacity', { signal: 'shared', target: BindingTarget.css }], '', null),
|
|
374
|
+
helper.applyBinding(cssVarElement, ['--amount', { signal: 'shared', target: BindingTarget.cssvar }], '', null),
|
|
375
|
+
helper.applyBinding(visibleElement, ['', { signal: 'shared', target: BindingTarget.visible }], '', null)
|
|
376
|
+
];
|
|
377
|
+
await flushMicrotasks();
|
|
378
|
+
|
|
379
|
+
expect(propertyElement['value']).toBe(1);
|
|
380
|
+
expect(attributeElement.getAttribute('data-value')).toBe('1');
|
|
381
|
+
expect(classElement.classList.contains('active')).toBe(true);
|
|
382
|
+
expect(cssElement.style.opacity).toBe('1');
|
|
383
|
+
expect(cssVarElement.style.getPropertyValue('--amount')).toBe('1');
|
|
384
|
+
expect(visibleElement.style.visibility).toBe('');
|
|
385
|
+
|
|
386
|
+
handler.emit('shared', 0);
|
|
387
|
+
await flushMicrotasks();
|
|
388
|
+
expect(classElement.classList.contains('active')).toBe(false);
|
|
389
|
+
expect(cssElement.style.opacity).toBe('0');
|
|
390
|
+
expect(visibleElement.style.visibility).toBe('collapse');
|
|
391
|
+
expect(handler.subscribeCalls).toEqual(['shared']);
|
|
392
|
+
for (const dispose of disposers)
|
|
393
|
+
dispose();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('removes native two-way listeners on disposal', async () => {
|
|
397
|
+
const handler = new TestVisualizationHandler();
|
|
398
|
+
handler.values.set('sensor', 1);
|
|
399
|
+
const helper = new BindingsHelper(handler);
|
|
400
|
+
const element = document.createElement('input');
|
|
401
|
+
const dispose = helper.applyBinding(element, ['value', {
|
|
402
|
+
signal: 'sensor',
|
|
403
|
+
twoWay: true,
|
|
404
|
+
events: ['change'],
|
|
405
|
+
type: 'number',
|
|
406
|
+
target: BindingTarget.property
|
|
407
|
+
}], '', null);
|
|
408
|
+
await flushMicrotasks();
|
|
409
|
+
|
|
410
|
+
element.value = '5';
|
|
411
|
+
element.dispatchEvent(new Event('change'));
|
|
412
|
+
expect(handler.setStateCalls.at(-1)).toEqual({ id: 'sensor', value: 5, ack: undefined });
|
|
413
|
+
|
|
414
|
+
dispose();
|
|
415
|
+
element.value = '6';
|
|
416
|
+
element.dispatchEvent(new Event('change'));
|
|
417
|
+
expect(handler.setStateCalls).toHaveLength(1);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('removes TypedEvent two-way listeners on disposal', async () => {
|
|
421
|
+
const { TypedEvent } = await import('@node-projects/base-custom-webcomponent');
|
|
422
|
+
const handler = new TestVisualizationHandler();
|
|
423
|
+
handler.values.set('sensor', 1);
|
|
424
|
+
const helper = new BindingsHelper(handler);
|
|
425
|
+
const element = document.createElement('div');
|
|
426
|
+
element['valueChanged'] = new TypedEvent<void>();
|
|
427
|
+
const dispose = helper.applyBinding(element, ['value', {
|
|
428
|
+
signal: 'sensor',
|
|
429
|
+
twoWay: true,
|
|
430
|
+
events: ['valueChanged'],
|
|
431
|
+
target: BindingTarget.property
|
|
432
|
+
}], '', null);
|
|
433
|
+
await flushMicrotasks();
|
|
434
|
+
|
|
435
|
+
element['value'] = 5;
|
|
436
|
+
element['valueChanged'].emit();
|
|
437
|
+
expect(handler.setStateCalls.at(-1)).toEqual({ id: 'sensor', value: 5, ack: undefined });
|
|
438
|
+
|
|
439
|
+
dispose();
|
|
440
|
+
element['value'] = 6;
|
|
441
|
+
element['valueChanged'].emit();
|
|
442
|
+
expect(handler.setStateCalls).toHaveLength(1);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('removes special-value callbacks and ignores late async values', async () => {
|
|
446
|
+
const handler = new TestVisualizationHandler();
|
|
447
|
+
const helper = new BindingsHelper(handler);
|
|
448
|
+
const element = document.createElement('div');
|
|
449
|
+
let resolveValue: (value: any) => void;
|
|
450
|
+
const specialValueHandler: SpecialValueHandler = {
|
|
451
|
+
valueProvider: () => new Promise(resolve => resolveValue = resolve),
|
|
452
|
+
valueChangedCallbacks: new Map()
|
|
453
|
+
};
|
|
454
|
+
const dispose = helper.applyBinding(element, ['value', {
|
|
455
|
+
signal: '§clock',
|
|
456
|
+
target: BindingTarget.property
|
|
457
|
+
}], '', null, specialValueHandler);
|
|
458
|
+
|
|
459
|
+
expect(specialValueHandler.valueChangedCallbacks.get('clock')).toHaveLength(1);
|
|
460
|
+
dispose();
|
|
461
|
+
expect(specialValueHandler.valueChangedCallbacks.has('clock')).toBe(false);
|
|
462
|
+
resolveValue(42);
|
|
463
|
+
await flushMicrotasks();
|
|
464
|
+
expect(element['value']).toBeUndefined();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('uses precompiled exact, range, and template converters', async () => {
|
|
468
|
+
const handler = new TestVisualizationHandler();
|
|
469
|
+
handler.values.set('sensor', 5);
|
|
470
|
+
const helper = new BindingsHelper(handler);
|
|
471
|
+
const element = document.createElement('div');
|
|
472
|
+
const dispose = helper.applyBinding(element, ['value', {
|
|
473
|
+
signal: 'sensor',
|
|
474
|
+
converter: {
|
|
475
|
+
'5': 'exact ${__0}',
|
|
476
|
+
'>=10': 'high ${__0}',
|
|
477
|
+
'0-9': 'low'
|
|
478
|
+
},
|
|
479
|
+
target: BindingTarget.property
|
|
480
|
+
}], '', null);
|
|
481
|
+
await flushMicrotasks();
|
|
482
|
+
expect(element['value']).toBe('exact 5');
|
|
483
|
+
|
|
484
|
+
handler.emit('sensor', 7);
|
|
485
|
+
await flushMicrotasks();
|
|
486
|
+
expect(element['value']).toBe('low');
|
|
487
|
+
|
|
488
|
+
handler.emit('sensor', 12);
|
|
489
|
+
await flushMicrotasks();
|
|
490
|
+
expect(element['value']).toBe('high 12');
|
|
491
|
+
dispose();
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test('parseBindingString splits without per-character string concatenation', () => {
|
|
496
|
+
expect(parseBindingString('.devices.{room}.{sensor}')).toEqual({
|
|
497
|
+
parts: ['.devices.', '.', ''],
|
|
498
|
+
signals: ['room', 'sensor']
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test('binding refactoring preserves attribute source prefixes', () => {
|
|
503
|
+
const binding: any = {
|
|
504
|
+
bindableObjectNames: ['screen:?@theme', '#@state', '?$signalObject', '#$signalObject', '##value'],
|
|
505
|
+
target: BindingTarget.property,
|
|
506
|
+
targetName: 'value'
|
|
507
|
+
};
|
|
508
|
+
let savedBinding: any;
|
|
509
|
+
const bindingService = {
|
|
510
|
+
getBindings: () => [binding],
|
|
511
|
+
setBinding: (_designItem: any, value: any) => savedBinding = value
|
|
512
|
+
};
|
|
513
|
+
const designItem: any = { serviceContainer: { bindingService } };
|
|
514
|
+
const service = new VisualizationBindingsRefactorService();
|
|
515
|
+
const refactorings: any[] = service.getRefactorings([designItem]);
|
|
516
|
+
|
|
517
|
+
expect(refactorings.map(x => ({ name: x.name, prefix: x.prefix, itemType: x.itemType }))).toEqual([
|
|
518
|
+
{ name: 'theme', prefix: '?@', itemType: 'attribute' },
|
|
519
|
+
{ name: 'state', prefix: '#@', itemType: 'attribute' },
|
|
520
|
+
{ name: 'signalObject', prefix: '?$', itemType: 'signalObject' },
|
|
521
|
+
{ name: 'signalObject', prefix: '#$', itemType: 'signalObject' },
|
|
522
|
+
{ name: 'value', prefix: '##', itemType: 'property' }
|
|
523
|
+
]);
|
|
524
|
+
|
|
525
|
+
service.refactor(refactorings[0], 'theme', 'palette');
|
|
526
|
+
expect(savedBinding.bindableObjectNames).toEqual(['screen:?@palette', '#@state', '?$signalObject', '#$signalObject', '##value']);
|
|
527
|
+
});
|