@schukai/monster 4.124.1 → 4.125.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/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
 
4
4
 
5
+ ## [4.125.0] - 2026-03-03
6
+
7
+ ### Add Features
8
+
9
+ - Add new issue 385 HTML and JS files with property attribute updates
10
+ ### Changes
11
+
12
+ - close issue
13
+
14
+
15
+
16
+ ## [4.124.2] - 2026-03-02
17
+
18
+ ### Bug Fixes
19
+
20
+ - **digits:** stabilize rendering and input behavior ([#384](https://gitlab.schukai.com/oss/libraries/javascript/monster/issues/384))
21
+
22
+
23
+
5
24
  ## [4.124.1] - 2026-02-23
6
25
 
7
26
  ### Bug Fixes
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.4","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.124.1"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.5","@popperjs/core":"^2.11.8"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.125.0"}
@@ -1311,8 +1311,14 @@ function collectSearchQueries() {
1311
1311
  }
1312
1312
 
1313
1313
  const controlValue = getControlValuesFromLabel(element);
1314
- if (!controlValue) {
1315
- if (controlValue === "" && currentHash?.[this.id]?.[id]) {
1314
+ const hasEmptyArrayValue =
1315
+ isArray(controlValue) && controlValue.length === 0;
1316
+
1317
+ if (!controlValue || hasEmptyArrayValue) {
1318
+ if (
1319
+ (controlValue === "" || hasEmptyArrayValue) &&
1320
+ currentHash?.[this.id]?.[id]
1321
+ ) {
1316
1322
  delete currentHash[this.id][id];
1317
1323
  }
1318
1324
 
@@ -1324,9 +1330,13 @@ function collectSearchQueries() {
1324
1330
  }
1325
1331
  currentHash[this.id][id] = controlValue;
1326
1332
 
1333
+ const formatterValue = isArray(controlValue)
1334
+ ? controlValue.join(",")
1335
+ : controlValue;
1336
+
1327
1337
  const mapping = {
1328
1338
  id,
1329
- value: controlValue,
1339
+ value: formatterValue,
1330
1340
  label,
1331
1341
  };
1332
1342
 
@@ -100,10 +100,11 @@ class Digits extends CustomControl {
100
100
  * @property {string} value
101
101
  */
102
102
  set value(value) {
103
- const chars = String(value).split("");
103
+ const normalizedValue = value == null ? "" : String(value);
104
+ const chars = normalizedValue.split("");
104
105
 
105
- this.setOption("value", value);
106
- this.setFormValue(value);
106
+ this.setOption("value", normalizedValue);
107
+ this.setFormValue(normalizedValue);
107
108
 
108
109
  if (chars.every(checkCharacter.bind(this))) {
109
110
  this.setValidity({ badInput: false }, "");
@@ -209,7 +210,9 @@ function updateDigitControls() {
209
210
 
210
211
  const controls = [];
211
212
 
212
- const values = this.getOption("value") || "";
213
+ const values = this.getOption("value") == null
214
+ ? ""
215
+ : String(this.getOption("value"));
213
216
 
214
217
  if (this[digitsElementSymbol]) {
215
218
  this[digitsElementSymbol].style.gridTemplateColumns =
@@ -218,11 +221,17 @@ function updateDigitControls() {
218
221
 
219
222
  for (let i = 0; i < digits; i++) {
220
223
  controls.push({
221
- value: values[i] ?? " ",
224
+ value: values[i] ?? "",
222
225
  });
223
226
  }
224
227
 
225
228
  this.setOption("digitsControls", controls);
229
+
230
+ // Keep real input.value in sync with the option model. Attribute updates alone
231
+ // can drift after user interaction because inputs keep an internal dirty value.
232
+ setTimeout(() => {
233
+ syncInputValues.call(this, controls);
234
+ }, 0);
226
235
  }
227
236
 
228
237
  /**
@@ -287,7 +296,7 @@ function initEventHandler() {
287
296
  }
288
297
 
289
298
  if (pressedKey === "Backspace") {
290
- if (inputControl.value !== "" && inputControl.value !== " ") {
299
+ if (inputControl.value !== "") {
291
300
  event.preventDefault();
292
301
  inputControl.value = "";
293
302
  collectValues.call(self);
@@ -313,16 +322,14 @@ function initEventHandler() {
313
322
  return;
314
323
  }
315
324
 
316
- if (inputControl.value.length === 1) {
317
- event.preventDefault();
318
- inputControl.value = pressedKey;
319
- const nextControl = inputControl.nextElementSibling;
320
- if (nextControl && nextControl.tagName === "INPUT") {
321
- nextControl.focus();
322
- }
323
- collectValues.call(self);
324
- return;
325
+ event.preventDefault();
326
+ inputControl.value = pressedKey;
327
+ const nextControl = inputControl.nextElementSibling;
328
+ if (nextControl && nextControl.tagName === "INPUT") {
329
+ nextControl.focus();
325
330
  }
331
+ collectValues.call(self);
332
+ return;
326
333
  }
327
334
  });
328
335
 
@@ -344,10 +351,28 @@ function initEventHandler() {
344
351
  function collectValues() {
345
352
  const controlsValues = Array.from(
346
353
  this[digitsElementSymbol].querySelectorAll("input"),
347
- ).map((input) => input.value || " ");
354
+ ).map((input) => input.value || "");
348
355
  this.value = controlsValues.join("");
349
356
  }
350
357
 
358
+ /**
359
+ * @private
360
+ * @param {Array<{value:string}>} controls
361
+ * @returns {void}
362
+ */
363
+ function syncInputValues(controls) {
364
+ if (!this[digitsElementSymbol]) return;
365
+ const inputs = this[digitsElementSymbol].querySelectorAll("input");
366
+ if (!inputs.length) return;
367
+
368
+ for (let i = 0; i < inputs.length; i++) {
369
+ const nextValue = controls?.[i]?.value ?? "";
370
+ if (inputs[i].value !== nextValue) {
371
+ inputs[i].value = nextValue;
372
+ }
373
+ }
374
+ }
375
+
351
376
  /**
352
377
  * @private
353
378
  * @return {void}
@@ -297,6 +297,8 @@ class ToggleSwitch extends CustomControl {
297
297
  ) {
298
298
  if (this.getOption("value") !== normalized) {
299
299
  this.setOption("value", normalized);
300
+ } else {
301
+ validateAndSetValue.call(this);
300
302
  }
301
303
  return;
302
304
  }
@@ -350,6 +352,7 @@ function toggleOn() {
350
352
  return;
351
353
  }
352
354
 
355
+ this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
353
356
  this[switchElementSymbol].classList.remove(this.getOption("classes.off")); // change color
354
357
  this[switchElementSymbol].classList.add(this.getOption("classes.on")); // change color
355
358
 
@@ -371,6 +374,7 @@ function toggleOff() {
371
374
  return;
372
375
  }
373
376
 
377
+ this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
374
378
  this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color
375
379
  this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color
376
380
 
@@ -397,6 +401,17 @@ function showError() {
397
401
  this[switchElementSymbol].classList.add(this.getOption("classes.error"));
398
402
  }
399
403
 
404
+ /**
405
+ * @private
406
+ */
407
+ function clearError() {
408
+ if (this[switchElementSymbol]) {
409
+ this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
410
+ }
411
+
412
+ this.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
413
+ }
414
+
400
415
  /**
401
416
  * @private
402
417
  */
@@ -407,6 +422,21 @@ function validateAndSetValue() {
407
422
  this.setOption("value", value);
408
423
  }
409
424
 
425
+ const offValue = this.getOption("values.off");
426
+ if (value === undefined || value === null || value === "") {
427
+ clearError.call(this);
428
+ if (this[invalidDisabledSymbol]) {
429
+ this.setOption("disabled", false);
430
+ this.formDisabledCallback(false);
431
+ this[invalidDisabledSymbol] = false;
432
+ }
433
+ if (this.getOption("value") !== offValue) {
434
+ this.setOption("value", offValue);
435
+ }
436
+ toggleOff.call(this);
437
+ return;
438
+ }
439
+
410
440
  const validatedValues = [];
411
441
  validatedValues.push(this.getOption("values.on"));
412
442
  validatedValues.push(this.getOption("values.off"));
@@ -437,6 +467,7 @@ function validateAndSetValue() {
437
467
  this.formDisabledCallback(false);
438
468
  this[invalidDisabledSymbol] = false;
439
469
  }
470
+ clearError.call(this);
440
471
 
441
472
  if (value === this.getOption("values.on")) {
442
473
  toggleOn.call(this);
@@ -20,6 +20,7 @@ export {
20
20
  ATTRIBUTE_THEME_PREFIX,
21
21
  ATTRIBUTE_THEME_NAME,
22
22
  ATTRIBUTE_UPDATER_ATTRIBUTES,
23
+ ATTRIBUTE_UPDATER_PROPERTIES,
23
24
  ATTRIBUTE_UPDATER_SELECT_THIS,
24
25
  ATTRIBUTE_UPDATER_REPLACE,
25
26
  ATTRIBUTE_UPDATER_INSERT,
@@ -151,6 +152,13 @@ const ATTRIBUTE_THEME_NAME = `${ATTRIBUTE_THEME_PREFIX}name`;
151
152
  */
152
153
  const ATTRIBUTE_UPDATER_ATTRIBUTES = `${ATTRIBUTE_PREFIX}attributes`;
153
154
 
155
+ /**
156
+ * @type {string}
157
+ * @license AGPLv3
158
+ * @since 4.125.0
159
+ */
160
+ const ATTRIBUTE_UPDATER_PROPERTIES = `${ATTRIBUTE_PREFIX}properties`;
161
+
154
162
  /**
155
163
  * @type {string}
156
164
  * @license AGPLv3
@@ -23,6 +23,7 @@ import {
23
23
  ATTRIBUTE_UPDATER_BIND_TYPE,
24
24
  ATTRIBUTE_UPDATER_INSERT,
25
25
  ATTRIBUTE_UPDATER_INSERT_REFERENCE,
26
+ ATTRIBUTE_UPDATER_PROPERTIES,
26
27
  ATTRIBUTE_UPDATER_REMOVE,
27
28
  ATTRIBUTE_UPDATER_REPLACE,
28
29
  ATTRIBUTE_UPDATER_SELECT_THIS,
@@ -190,6 +191,7 @@ class Updater extends Base {
190
191
  for (const path of updatePaths.values()) {
191
192
  updateContent.call(this, { path });
192
193
  updateAttributes.call(this, { path });
194
+ updateProperties.call(this, { path });
193
195
  }
194
196
  } else {
195
197
  for (const change of Object.values(diffResult)) {
@@ -211,6 +213,7 @@ class Updater extends Base {
211
213
  updateContent.call(this, change);
212
214
  await Promise.resolve();
213
215
  updateAttributes.call(this, change);
216
+ updateProperties.call(this, change);
214
217
  }
215
218
 
216
219
  /**
@@ -855,6 +858,14 @@ function applyRecursive(node, key, path) {
855
858
  );
856
859
  }
857
860
 
861
+ if (node.hasAttribute(ATTRIBUTE_UPDATER_PROPERTIES)) {
862
+ const value = node.getAttribute(ATTRIBUTE_UPDATER_PROPERTIES);
863
+ node.setAttribute(
864
+ ATTRIBUTE_UPDATER_PROPERTIES,
865
+ value.replaceAll(`path:${key}`, `path:${path}`),
866
+ );
867
+ }
868
+
858
869
  if (node.hasAttribute(ATTRIBUTE_UPDATER_BIND)) {
859
870
  const value = node.getAttribute(ATTRIBUTE_UPDATER_BIND);
860
871
  node.setAttribute(
@@ -984,6 +995,18 @@ function updateAttributes(change) {
984
995
  runUpdateAttributes.call(this, this[internalSymbol].element, p, subject);
985
996
  }
986
997
 
998
+ /**
999
+ * @private
1000
+ * @since 4.125.0
1001
+ * @param {object} change
1002
+ * @return {void}
1003
+ */
1004
+ function updateProperties(change) {
1005
+ const subject = this[internalSymbol].subject.getRealSubject();
1006
+ const p = clone(change?.["path"]);
1007
+ runUpdateProperties.call(this, this[internalSymbol].element, p, subject);
1008
+ }
1009
+
987
1010
  /**
988
1011
  * @private
989
1012
  * @param {HTMLElement} container
@@ -1059,6 +1082,75 @@ function runUpdateAttributes(container, parts, subject) {
1059
1082
  }
1060
1083
  }
1061
1084
 
1085
+ /**
1086
+ * @private
1087
+ * @param {HTMLElement} container
1088
+ * @param {array} parts
1089
+ * @param {object} subject
1090
+ * @return {void}
1091
+ * @this Updater
1092
+ */
1093
+ function runUpdateProperties(container, parts, subject) {
1094
+ if (!isArray(parts)) return;
1095
+ parts = clone(parts);
1096
+
1097
+ const mem = new WeakSet();
1098
+
1099
+ while (parts.length > 0) {
1100
+ const current = parts.join(".");
1101
+ parts.pop();
1102
+
1103
+ let iterator = new Set();
1104
+
1105
+ const query = `[${ATTRIBUTE_UPDATER_SELECT_THIS}][${ATTRIBUTE_UPDATER_PROPERTIES}], [${ATTRIBUTE_UPDATER_PROPERTIES}*="path:${current}"], [${ATTRIBUTE_UPDATER_PROPERTIES}^="static:"], [${ATTRIBUTE_UPDATER_PROPERTIES}^="i18n:"]`;
1106
+
1107
+ const e = container.querySelectorAll(query);
1108
+
1109
+ if (e.length > 0) {
1110
+ iterator = new Set([...e]);
1111
+ }
1112
+
1113
+ if (container.matches(query)) {
1114
+ iterator.add(container);
1115
+ }
1116
+
1117
+ for (const [element] of iterator.entries()) {
1118
+ if (mem.has(element)) return;
1119
+ mem.add(element);
1120
+
1121
+ // this case occurs when the ATTRIBUTE_UPDATER_SELECT_THIS attribute is set
1122
+ if (!element.hasAttribute(ATTRIBUTE_UPDATER_PROPERTIES)) {
1123
+ continue;
1124
+ }
1125
+
1126
+ const properties = element.getAttribute(ATTRIBUTE_UPDATER_PROPERTIES);
1127
+
1128
+ for (let [, def] of Object.entries(properties.split(","))) {
1129
+ def = trimSpaces(def);
1130
+ const i = def.indexOf(" ");
1131
+ const name = trimSpaces(def.substr(0, i));
1132
+ const cmd = trimSpaces(def.substr(i));
1133
+
1134
+ const pipe = new Pipe(cmd);
1135
+
1136
+ this[internalSymbol].callbacks.forEach((f, n) => {
1137
+ pipe.setCallback(n, f, element);
1138
+ });
1139
+
1140
+ let value;
1141
+ try {
1142
+ element.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
1143
+ value = pipe.run(subject);
1144
+ } catch (e) {
1145
+ addErrorAttribute(element, e);
1146
+ }
1147
+
1148
+ handleInputControlPropertyUpdate.call(this, element, name, value);
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+
1062
1154
  /**
1063
1155
  * @private
1064
1156
  * @param {HTMLElement|*} element
@@ -1133,6 +1225,189 @@ function handleInputControlAttributeUpdate(element, name, value) {
1133
1225
  }
1134
1226
  }
1135
1227
 
1228
+ /**
1229
+ * @private
1230
+ * @param {HTMLElement|*} element
1231
+ * @param {string} name
1232
+ * @param {*} value
1233
+ * @return {void}
1234
+ * @this Updater
1235
+ */
1236
+ function handleInputControlPropertyUpdate(element, name, value) {
1237
+ if (element instanceof HTMLSelectElement && name === "value") {
1238
+ switch (element.type) {
1239
+ case "select-multiple":
1240
+ if (isArray(value)) {
1241
+ for (const [, opt] of Object.entries(element.options)) {
1242
+ opt.selected = value.indexOf(opt.value) !== -1;
1243
+ }
1244
+ return;
1245
+ }
1246
+ break;
1247
+ case "select-one":
1248
+ if (value === undefined) {
1249
+ element.selectedIndex = -1;
1250
+ return;
1251
+ }
1252
+ break;
1253
+ }
1254
+ }
1255
+
1256
+ if (element instanceof HTMLInputElement) {
1257
+ switch (name) {
1258
+ case "checked":
1259
+ element.checked =
1260
+ value === true || value === "true" || value === "1" || value === "on";
1261
+ return;
1262
+ case "value":
1263
+ if (element.type === "color") {
1264
+ if (value === undefined || value === "") {
1265
+ return;
1266
+ }
1267
+ }
1268
+ break;
1269
+ }
1270
+ }
1271
+
1272
+ if (element instanceof HTMLTextAreaElement && name === "value") {
1273
+ element.value = value === undefined ? "" : value;
1274
+ return;
1275
+ }
1276
+
1277
+ const optionPath = getControlOptionPathFromPropertyName(name);
1278
+ if (
1279
+ optionPath !== null &&
1280
+ typeof element?.setOption === "function" &&
1281
+ typeof element?.getOption === "function"
1282
+ ) {
1283
+ try {
1284
+ const current = element.getOption(optionPath);
1285
+ const normalized = normalizeValueForOptionTarget(value, current);
1286
+ element.setOption(optionPath, normalized);
1287
+ } catch (e) {
1288
+ addErrorAttribute(element, e);
1289
+ }
1290
+ return;
1291
+ }
1292
+
1293
+ if (
1294
+ optionPath !== null &&
1295
+ typeof element?.setOption === "function" &&
1296
+ typeof element?.getOption !== "function"
1297
+ ) {
1298
+ try {
1299
+ element.setOption(optionPath, value);
1300
+ } catch (e) {
1301
+ addErrorAttribute(element, e);
1302
+ }
1303
+ return;
1304
+ }
1305
+
1306
+ try {
1307
+ element[name] = value;
1308
+ } catch (e) {
1309
+ addErrorAttribute(element, e);
1310
+ }
1311
+ }
1312
+
1313
+ /**
1314
+ * @private
1315
+ * @param {string} name
1316
+ * @return {string|null}
1317
+ */
1318
+ function getControlOptionPathFromPropertyName(name) {
1319
+ if (!isString(name)) {
1320
+ return null;
1321
+ }
1322
+
1323
+ let path = null;
1324
+
1325
+ if (name.startsWith("option:")) {
1326
+ path = name.substring(7);
1327
+ } else if (name.startsWith("option.")) {
1328
+ path = name.substring(7);
1329
+ } else if (name.startsWith("options.")) {
1330
+ path = name.substring(8);
1331
+ }
1332
+
1333
+ if (!isString(path)) {
1334
+ return null;
1335
+ }
1336
+
1337
+ path = path
1338
+ .split(".")
1339
+ .map((part) => trimSpaces(part))
1340
+ .filter((part) => part !== "")
1341
+ .join(".");
1342
+
1343
+ if (path === "") {
1344
+ return null;
1345
+ }
1346
+
1347
+ return path;
1348
+ }
1349
+
1350
+ /**
1351
+ * @private
1352
+ * @param {*} value
1353
+ * @param {*} current
1354
+ * @return {*}
1355
+ */
1356
+ function normalizeValueForOptionTarget(value, current) {
1357
+ if (current === undefined || current === null) {
1358
+ return value;
1359
+ }
1360
+
1361
+ if (typeof current === "boolean") {
1362
+ return (
1363
+ value === true || value === "true" || value === "1" || value === "on"
1364
+ );
1365
+ }
1366
+
1367
+ if (typeof current === "number") {
1368
+ const numberValue = Number(value);
1369
+ return isNaN(numberValue) ? current : numberValue;
1370
+ }
1371
+
1372
+ if (typeof current === "string") {
1373
+ return value === undefined || value === null ? "" : `${value}`;
1374
+ }
1375
+
1376
+ if (isArray(current)) {
1377
+ if (isArray(value)) {
1378
+ return value;
1379
+ }
1380
+
1381
+ if (isString(value)) {
1382
+ if (value.trim() === "") {
1383
+ return [];
1384
+ }
1385
+ return value.split("::");
1386
+ }
1387
+
1388
+ if (value === undefined || value === null) {
1389
+ return [];
1390
+ }
1391
+ return [value];
1392
+ }
1393
+
1394
+ if (typeof current === "object") {
1395
+ if (value !== null && typeof value === "object") {
1396
+ return value;
1397
+ }
1398
+
1399
+ if (isString(value)) {
1400
+ try {
1401
+ return JSON.parse(value);
1402
+ } catch (e) {
1403
+ return current;
1404
+ }
1405
+ }
1406
+ }
1407
+
1408
+ return value;
1409
+ }
1410
+
1136
1411
  /**
1137
1412
  * @param {NodeList|HTMLElement|Set<HTMLElement>} elements
1138
1413
  * @param {Symbol} symbol
@@ -397,6 +397,21 @@ describe('ToggleSwitch', function () {
397
397
 
398
398
  });
399
399
 
400
+ it('does not set error state for empty initial values', function (done) {
401
+
402
+ const toggleSwitch = document.createElement('monster-toggle-switch');
403
+ toggleSwitch.setOption('value', undefined);
404
+ document.getElementById('mocks').appendChild(toggleSwitch);
405
+
406
+ setTimeout(() => {
407
+ const switchEl = toggleSwitch.shadowRoot.querySelector('[data-monster-role="switch"]');
408
+ expect(toggleSwitch.getAttribute('data-monster-error')).is.equal(null);
409
+ expect(switchEl.classList.contains(toggleSwitch.getOption('classes.error'))).is.false;
410
+ expect(toggleSwitch.value).is.equal(toggleSwitch.getOption('values.off'));
411
+ done();
412
+ }, 0);
413
+ });
414
+
400
415
  });
401
416
 
402
417
 
@@ -48,6 +48,11 @@ let html1 = `
48
48
 
49
49
  <textarea name="textarea" id="textarea" data-monster-attributes="value path:a.textarea"></textarea>
50
50
 
51
+ <input id="property-checkbox" type="checkbox" data-monster-properties="checked path:a.checkboxBool">
52
+ <input id="property-input" type="text" data-monster-properties="value path:a.checkboxBool">
53
+ <monster-test-property id="property-custom" data-monster-properties="active path:a.checkboxBool, payload path:a.payload"></monster-test-property>
54
+ <monster-test-option-control id="property-option-control" data-monster-properties="option:features.enabled path:a.checkboxBool, option:labels.count path:a.optionCount"></monster-test-option-control>
55
+
51
56
  </div>
52
57
  </div>
53
58
 
@@ -64,7 +69,7 @@ let html2 = `
64
69
  let html3 = `
65
70
 
66
71
  <template id="myinnerid">
67
- <span data-monster-replace="path:myinnerid | tojson"></span>
72
+ <span data-monster-replace="path:myinnerid | tojson" data-monster-properties="title path:myinnerid.i"></span>
68
73
  </template>
69
74
 
70
75
  <template id="myid">
@@ -127,6 +132,67 @@ describe("DOM", function () {
127
132
  import("../../../source/dom/updater.mjs")
128
133
  .then((m) => {
129
134
  Updater = m.Updater;
135
+
136
+ if (!customElements.get("monster-test-property")) {
137
+ class MonsterTestProperty extends HTMLElement {
138
+ set active(value) {
139
+ this._active = value;
140
+ }
141
+
142
+ get active() {
143
+ return this._active;
144
+ }
145
+
146
+ set payload(value) {
147
+ this._payload = value;
148
+ }
149
+
150
+ get payload() {
151
+ return this._payload;
152
+ }
153
+ }
154
+
155
+ customElements.define("monster-test-property", MonsterTestProperty);
156
+ }
157
+
158
+ if (!customElements.get("monster-test-option-control")) {
159
+ class MonsterTestOptionControl extends HTMLElement {
160
+ constructor() {
161
+ super();
162
+ this._options = {
163
+ features: {
164
+ enabled: false,
165
+ },
166
+ labels: {
167
+ count: 0,
168
+ },
169
+ };
170
+ }
171
+
172
+ getOption(path) {
173
+ return path.split(".").reduce((ref, part) => ref?.[part], this._options);
174
+ }
175
+
176
+ setOption(path, value) {
177
+ const parts = path.split(".");
178
+ let ref = this._options;
179
+ while (parts.length > 1) {
180
+ const part = parts.shift();
181
+ if (ref[part] === undefined || ref[part] === null) {
182
+ ref[part] = {};
183
+ }
184
+ ref = ref[part];
185
+ }
186
+ ref[parts[0]] = value;
187
+ }
188
+ }
189
+
190
+ customElements.define(
191
+ "monster-test-option-control",
192
+ MonsterTestOptionControl,
193
+ );
194
+ }
195
+
130
196
  done();
131
197
  })
132
198
  .catch((e) => {
@@ -714,8 +780,12 @@ describe("DOM", function () {
714
780
  '<p data-monster-insert="myinnerid path:a.b" data-monster-insert-reference="myid-0">',
715
781
  );
716
782
  expect(element).contain.html(
717
- '<span data-monster-replace="path:a.b.0 | tojson" data-monster-insert-reference="myinnerid-0">{"i":"0"}</span>',
783
+ 'data-monster-replace="path:a.b.0 | tojson"',
784
+ );
785
+ expect(element).contain.html(
786
+ 'data-monster-properties="title path:a.b.0.i"',
718
787
  );
788
+ expect(element).contain.html('title="0"');
719
789
 
720
790
  done();
721
791
  }, 100);
@@ -750,6 +820,22 @@ describe("DOM", function () {
750
820
  let textarea = document.getElementById("textarea");
751
821
  expect(textarea.value).to.be.equal("");
752
822
 
823
+ let propertyCheckbox = document.getElementById("property-checkbox");
824
+ expect(propertyCheckbox.checked).to.be.false;
825
+
826
+ let propertyInput = document.getElementById("property-input");
827
+ expect(propertyInput.value).to.be.equal("");
828
+
829
+ let propertyCustom = document.getElementById("property-custom");
830
+ expect(propertyCustom.active).to.be.equal(undefined);
831
+ expect(propertyCustom.payload).to.be.equal(undefined);
832
+
833
+ let propertyOptionControl = document.getElementById(
834
+ "property-option-control",
835
+ );
836
+ expect(propertyOptionControl.getOption("features.enabled")).to.be.false;
837
+ expect(propertyOptionControl.getOption("labels.count")).to.be.equal(0);
838
+
753
839
  let d = new Updater(element, {
754
840
  a: {
755
841
  b: "div-class",
@@ -760,6 +846,11 @@ describe("DOM", function () {
760
846
  multiselect: ["value3", "value4", "other-value5"],
761
847
  select: "value2",
762
848
  checkbox: "true",
849
+ checkboxBool: true,
850
+ optionCount: 1234,
851
+ payload: {
852
+ state: "ok",
853
+ },
763
854
  },
764
855
  });
765
856
 
@@ -800,6 +891,26 @@ describe("DOM", function () {
800
891
  "multiselect control",
801
892
  ).to.be.equal(JSON.stringify(d.getSubject()["a"]["multiselect"]));
802
893
  expect(checkbox.checked, "checkbox control").to.be.true;
894
+ expect(propertyCheckbox.checked, "property checkbox control").to.be
895
+ .true;
896
+ expect(propertyInput.value, "property input value").to.be.equal(
897
+ "true",
898
+ );
899
+ expect(propertyCustom.active, "typed property boolean").to.be.true;
900
+ expect(propertyCustom.payload, "typed property object").to.deep.equal(
901
+ {
902
+ state: "ok",
903
+ },
904
+ );
905
+ expect(propertyCustom.getAttribute("active")).to.be.equal(null);
906
+ expect(
907
+ propertyOptionControl.getOption("features.enabled"),
908
+ "control option boolean",
909
+ ).to.be.true;
910
+ expect(
911
+ propertyOptionControl.getOption("labels.count"),
912
+ "control option number",
913
+ ).to.be.equal(1234);
803
914
 
804
915
  done();
805
916
  }, 100);