@optionfactory/ful 0.90.0 → 0.92.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/ful.mjs CHANGED
@@ -70,21 +70,51 @@ class Hex {
70
70
  }
71
71
  }
72
72
 
73
+ /**
74
+ * @typedef {{ type: string; context: string?; reason: string; details: any?; }} Problem
75
+ */
73
76
  class Failure extends Error {
74
- constructor(name, problems, cause) {
75
- super(JSON.stringify(problems), { cause });
76
- this.name = name;
77
+ /**
78
+ *
79
+ * @param {string} message
80
+ * @param {Problem[]} problems
81
+ * @param {*} cause
82
+ */
83
+ constructor(message, problems, cause) {
84
+ super(message, { cause });
85
+ this.name = 'Failure';
77
86
  this.problems = problems;
78
87
  }
79
88
  }
80
89
 
90
+ /**
91
+ * @typedef {Int8Array| Uint8Array| Uint8ClampedArray| Int16Array| Uint16Array| Int32Array| Uint32Array| Float32Array| Float64Array| BigInt64Array| BigUint64Array} TypedArray
92
+ */
93
+ /**
94
+ * @typedef HttpInterceptor
95
+ * @property {function(Request,HttpInterceptorChain):Promise<Response>} intercept
96
+ */
97
+
81
98
  class HttpClientError extends Failure {
82
- constructor(status, problems, cause) {
83
- super(`HttpClientError:${status}`, problems, cause);
99
+ /**
100
+ * @param {string} message
101
+ * @param {number} status
102
+ * @param {{ type: string; context: string?; reason: string; details: any?; }[]} problems
103
+ * @param {Error|undefined} [cause]
104
+ */
105
+ constructor(message, status, problems, cause) {
106
+ super(message, problems, cause);
107
+ this.name = 'HttpClientError';
84
108
  this.status = status;
85
109
  }
110
+ /**
111
+ *
112
+ * @param {string} type
113
+ * @param {any} cause
114
+ * @returns
115
+ */
86
116
  static of(type, cause) {
87
- return new HttpClientError(0, [{
117
+ return new HttpClientError(cause.message, 0, [{
88
118
  type,
89
119
  context: null,
90
120
  reason: cause.message,
@@ -98,47 +128,61 @@ class HttpClientError extends Failure {
98
128
  */
99
129
  static async fromResponse(response) {
100
130
  const text = await response.text();
101
- const def = [{
131
+ const message = `${response.status} ${response.statusText}: ${text}`;
132
+ const fallback = [{
102
133
  type: "GENERIC_PROBLEM",
103
134
  context: null,
104
- reason: `${response.status} ${response.statusText}: ${text}`,
135
+ reason: message,
105
136
  details: null
106
137
  }];
107
138
  try {
108
- return new HttpClientError(response.status, text ? JSON.parse(text) : def);
139
+ return new HttpClientError(message, response.status, text ? JSON.parse(text) : fallback);
109
140
  } catch (e) {
110
- return new HttpClientError(response.status, def);
141
+ return new HttpClientError(message, response.status, fallback);
111
142
  }
112
143
  }
113
144
  }
114
145
 
146
+ /**
147
+ * @implements {HttpInterceptor}
148
+ */
115
149
  class CsrfTokenInterceptor {
116
150
  #k; #v;
117
151
  constructor() {
118
- this.#k = document.querySelector("meta[name='_csrf_header']").getAttribute("content");
119
- this.#v = document.querySelector("meta[name='_csrf']").getAttribute("content");
120
- }
152
+ this.#k = document.querySelector("meta[name='_csrf_header']")?.getAttribute("content");
153
+ this.#v = document.querySelector("meta[name='_csrf']")?.getAttribute("content");
154
+ }
121
155
  async intercept(request, chain) {
122
- request.headers.set(this.#k, this.#v);
156
+ if(this.#k && this.#v) {
157
+ request.headers.set(this.#k, this.#v);
158
+ }
123
159
  return await chain.proceed(request);
124
160
  }
125
161
  }
126
-
162
+ /**
163
+ * @implements {HttpInterceptor}
164
+ */
127
165
  class RedirectOnUnauthorizedInterceptor {
128
166
  #redirectUri;
167
+ /**
168
+ * @param {string} redirectUri
169
+ */
129
170
  constructor(redirectUri) {
130
171
  this.#redirectUri = redirectUri;
131
172
  }
132
173
  async intercept(request, chain) {
133
174
  const response = await chain.proceed(request);
134
- if (response.status !== 401) {
135
- return response;
175
+ if (response.status === 401) {
176
+ window.location.href = this.#redirectUri;
136
177
  }
137
- window.location.href = this.#redirectUri;
178
+ return response;
138
179
  }
139
180
  }
140
181
 
141
182
  class HttpClientBuilder {
183
+ /**
184
+ * @type {HttpInterceptor[]}
185
+ */
142
186
  #interceptors;
143
187
  constructor() {
144
188
  this.#interceptors = [];
@@ -151,6 +195,9 @@ class HttpClientBuilder {
151
195
  this.#interceptors.push(new RedirectOnUnauthorizedInterceptor(redirectUri));
152
196
  return this;
153
197
  }
198
+ /**
199
+ * @param {...HttpInterceptor} interceptors
200
+ */
154
201
  withInterceptors(...interceptors) {
155
202
  this.#interceptors.push(...interceptors);
156
203
  return this;
@@ -160,27 +207,35 @@ class HttpClientBuilder {
160
207
  }
161
208
  }
162
209
 
210
+ /**
211
+ * @implements {HttpInterceptor}
212
+ */
163
213
  class HttpCall {
164
- /**
165
- *
166
- * @async
167
- * @param {Request} request
168
- * @param {HttpInterceptorChain} chain
169
- * @returns {Promise<Response>} the response
170
- */
171
214
  async intercept(request, chain) {
172
215
  return await fetch(request);
173
216
  }
174
217
  }
175
218
 
176
219
  class HttpInterceptorChain {
220
+ #interceptors;
221
+ #current;
222
+ /**
223
+ *
224
+ * @param {HttpInterceptor[]} interceptors
225
+ * @param {number} current
226
+ */
177
227
  constructor(interceptors, current) {
178
- this.interceptors = interceptors;
179
- this.current = current;
228
+ this.#interceptors = interceptors;
229
+ this.#current = current;
180
230
  }
231
+ /**
232
+ *
233
+ * @param {Request} request
234
+ * @returns {Promise<Response>} the response
235
+ */
181
236
  async proceed(request) {
182
- const interceptor = this.interceptors[this.current];
183
- return await interceptor.intercept(request, new HttpInterceptorChain(this.interceptors, this.current + 1));
237
+ const interceptor = this.#interceptors[this.#current];
238
+ return await interceptor.intercept(request, new HttpInterceptorChain(this.#interceptors, this.#current + 1));
184
239
  }
185
240
  }
186
241
 
@@ -188,14 +243,14 @@ class HttpClient {
188
243
  #interceptors;
189
244
  /**
190
245
  * Creates a builder for an HttpClient.
191
- * @returns {HttpRequestBuilder} the client builder
246
+ * @returns {HttpClientBuilder} the client builder
192
247
  */
193
248
  static builder() {
194
249
  return new HttpClientBuilder();
195
250
  }
196
251
  /**
197
252
  * Creates an HttpClient.
198
- * @returns {[HttpInterceptor]} interceptors - a list of interceptors to be registered for every request performed by the created client.
253
+ * @param {HttpInterceptor[]|undefined} interceptors - a list of interceptors to be registered for every request performed by the created client.
199
254
  */
200
255
  constructor(interceptors) {
201
256
  this.#interceptors = interceptors || [];
@@ -205,7 +260,7 @@ class HttpClient {
205
260
  * @async
206
261
  * @param {string} uri - the (possibly relative) request url
207
262
  * @param {RequestInit|undefined} options - fetch options
208
- * @param {[any]|undefined} interceptors - the HttpInterceptors to be registered for this request.
263
+ * @param {HttpInterceptor[]|undefined} interceptors - the HttpInterceptors to be registered for this exchange.
209
264
  * @returns {Promise<Response>} the response
210
265
  */
211
266
  async exchange(uri, options, interceptors) {
@@ -272,12 +327,17 @@ class HttpClient {
272
327
  }
273
328
  }
274
329
 
275
-
330
+ /**
331
+ *
332
+ * @param {Response} response
333
+ * @param {'text'|'json'|'blob'|'arrayBuffer'} type
334
+ * @returns
335
+ */
276
336
  const unmarshal = async (response, type) => {
277
337
  try {
278
338
  return await response[type]();
279
- } catch (e) {
280
- throw HttpClientError.of("UNMARSHALING_PROBLEM", e);
339
+ } catch (ex) {
340
+ throw HttpClientError.of("UNMARSHALING_PROBLEM", ex);
281
341
  }
282
342
  };
283
343
 
@@ -319,7 +379,7 @@ class HttpRequestBuilder {
319
379
  * @param {Headers} headers
320
380
  * @param {any} body
321
381
  * @param {Omit<RequestInit,"headers"|"method"|"body">} options
322
- * @param {[HttpInterceptor]} interceptors
382
+ * @param {HttpInterceptor[]} interceptors
323
383
  */
324
384
  constructor(client, method, uri, params, headers, body, options, interceptors) {
325
385
  this.#client = client;
@@ -333,7 +393,7 @@ class HttpRequestBuilder {
333
393
  }
334
394
  /**
335
395
  * Add all passed headers to the request, overriding existing ones if that key already exists.
336
- * @param {headersInit} hs
396
+ * @param {HeadersInit} hs
337
397
  * @returns {HttpRequestBuilder} this builder
338
398
  */
339
399
  headers(hs) {
@@ -395,6 +455,18 @@ class HttpRequestBuilder {
395
455
  this.#body = JSON.stringify(body);
396
456
  return this;
397
457
  }
458
+ /**
459
+ * Sets the request body as a FormData configured using the callback.
460
+ * `Content-Type: multipart/form-data` header is automatically added by fetch if not explicitly set.
461
+ * @param {function(HttpMultipartRequestCustomizer):void} callback
462
+ */
463
+ multipart(callback) {
464
+ const formData = new FormData();
465
+ const builder = new HttpMultipartRequestCustomizer(formData);
466
+ callback(builder);
467
+ this.#body = formData;
468
+ return this;
469
+ }
398
470
  /**
399
471
  * Sets a fetch options for the request.
400
472
  * @param {Omit<RequestInit,"headers"|"method"|"body">} kvs
@@ -402,6 +474,7 @@ class HttpRequestBuilder {
402
474
  */
403
475
  options(kvs) {
404
476
  for (const [k, v] of Object.entries(kvs)) {
477
+ // @ts-ignore
405
478
  this.#options[k] = v;
406
479
  }
407
480
  return this;
@@ -468,11 +541,11 @@ class HttpRequestBuilder {
468
541
  throw await HttpClientError.fromResponse(response);
469
542
  }
470
543
  return response;
471
- } catch (e) {
472
- if (e instanceof Failure) {
473
- throw e;
544
+ } catch (ex) {
545
+ if (ex instanceof Failure) {
546
+ throw ex;
474
547
  }
475
- throw HttpClientError.of("CONNECTION_PROBLEM", e);
548
+ throw HttpClientError.of("CONNECTION_PROBLEM", ex);
476
549
  }
477
550
  }
478
551
  /**
@@ -491,14 +564,6 @@ class HttpRequestBuilder {
491
564
  const response = await this.fetch();
492
565
  return await unmarshal(response, 'json');
493
566
  }
494
- /**
495
- * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
496
- * @returns {Promise<Uint8Array>} the response body, as an Uint8Array
497
- */
498
- async fetchBytes() {
499
- const response = await this.fetch();
500
- return await unmarshal(response, 'bytes');
501
- }
502
567
  /**
503
568
  * Performs an HTTP exchange using the configured client request, and interceptos throwing a failure when response status is not in the 200-299 range.
504
569
  * @returns {Promise<Blob>} the response body, as a Blob
@@ -517,6 +582,68 @@ class HttpRequestBuilder {
517
582
  }
518
583
  }
519
584
 
585
+
586
+ class HttpMultipartRequestCustomizer {
587
+ #formData;
588
+ /**
589
+ *
590
+ * @param {FormData} formData
591
+ */
592
+ constructor(formData){
593
+ this.#formData = formData;
594
+ }
595
+ /**
596
+ * Appends a value to the FormData.
597
+ * @param {string} name
598
+ * @param {*} value
599
+ * @returns this builder
600
+ */
601
+ field(name, value){
602
+ this.#formData.append(name, value);
603
+ return this;
604
+ }
605
+ /**
606
+ * Appends a Blob to the FormData.
607
+ * If `filename` is omitted, FormData defaults are applied:
608
+ * The default filename for Blob objects is "blob";
609
+ * The default filename for File objects is the file's filename.
610
+ * @param {string} name
611
+ * @param {Blob} value
612
+ * @param {string|undefined} filename
613
+ * @returns this builder
614
+ */
615
+ blob(name, value, filename){
616
+ this.#formData.append(name, value, filename);
617
+ return this;
618
+ }
619
+ /**
620
+ * Appends multiple Blobs to the FormData with the same name.
621
+ * The default filename for Blob objects is "blob";
622
+ * The default filename for File objects is the file's filename.
623
+ * @param {string} name
624
+ * @param {Blob[]} values
625
+ * @returns this builder
626
+ */
627
+ blobs(name, values){
628
+ for(let v of values){
629
+ this.#formData.append(name, v);
630
+ }
631
+ return this;
632
+ }
633
+ /**
634
+ * Appends a JSON serialized blob to the FormData.
635
+ * @param {string} name
636
+ * @param {any} value
637
+ * @param {string|undefined} filename
638
+ * @returns this builder
639
+ */
640
+ json(name, value, filename){
641
+ const blob = new Blob([JSON.stringify(value)], {type: 'application/json'});
642
+ this.#formData.append(name, blob, filename);
643
+ return this;
644
+ }
645
+ }
646
+
520
647
  class Storage {
521
648
  constructor(prefix, storage) {
522
649
  this.prefix = prefix;
@@ -613,7 +740,7 @@ class AuthorizationCodeFlow {
613
740
  Object.entries(additionalParams || {}).forEach(kv => {
614
741
  url.searchParams.set(kv[0], kv[1]);
615
742
  });
616
- window.location = url;
743
+ window.location.href = url.toString();
617
744
  }
618
745
  async registration(additionalParams){
619
746
  await this.action(this.uri.registration, additionalParams);
@@ -623,7 +750,7 @@ class AuthorizationCodeFlow {
623
750
  kc_action: kcAction
624
751
  });
625
752
  }
626
- async _tokenExchange(code, state) {
753
+ async #tokenExchange(code, state) {
627
754
  window.history.replaceState('', "", this.uri.redirect);
628
755
  const stateAndVerifier = this.storage.pop(AuthorizationCodeFlow.PKCE_AND_STATE_KEY);
629
756
  if (stateAndVerifier.state !== state) {
@@ -656,7 +783,7 @@ class AuthorizationCodeFlow {
656
783
  if (code && this.storage.load(AuthorizationCodeFlow.PKCE_AND_STATE_KEY)) {
657
784
  //if callback from keycloak and we have our state still stored
658
785
  const state = url.searchParams.get("state");
659
- return await this._tokenExchange(code, state);
786
+ return await this.#tokenExchange(code, state);
660
787
  }
661
788
  //if not authorized
662
789
  await this.action(this.uri.auth, {});
@@ -727,7 +854,7 @@ class AuthorizationCodeFlowSession {
727
854
  const url = new URL(this.uri.logout);
728
855
  url.searchParams.set("post_logout_redirect_uri", this.uri.redirect);
729
856
  url.searchParams.set("id_token_hint", this.token.id_token);
730
- window.location = url;
857
+ window.location.href = url.toString();
731
858
  }
732
859
 
733
860
  bearerToken() {
@@ -786,7 +913,7 @@ const timing = {
786
913
  };
787
914
 
788
915
  return function () {
789
- args = arguments;
916
+ args = [...arguments];
790
917
  previousTimestamp = new Date().getTime();
791
918
  if (tid === null) {
792
919
  tid = setTimeout(later, timeoutMs);
@@ -820,7 +947,7 @@ const timing = {
820
947
  previousTimestamp = now;
821
948
  }
822
949
  const remaining = timeoutMs - (now - previousTimestamp);
823
- args = arguments;
950
+ args = [...arguments];
824
951
  if (remaining <= 0 || remaining > timeoutMs) {
825
952
  if (tid !== null) {
826
953
  clearTimeout(tid);
@@ -926,16 +1053,18 @@ class Attributes {
926
1053
  .forEach(a => {
927
1054
  const target = a.substring(prefix.length);
928
1055
  if (target === 'class') {
929
- to.classList.add(...from.getAttribute(prefix + "class").split(" ").filter(a => a.length));
1056
+ const classes = from.getAttribute(prefix + "class")?.split(" ").filter(a => a.length) ?? [];
1057
+ to.classList.add(...classes);
930
1058
  return;
931
1059
  }
1060
+ // @ts-ignore
932
1061
  to.setAttribute(target, from.getAttribute(a));
933
1062
  });
934
1063
  }
935
1064
  /**
936
1065
  *
937
1066
  * @param {HTMLElement} el
938
- * @param {stirng} attr
1067
+ * @param {string} attr
939
1068
  * @param {boolean} value
940
1069
  */
941
1070
  static toggle(el, attr, value) {
@@ -962,19 +1091,21 @@ class LightSlots {
962
1091
  * @returns the slots
963
1092
  */
964
1093
  static from(el) {
1094
+ /** @type [string, Element][] */
965
1095
  const namedSlots = Array.from(el.childNodes)
966
- .filter(el => el.matches && el.matches('[slot]'))
1096
+ .filter(el => el instanceof Element)
1097
+ .filter(el => el.matches('[slot]'))
967
1098
  .map(el => {
968
1099
  el.remove();
969
1100
  const slot = el.getAttribute("slot");
970
1101
  el.removeAttribute("slot");
971
- return [slot, el];
1102
+ return [slot ?? 'unnamed', el];
972
1103
  });
973
1104
  const slots = {};
974
1105
  slots.default = new DocumentFragment();
975
1106
  slots.default.append(...el.childNodes);
976
- for(const [name,el] of namedSlots){
977
- if(!(name in slots)){
1107
+ for (const [name, el] of namedSlots) {
1108
+ if (!(name in slots)) {
978
1109
  slots[name] = new DocumentFragment();
979
1110
  }
980
1111
  slots[name].append(el);
@@ -1000,7 +1131,8 @@ class TemplatesRegistry {
1000
1131
  #ec;
1001
1132
  put(k, fragment) {
1002
1133
  if (this.#ec) {
1003
- this.#idToTemplate[k] = Template.fromFragment(fragment, ec);
1134
+ // @ts-ignore
1135
+ this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, this.#ec);
1004
1136
  return;
1005
1137
  }
1006
1138
  this.#idToFragment[k] = fragment;
@@ -1019,6 +1151,7 @@ class TemplatesRegistry {
1019
1151
  this.#ec = ec;
1020
1152
  for (const [k, fragment] of Object.entries(this.#idToFragment)) {
1021
1153
  delete this.#idToFragment[k];
1154
+ // @ts-ignore
1022
1155
  this.#idToTemplate[k] = ftl.Template.fromFragment(fragment, ec, ...data);
1023
1156
  }
1024
1157
  }
@@ -1122,8 +1255,8 @@ const ParsedElement = (conf) => {
1122
1255
  #initialized;
1123
1256
  #reflecting;
1124
1257
  #internals;
1125
- constructor(...args) {
1126
- super(...args);
1258
+ constructor() {
1259
+ super();
1127
1260
  this.#internals = this.attachInternals();
1128
1261
  }
1129
1262
  get initialized() {
@@ -1151,6 +1284,7 @@ const ParsedElement = (conf) => {
1151
1284
  observer.disconnect();
1152
1285
  upgradeQueue.enqueue(this);
1153
1286
  });
1287
+ // @ts-ignore
1154
1288
  observer.observe(this.parentNode, { childList: true, subtree: true });
1155
1289
  }
1156
1290
  attributeChangedCallback(attr, oldValue, newValue) {
@@ -1176,6 +1310,7 @@ const ParsedElement = (conf) => {
1176
1310
  return;
1177
1311
  }
1178
1312
  this.#parsed = true;
1313
+ // @ts-ignore
1179
1314
  await this.render(elements.template(templateId), slots ? LightSlots.from(this) : undefined);
1180
1315
 
1181
1316
  for (const [attr, mapper] of attrsAndMappers) {
@@ -1278,6 +1413,7 @@ class Form extends ParsedElement() {
1278
1413
  static IGNORED_CHILDREN_SELECTOR = '.d-none, [hidden]';
1279
1414
  static SCROLL_OFFSET = 50;
1280
1415
  static INVALID_CLASS = 'is-invalid';
1416
+ submitter;
1281
1417
  render() {
1282
1418
  const form = document.createElement('form');
1283
1419
  form.replaceChildren(...this.childNodes);
@@ -1290,8 +1426,14 @@ class Form extends ParsedElement() {
1290
1426
  this.replaceChildren(form);
1291
1427
  }
1292
1428
  spinner(spin) {
1293
- this.querySelectorAll('ful-spinner').forEach(el => el.hidden = !spin);
1294
- this.querySelectorAll('[type=submit],[type=reset]').forEach(el => el.disabled = spin);
1429
+ this.querySelectorAll('ful-spinner').forEach(el => {
1430
+ const hel = /** @type HTMLElement} */ (el);
1431
+ hel.hidden = !spin;
1432
+ });
1433
+ this.querySelectorAll('[type=submit],[type=reset]').forEach(el => {
1434
+ const hel = /** @type HTMLButtonElement} */ (el);
1435
+ hel.disabled = spin;
1436
+ });
1295
1437
  }
1296
1438
  async remoting(fn) {
1297
1439
  try {
@@ -1326,8 +1468,8 @@ class Form extends ParsedElement() {
1326
1468
  }
1327
1469
  }
1328
1470
  get values() {
1329
- return Array.from(this.querySelectorAll('[name]'))
1330
- .filter((el) => {
1471
+ return Array.from(/** @type {NodeListOf<HTMLElement>} */ (this.querySelectorAll('[name]')))
1472
+ .filter(el => {
1331
1473
  if (el.dataset['fulBindInclude'] === 'never') {
1332
1474
  return false;
1333
1475
  }
@@ -1338,22 +1480,26 @@ class Form extends ParsedElement() {
1338
1480
  }, {});
1339
1481
  }
1340
1482
  set errors(es) {
1341
- const fieldErrors = es.filter((e) => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
1342
- const globalErrors = es.filter((e) => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
1483
+ const fieldErrors = es.filter(e => e.type === 'FIELD_ERROR' || e.type === 'INVALID_FORMAT');
1484
+ const globalErrors = es.filter(e => e.type !== 'FIELD_ERROR' && e.type !== 'INVALID_FORMAT');
1343
1485
  this.querySelectorAll(`.${Form.INVALID_CLASS}`).forEach(el => el.classList.remove(Form.INVALID_CLASS));
1344
1486
  this.querySelectorAll("ful-errors").forEach(el => {
1345
1487
  el.replaceChildren();
1346
1488
  el.setAttribute('hidden', '');
1347
1489
  });
1348
- fieldErrors.forEach((e) => {
1490
+ fieldErrors.forEach(e => {
1349
1491
  const name = e.context.replace("[", ".").replace("].", ".");
1350
1492
  const validationTargetsSelector = `[name='${CSS.escape(name)}'] [ful-validation-target],[name='${CSS.escape(name)}']:not(:has([ful-validation-target]))`;
1351
1493
  this.querySelectorAll(validationTargetsSelector).forEach(input => input.classList.add(Form.INVALID_CLASS));
1352
1494
  const fieldErrorsSelector = `ful-field-error[field='${CSS.escape(name)}']`;
1353
- this.querySelectorAll(fieldErrorsSelector).forEach(el => el.innerText = e.reason);
1495
+ this.querySelectorAll(fieldErrorsSelector).forEach(el => {
1496
+ const hel = /** @type HTMLElement} */ (el);
1497
+ hel.innerText = e.reason;
1498
+ });
1354
1499
  });
1355
1500
  this.querySelectorAll("ful-errors").forEach(el => {
1356
- el.innerText = globalErrors.map(e => e.reason).join("\n");
1501
+ const hel = /** @type HTMLElement} */ (el);
1502
+ hel.innerText = globalErrors.map(e => e.reason).join("\n");
1357
1503
  if (globalErrors.length !== 0) {
1358
1504
  el.removeAttribute('hidden');
1359
1505
  }
@@ -1416,6 +1562,7 @@ class Input extends ParsedElement({
1416
1562
  slots: true,
1417
1563
  template: INPUT_TEMPLATE
1418
1564
  }){
1565
+ input;
1419
1566
  render(template, slots) {
1420
1567
  const fragment = makeInputFragment(this, template, slots);
1421
1568
  this.replaceChildren(fragment);
@@ -1451,6 +1598,8 @@ class Select extends ParsedElement({
1451
1598
  </div>
1452
1599
  `
1453
1600
  }) {
1601
+ shouldLoad;
1602
+ _unwrappedRemoteLoad;
1454
1603
  constructor(tsConfig) {
1455
1604
  super();
1456
1605
  this.tsConfig = tsConfig;
@@ -1502,6 +1651,7 @@ class Select extends ParsedElement({
1502
1651
  }
1503
1652
  callback(data);
1504
1653
  };
1654
+ // @ts-ignore
1505
1655
  this.ts = new TomSelect(input, Object.assign(remote ? {
1506
1656
  preload: 'focus',
1507
1657
  load: this._unwrappedRemoteLoad,
@@ -1586,27 +1736,40 @@ class RadioGroup extends ParsedElement({
1586
1736
  input.addEventListener('change', evt => {
1587
1737
  evt.stopPropagation();
1588
1738
  //change is not cancelable
1589
- this.dispatchEvent(new CustomEvent('change', {
1590
- bubbles: true,
1591
- cancelable: false,
1739
+ this.dispatchEvent(new CustomEvent('change', {
1740
+ bubbles: true,
1741
+ cancelable: false,
1592
1742
  detail: {
1593
1743
  value: this.value
1594
1744
  }
1595
- }));
1596
- });
1745
+ }));
1746
+ });
1597
1747
  const label = Fragments.fromChildNodes(el);
1598
1748
  return [input, label];
1599
1749
  });
1600
1750
 
1601
1751
  radioEls.forEach(el => el.remove());
1602
- template.renderTo(this, {name, slots, inputsAndLabels});
1752
+ template.renderTo(this, { name, slots, inputsAndLabels });
1603
1753
  }
1604
1754
  get value() {
1755
+ /** @type {HTMLInputElement|null} */
1605
1756
  const checked = this.querySelector('input[type=radio]:checked');
1606
1757
  return checked ? checked.value : null;
1607
1758
  }
1608
1759
  set value(value) {
1609
- this.querySelector(`input[type=radio][value=${CSS.escape(value)}]`).checked = true;
1760
+ if (value === null) {
1761
+ /** @type {HTMLInputElement[]} */
1762
+ this.querySelectorAll(`input[type=radio]`).forEach(el => {
1763
+ // @ts-ignore
1764
+ el.checked = false;
1765
+ });
1766
+ return;
1767
+ }
1768
+ /** @type {HTMLInputElement|null} */
1769
+ const el = this.querySelector(`input[type=radio][value=${CSS.escape(value)}]`);
1770
+ if (el) {
1771
+ el.checked = true;
1772
+ }
1610
1773
  }
1611
1774
  }
1612
1775