@schukai/monster 4.148.2 → 4.148.4

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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/source/components/datatable/filter/date-presets.mjs +13 -0
  3. package/source/components/datatable/filter/date-range.mjs +12 -0
  4. package/source/components/datatable/filter/date-time.mjs +14 -1
  5. package/source/components/datatable/filter/date.mjs +14 -1
  6. package/source/components/datatable/filter/input.mjs +14 -1
  7. package/source/components/datatable/filter/range.mjs +11 -0
  8. package/source/components/datatable/filter/text-operator.mjs +19 -0
  9. package/source/components/datatable/filter/time.mjs +14 -1
  10. package/source/components/form/buy-box.mjs +2 -0
  11. package/source/components/form/cart-control.mjs +2 -0
  12. package/source/components/form/select.mjs +104 -16
  13. package/source/components/state/state.mjs +2 -0
  14. package/source/data/extend.mjs +7 -1
  15. package/source/dom/customcontrol.mjs +28 -23
  16. package/source/dom/customelement.mjs +105 -39
  17. package/source/dom/util/extract-keys.mjs +2 -1
  18. package/source/dom/util/init-options-from-attributes.mjs +1 -1
  19. package/source/dom/util/set-option-from-attribute.mjs +14 -3
  20. package/test/cases/components/datatable/filter-form-value.mjs +98 -0
  21. package/test/cases/components/form/non-value-form-association.mjs +53 -0
  22. package/test/cases/components/form/select.mjs +275 -0
  23. package/test/cases/data/extend.mjs +27 -1
  24. package/test/cases/dom/customcontrol.mjs +188 -6
  25. package/test/cases/dom/customelement-initfromscripthost.mjs +62 -0
  26. package/test/cases/dom/customelement.mjs +123 -2
  27. package/test/cases/dom/util/extract-keys.mjs +14 -0
  28. package/test/cases/dom/util/init-options-from-attributes.mjs +18 -0
  29. package/test/cases/dom/util/set-option-from-attribute.mjs +151 -0
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.148.2"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"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.148.4"}
@@ -88,6 +88,8 @@ class DatePresets extends CustomControl {
88
88
 
89
89
  initControlReferences.call(this);
90
90
  updateOptions.call(this);
91
+ initEventHandler.call(this);
92
+ syncFormValue.call(this);
91
93
  }
92
94
 
93
95
  /**
@@ -104,6 +106,7 @@ class DatePresets extends CustomControl {
104
106
  */
105
107
  set value(value) {
106
108
  this[selectElementSymbol].value = value;
109
+ syncFormValue.call(this);
107
110
  }
108
111
 
109
112
  /**
@@ -340,6 +343,16 @@ function initControlReferences() {
340
343
  return this;
341
344
  }
342
345
 
346
+ function initEventHandler() {
347
+ this[selectElementSymbol].addEventListener("change", () => {
348
+ syncFormValue.call(this);
349
+ });
350
+ }
351
+
352
+ function syncFormValue() {
353
+ this.setFormValue(this.value);
354
+ }
355
+
343
356
  /**
344
357
  * @private
345
358
  * @return {void}
@@ -166,6 +166,7 @@ class DateRange extends AbstractBase {
166
166
 
167
167
  initControlReferences.call(this);
168
168
  initEventHandler.call(this);
169
+ syncFormValue.call(this);
169
170
  }
170
171
 
171
172
  /**
@@ -175,6 +176,7 @@ class DateRange extends AbstractBase {
175
176
  */
176
177
  set value(value) {
177
178
  this[inputElementSymbol].value = value;
179
+ syncFormValue.call(this);
178
180
  }
179
181
 
180
182
  /**
@@ -580,6 +582,7 @@ function initEventHandler() {
580
582
 
581
583
  if (type === "today") {
582
584
  this[inputElementSymbol].value = new Date().toISOString().slice(0, 10);
585
+ syncFormValue.call(this);
583
586
  return;
584
587
  }
585
588
 
@@ -609,10 +612,14 @@ function initEventHandler() {
609
612
 
610
613
  // change the input value if the user change the value of the radio button
611
614
  this[inputElementSymbol].addEventListener("change", (event) => {
615
+ syncFormValue.call(this);
612
616
  queueMicrotask(() => {
613
617
  updatePopperInputsFromMainValue.call(this);
614
618
  });
615
619
  });
620
+ this[inputElementSymbol].addEventListener("input", () => {
621
+ syncFormValue.call(this);
622
+ });
616
623
 
617
624
  // if the user change the value of on of the input fields, we should update the value of the input field
618
625
  this[formContainerElementSymbol].addEventListener("change", (event) => {
@@ -636,6 +643,7 @@ function initEventHandler() {
636
643
 
637
644
  const type = group.getAttribute("data-monster-range-type");
638
645
  updateMainFilterValueFromPopperChange.call(this, group, type);
646
+ syncFormValue.call(this);
639
647
  });
640
648
 
641
649
  this.addEventListener("keydown", (event) => {
@@ -669,6 +677,10 @@ function initEventHandler() {
669
677
  return this;
670
678
  }
671
679
 
680
+ function syncFormValue() {
681
+ this.setFormValue(this.value);
682
+ }
683
+
672
684
  /**
673
685
  * @private
674
686
  */
@@ -85,6 +85,7 @@ class DateTimeFilter extends CustomControl {
85
85
 
86
86
  initControlReferences.call(this);
87
87
  initEventHandler.call(this);
88
+ syncFormValue.call(this);
88
89
  }
89
90
 
90
91
  /**
@@ -101,6 +102,7 @@ class DateTimeFilter extends CustomControl {
101
102
  */
102
103
  set value(value) {
103
104
  this[inputElementSymbol].value = value;
105
+ syncFormValue.call(this);
104
106
  }
105
107
 
106
108
  /**
@@ -129,7 +131,18 @@ function initControlReferences() {
129
131
  /**
130
132
  * @private
131
133
  */
132
- function initEventHandler() {}
134
+ function initEventHandler() {
135
+ this[inputElementSymbol].addEventListener("input", () => {
136
+ syncFormValue.call(this);
137
+ });
138
+ this[inputElementSymbol].addEventListener("change", () => {
139
+ syncFormValue.call(this);
140
+ });
141
+ }
142
+
143
+ function syncFormValue() {
144
+ this.setFormValue(this.value);
145
+ }
133
146
 
134
147
  /**
135
148
  * @private
@@ -85,6 +85,7 @@ class DateFilter extends CustomControl {
85
85
 
86
86
  initControlReferences.call(this);
87
87
  initEventHandler.call(this);
88
+ syncFormValue.call(this);
88
89
  }
89
90
 
90
91
  /**
@@ -101,6 +102,7 @@ class DateFilter extends CustomControl {
101
102
  */
102
103
  set value(value) {
103
104
  this[inputElementSymbol].value = value;
105
+ syncFormValue.call(this);
104
106
  }
105
107
 
106
108
  /**
@@ -129,7 +131,18 @@ function initControlReferences() {
129
131
  /**
130
132
  * @private
131
133
  */
132
- function initEventHandler() {}
134
+ function initEventHandler() {
135
+ this[inputElementSymbol].addEventListener("input", () => {
136
+ syncFormValue.call(this);
137
+ });
138
+ this[inputElementSymbol].addEventListener("change", () => {
139
+ syncFormValue.call(this);
140
+ });
141
+ }
142
+
143
+ function syncFormValue() {
144
+ this.setFormValue(this.value);
145
+ }
133
146
 
134
147
  /**
135
148
  * @private
@@ -84,6 +84,7 @@ class Input extends AbstractBase {
84
84
 
85
85
  initControlReferences.call(this);
86
86
  initEventHandler.call(this);
87
+ syncFormValue.call(this);
87
88
  }
88
89
 
89
90
  /**
@@ -100,6 +101,7 @@ class Input extends AbstractBase {
100
101
  */
101
102
  set value(value) {
102
103
  this[inputElementSymbol].value = value;
104
+ syncFormValue.call(this);
103
105
  }
104
106
 
105
107
  /**
@@ -175,7 +177,18 @@ function initControlReferences() {
175
177
  /**
176
178
  * @private
177
179
  */
178
- function initEventHandler() {}
180
+ function initEventHandler() {
181
+ this[inputElementSymbol].addEventListener("input", () => {
182
+ syncFormValue.call(this);
183
+ });
184
+ this[inputElementSymbol].addEventListener("change", () => {
185
+ syncFormValue.call(this);
186
+ });
187
+ }
188
+
189
+ function syncFormValue() {
190
+ this.setFormValue(this.value);
191
+ }
179
192
 
180
193
  /**
181
194
  * @private
@@ -136,6 +136,7 @@ class Range extends AbstractBase {
136
136
 
137
137
  initControlReferences.call(this);
138
138
  initEventHandler.call(this);
139
+ syncFormValue.call(this);
139
140
  }
140
141
 
141
142
  /**
@@ -145,6 +146,7 @@ class Range extends AbstractBase {
145
146
  */
146
147
  set value(value) {
147
148
  this[inputElementSymbol].value = value;
149
+ syncFormValue.call(this);
148
150
  }
149
151
 
150
152
  /**
@@ -484,12 +486,17 @@ function initEventHandler() {
484
486
  const type = typeElement.getAttribute("data-monster-range-type");
485
487
  const value = element.value;
486
488
  updateFilterValue(type, this, value, element);
489
+ syncFormValue.call(this);
487
490
  });
488
491
 
489
492
  // we should watch the change event of self[inputElementSymbol] and call updateInputFromValue
490
493
  // if the value is changed from outside
491
494
  this[inputElementSymbol].addEventListener("change", () => {
492
495
  updateInputFromValue.call(this);
496
+ syncFormValue.call(this);
497
+ });
498
+ this[inputElementSymbol].addEventListener("input", () => {
499
+ syncFormValue.call(this);
493
500
  });
494
501
 
495
502
  this.addEventListener("keydown", (event) => {
@@ -537,6 +544,10 @@ function initEventHandler() {
537
544
  return this;
538
545
  }
539
546
 
547
+ function syncFormValue() {
548
+ this.setFormValue(this.value);
549
+ }
550
+
540
551
  /**
541
552
  * @private
542
553
  */
@@ -103,6 +103,8 @@ class TextOperator extends CustomControl {
103
103
 
104
104
  initControlReferences.call(this);
105
105
  updateOptions.call(this);
106
+ initEventHandler.call(this);
107
+ syncFormValue.call(this);
106
108
  }
107
109
 
108
110
  /**
@@ -121,6 +123,7 @@ class TextOperator extends CustomControl {
121
123
  const parsed = parseValue(value);
122
124
  this[operatorElementSymbol].value = parsed.operator;
123
125
  this[inputElementSymbol].value = parsed.query;
126
+ syncFormValue.call(this);
124
127
  }
125
128
 
126
129
  /**
@@ -327,6 +330,22 @@ function initControlReferences() {
327
330
  return this;
328
331
  }
329
332
 
333
+ function initEventHandler() {
334
+ this[operatorElementSymbol].addEventListener("change", () => {
335
+ syncFormValue.call(this);
336
+ });
337
+ this[inputElementSymbol].addEventListener("input", () => {
338
+ syncFormValue.call(this);
339
+ });
340
+ this[inputElementSymbol].addEventListener("change", () => {
341
+ syncFormValue.call(this);
342
+ });
343
+ }
344
+
345
+ function syncFormValue() {
346
+ this.setFormValue(this.value);
347
+ }
348
+
330
349
  /**
331
350
  * @private
332
351
  * @return {void}
@@ -85,6 +85,7 @@ class TimeFilter extends CustomControl {
85
85
 
86
86
  initControlReferences.call(this);
87
87
  initEventHandler.call(this);
88
+ syncFormValue.call(this);
88
89
  }
89
90
 
90
91
  /**
@@ -101,6 +102,7 @@ class TimeFilter extends CustomControl {
101
102
  */
102
103
  set value(value) {
103
104
  this[inputElementSymbol].value = value;
105
+ syncFormValue.call(this);
104
106
  }
105
107
 
106
108
  /**
@@ -129,7 +131,18 @@ function initControlReferences() {
129
131
  /**
130
132
  * @private
131
133
  */
132
- function initEventHandler() {}
134
+ function initEventHandler() {
135
+ this[inputElementSymbol].addEventListener("input", () => {
136
+ syncFormValue.call(this);
137
+ });
138
+ this[inputElementSymbol].addEventListener("change", () => {
139
+ syncFormValue.call(this);
140
+ });
141
+ }
142
+
143
+ function syncFormValue() {
144
+ this.setFormValue(this.value);
145
+ }
133
146
 
134
147
  /**
135
148
  * @private
@@ -200,6 +200,8 @@ const pricingCacheSymbol = Symbol("pricingCache");
200
200
  * @fires monster-buy-box-loaded
201
201
  */
202
202
  class BuyBox extends CustomControl {
203
+ static formAssociated = false;
204
+
203
205
  /**
204
206
  * This method is called by the `instanceof` operator.
205
207
  * @return {symbol}
@@ -89,6 +89,8 @@ const pendingRequestSymbol = Symbol("pendingRequest");
89
89
  * @fires monster-cart-control-update
90
90
  */
91
91
  class CartControl extends CustomControl {
92
+ static formAssociated = false;
93
+
92
94
  static get [instanceSymbol]() {
93
95
  return Symbol.for(
94
96
  "@schukai/monster/components/form/cart-control@@instance",
@@ -113,6 +113,18 @@ const keyFilterEventSymbol = Symbol("keyFilterEvent");
113
113
  */
114
114
  const lazyLoadDoneSymbol = Symbol("lazyLoadDone");
115
115
 
116
+ /**
117
+ * @private
118
+ * @type {Symbol}
119
+ */
120
+ const lazyLoadRequestSymbol = Symbol("lazyLoadRequest");
121
+
122
+ /**
123
+ * @private
124
+ * @type {Symbol}
125
+ */
126
+ const lazyLoadErrorSymbol = Symbol("lazyLoadError");
127
+
116
128
  /**
117
129
  * @private
118
130
  * @type {Symbol}
@@ -300,6 +312,7 @@ const optionsMapVersionSnapshotSymbol = Symbol("optionsMapVersionSnapshot");
300
312
  const selectionVersionSymbol = Symbol("selectionVersion");
301
313
  const closeOnSelectAutoSymbol = Symbol("closeOnSelectAuto");
302
314
  const lastFilterValueSymbol = Symbol("lastFilterValue");
315
+ const filterInputVersionSymbol = Symbol("filterInputVersion");
303
316
 
304
317
  /**
305
318
  * @private
@@ -2051,6 +2064,7 @@ function invalidatePendingRemoteWork() {
2051
2064
  }
2052
2065
  this[isLoadingSymbol] = false;
2053
2066
  this[remoteInfoRequestSymbol] = null;
2067
+ delete this[lazyLoadRequestSymbol];
2054
2068
 
2055
2069
  if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) {
2056
2070
  try {
@@ -4014,6 +4028,7 @@ function handleFilterInputEvents() {
4014
4028
  }
4015
4029
 
4016
4030
  this[lastFilterValueSymbol] = filterValue;
4031
+ bumpFilterInputVersion.call(this);
4017
4032
 
4018
4033
  if (this[keyFilterEventSymbol] instanceof DeadMansSwitch) {
4019
4034
  try {
@@ -4061,6 +4076,16 @@ function getCurrentFilterInputValue() {
4061
4076
  return undefined;
4062
4077
  }
4063
4078
 
4079
+ function getFilterInputVersion() {
4080
+ return Number.isInteger(this[filterInputVersionSymbol])
4081
+ ? this[filterInputVersionSymbol]
4082
+ : 0;
4083
+ }
4084
+
4085
+ function bumpFilterInputVersion() {
4086
+ this[filterInputVersionSymbol] = getFilterInputVersion.call(this) + 1;
4087
+ }
4088
+
4064
4089
  /**
4065
4090
  * @private
4066
4091
  */
@@ -4601,7 +4626,15 @@ function areOptionsAvailableAndInitInternal() {
4601
4626
  options === null ||
4602
4627
  (isArray(options) && options.length === 0)
4603
4628
  ) {
4604
- setStatusOrRemoveBadges.call(this, "empty");
4629
+ const lazyLoadPending = this[lazyLoadRequestSymbol] instanceof Promise;
4630
+ const lazyLoadError = isString(this[lazyLoadErrorSymbol]);
4631
+ if (lazyLoadError) {
4632
+ setStatusOrRemoveBadges.call(this, "error");
4633
+ } else if (lazyLoadPending) {
4634
+ setStatusOrRemoveBadges.call(this, "loading");
4635
+ } else {
4636
+ setStatusOrRemoveBadges.call(this, "empty");
4637
+ }
4605
4638
  if (getFilterMode.call(this) === FILTER_MODE_REMOTE) {
4606
4639
  if (this[isLoadingSymbol] !== true) {
4607
4640
  if (isInteger(this.getOption("total"))) {
@@ -4615,7 +4648,9 @@ function areOptionsAvailableAndInitInternal() {
4615
4648
 
4616
4649
  let msg = this.getOption("labels.no-options-available");
4617
4650
 
4618
- if (
4651
+ if (lazyLoadError) {
4652
+ msg = this.getOption("labels.cannot-be-loaded");
4653
+ } else if (
4619
4654
  this.getOption("url") !== null &&
4620
4655
  this.getOption("features.lazyLoad") === true &&
4621
4656
  this[lazyLoadDoneSymbol] !== true
@@ -4651,7 +4686,11 @@ function areOptionsAvailableAndInitInternal() {
4651
4686
  if (this.getOption("features.emptyValueIfNoOptions") === true) {
4652
4687
  this.value = "";
4653
4688
  }
4654
- if (this[isLoadingSymbol] !== true) {
4689
+ if (
4690
+ this[isLoadingSymbol] !== true &&
4691
+ lazyLoadPending === false &&
4692
+ lazyLoadError === false
4693
+ ) {
4655
4694
  addErrorAttribute(this, "No options available.");
4656
4695
  }
4657
4696
  return false;
@@ -5087,24 +5126,65 @@ function show() {
5087
5126
  this.getOption("features.lazyLoad") && this[lazyLoadDoneSymbol] !== true;
5088
5127
 
5089
5128
  if (lazyLoadFlag === true) {
5090
- this[lazyLoadDoneSymbol] = true;
5129
+ if (this[lazyLoadRequestSymbol] instanceof Promise) {
5130
+ return;
5131
+ }
5132
+
5133
+ if (isString(this[lazyLoadErrorSymbol])) {
5134
+ removeErrorAttribute(this, this[lazyLoadErrorSymbol]);
5135
+ delete this[lazyLoadErrorSymbol];
5136
+ }
5137
+ removeErrorAttribute(this, "No options available.");
5091
5138
  setStatusOrRemoveBadges.call(this, "loading");
5092
5139
 
5093
- new Processing(200, () => {
5094
- this.fetch()
5095
- .then(() => {
5096
- checkOptionState.call(this);
5097
- requestAnimationFrame(() => {
5140
+ const lifecycleVersion = getRemoteLifecycleVersion.call(this);
5141
+ const lazyLoadRequest = new Processing(200, () => {
5142
+ if (
5143
+ !isRemoteLifecycleCurrent.call(this, lifecycleVersion) ||
5144
+ this.isConnected !== true
5145
+ ) {
5146
+ return;
5147
+ }
5148
+
5149
+ return this.fetch();
5150
+ }).run();
5151
+ this[lazyLoadRequestSymbol] = lazyLoadRequest;
5152
+
5153
+ lazyLoadRequest
5154
+ .then(() => {
5155
+ if (
5156
+ this[lazyLoadRequestSymbol] !== lazyLoadRequest ||
5157
+ !isRemoteLifecycleCurrent.call(this, lifecycleVersion) ||
5158
+ this.isConnected !== true
5159
+ ) {
5160
+ return;
5161
+ }
5162
+
5163
+ delete this[lazyLoadRequestSymbol];
5164
+ this[lazyLoadDoneSymbol] = true;
5165
+ delete this[lazyLoadErrorSymbol];
5166
+ checkOptionState.call(this);
5167
+ requestAnimationFrame(() => {
5168
+ if (
5169
+ isRemoteLifecycleCurrent.call(this, lifecycleVersion) &&
5170
+ this.isConnected === true
5171
+ ) {
5098
5172
  show.call(this);
5099
- });
5100
- })
5101
- .catch((e) => {
5102
- addErrorAttribute(this, e);
5103
- setStatusOrRemoveBadges.call(this, "error");
5173
+ }
5104
5174
  });
5105
- })
5106
- .run()
5175
+ })
5107
5176
  .catch((e) => {
5177
+ if (
5178
+ this[lazyLoadRequestSymbol] !== lazyLoadRequest ||
5179
+ !isRemoteLifecycleCurrent.call(this, lifecycleVersion)
5180
+ ) {
5181
+ return;
5182
+ }
5183
+
5184
+ delete this[lazyLoadRequestSymbol];
5185
+ this[lazyLoadDoneSymbol] = false;
5186
+ this[lazyLoadErrorSymbol] =
5187
+ e instanceof Error ? e.message : String(e);
5108
5188
  addErrorAttribute(this, e);
5109
5189
  setStatusOrRemoveBadges.call(this, "error");
5110
5190
  });
@@ -5141,6 +5221,8 @@ function show() {
5141
5221
  addAttributeToken(this[controlElementSymbol], "class", "open");
5142
5222
  registerWithHost.call(this);
5143
5223
 
5224
+ const filterInputVersion = getFilterInputVersion.call(this);
5225
+
5144
5226
  new Processing(() => {
5145
5227
  const shouldLoadRemoteOptions =
5146
5228
  getFilterMode.call(self) === FILTER_MODE_REMOTE &&
@@ -5152,12 +5234,18 @@ function show() {
5152
5234
 
5153
5235
  if (shouldLoadDefaultOptions) {
5154
5236
  setTimeout(() => {
5237
+ if (getFilterInputVersion.call(this) !== filterInputVersion) {
5238
+ return;
5239
+ }
5155
5240
  loadDefaultOptionsFromUrl.call(self).catch((e) => {
5156
5241
  addErrorAttribute(self, e);
5157
5242
  });
5158
5243
  }, 0);
5159
5244
  } else if (shouldLoadRemoteOptions) {
5160
5245
  setTimeout(() => {
5246
+ if (getFilterInputVersion.call(this) !== filterInputVersion) {
5247
+ return;
5248
+ }
5161
5249
  self[cleanupOptionsListSymbol] = true;
5162
5250
  filterFromRemote.call(self).catch((e) => {
5163
5251
  addErrorAttribute(self, e);
@@ -35,6 +35,8 @@ export { State };
35
35
  * @summary A compact state indicator for statuses such as success, warning, error or informational workflow state.
36
36
  **/
37
37
  class State extends CustomControl {
38
+ static formAssociated = false;
39
+
38
40
  /**
39
41
  * @return {void}
40
42
  */
@@ -17,6 +17,8 @@ import { typeOf } from "../types/typeof.mjs";
17
17
 
18
18
  export { extend };
19
19
 
20
+ const unsafeMutationKeys = new Set(["__proto__", "prototype", "constructor"]);
21
+
20
22
  /**
21
23
  * Extend copies all enumerable own properties from one or
22
24
  * more source objects to a target object. It returns the modified target object.
@@ -50,7 +52,11 @@ function extend(...args) {
50
52
  continue;
51
53
  }
52
54
 
53
- for (const k in a) {
55
+ for (const k of Object.keys(a)) {
56
+ if (unsafeMutationKeys.has(k)) {
57
+ continue;
58
+ }
59
+
54
60
  const v = a?.[k];
55
61
 
56
62
  if (k in o && v === o?.[k]) {
@@ -16,7 +16,7 @@ import { extend } from "../data/extend.mjs";
16
16
  import { addAttributeToken } from "./attributes.mjs";
17
17
  import { ATTRIBUTE_ERRORMESSAGE } from "./constants.mjs";
18
18
  import { CustomElement, attributeObserverSymbol } from "./customelement.mjs";
19
- import { instanceSymbol } from "../constants.mjs";
19
+ import { instanceSymbol, internalSymbol } from "../constants.mjs";
20
20
  import { DeadMansSwitch } from "../util/deadmansswitch.mjs";
21
21
  import { addErrorAttribute } from "./error.mjs";
22
22
  import { Observer } from "../types/observer.mjs";
@@ -28,6 +28,8 @@ export { CustomControl };
28
28
  * @type {symbol}
29
29
  */
30
30
  const attachedInternalSymbol = Symbol("attachedInternal");
31
+ const preUpgradeValueSymbol = Symbol("preUpgradeValue");
32
+ const preUpgradeValuePendingSymbol = Symbol("preUpgradeValuePending");
31
33
 
32
34
  /**
33
35
  * This is a base class for creating custom controls using the power of CustomElement.
@@ -77,6 +79,12 @@ class CustomControl extends CustomElement {
77
79
  constructor() {
78
80
  super();
79
81
 
82
+ if (Object.prototype.hasOwnProperty.call(this, "value")) {
83
+ this[preUpgradeValueSymbol] = this.value;
84
+ this[preUpgradeValuePendingSymbol] = true;
85
+ delete this.value;
86
+ }
87
+
80
88
  // check if element supports `attachInternals()`
81
89
  if (typeof this["attachInternals"] === "function") {
82
90
  this[attachedInternalSymbol] = this.attachInternals();
@@ -130,6 +138,13 @@ class CustomControl extends CustomElement {
130
138
  if (typeof disabledObserver === "function") {
131
139
  disabledObserver();
132
140
  }
141
+
142
+ if (this[preUpgradeValuePendingSymbol] === true) {
143
+ const value = this[preUpgradeValueSymbol];
144
+ this[preUpgradeValuePendingSymbol] = false;
145
+ delete this[preUpgradeValueSymbol];
146
+ this.value = value;
147
+ }
133
148
  }
134
149
 
135
150
  /**
@@ -309,35 +324,23 @@ class CustomControl extends CustomElement {
309
324
  }
310
325
 
311
326
  /**
312
- * Sets the `form` attribute of the custom control to the `id` of the passed form element.
313
- * If no form element is passed, removes the `form` attribute.
327
+ * Receives changes to the browser-managed form association. The author-provided
328
+ * `form` attribute remains the source of explicit associations.
314
329
  *
315
330
  * @param {HTMLFormElement} form - The form element to associate with the control
316
331
  */
317
- formAssociatedCallback(form) {
318
- if (form) {
319
- if (form.id) {
320
- this.setAttribute("form", form.id);
321
- }
322
- } else {
323
- this.removeAttribute("form");
324
- }
325
- }
332
+ formAssociatedCallback(form) {}
326
333
 
327
334
  /**
328
- * Sets or removes the `disabled` attribute of the custom control based on the passed value.
335
+ * Synchronizes browser-computed disabledness without changing the author-provided
336
+ * `disabled` attribute or option.
329
337
  *
330
338
  * @param {boolean} disabled - Whether or not the control should be disabled
331
339
  */
332
340
  formDisabledCallback(disabled) {
333
- if (disabled) {
334
- if (!this.hasAttribute("disabled")) {
335
- this.setAttribute("disabled", "");
336
- }
337
- } else {
338
- if (this.hasAttribute("disabled")) {
339
- this.removeAttribute("disabled");
340
- }
341
+ this[internalSymbol].formDisabledValue = disabled === true;
342
+ if (typeof this[internalSymbol].syncDisabledState === "function") {
343
+ this[internalSymbol].syncDisabledState(true);
341
344
  }
342
345
  }
343
346
 
@@ -345,7 +348,9 @@ class CustomControl extends CustomElement {
345
348
  * @param {string} state
346
349
  * @param {string} mode
347
350
  */
348
- formStateRestoreCallback(state, mode) {}
351
+ formStateRestoreCallback(state, mode) {
352
+ this.value = state;
353
+ }
349
354
 
350
355
  /**
351
356
  *
@@ -402,7 +407,7 @@ function initValueAttributeObserver() {
402
407
 
403
408
  if (oldValue !== newValue) {
404
409
  setTimeout(() => {
405
- self.value = self.getAttribute("value");
410
+ self.value = self.getAttribute("value") ?? "";
406
411
  }, 0);
407
412
  }
408
413
  });