@smartbit4all/ng-client 7.3.37 → 7.3.38

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/HTML-WIDGET.md CHANGED
@@ -14,7 +14,7 @@ html-t **két fázisban** oldjuk fel, két külön jelöléssel:
14
14
  | Fázis | Jelölés | Ki oldja fel | Mire jó |
15
15
  |---|---|---|---|
16
16
  | szerver-helyőrző | `${…}` | a backend, a page-ben, mielőtt a html elhagyja | feltétel, iteráció, szkript, lokalizáció — a platform meglévő template-dialektusa |
17
- | kliens-helyőrző | `{{…}}` | a kliens, a widget **feloldási kontextusán** | ugyanaz a html egyszer utazik és soronként / modell-változásonként újra feloldódik |
17
+ | kliens-helyőrző | `{{…}}` | a kliens, a widget **feloldási kontextusán** | ugyanaz a html egyszer utazik és soronként / modell-változásonként, a form egy kontrolljának minden szerkesztésére újra feloldódik |
18
18
 
19
19
  A kliens a szerver-fázisból semmit nem lát: mire a html megérkezik, a `${…}` már nincs benne.
20
20
 
@@ -40,7 +40,8 @@ A `DIV` widget változatlanul megmarad; a HTML widget annak a használatnak a re
40
40
  | `css` | a widget stíluslapja, a html mellett utazik — lásd *Stíluslap* |
41
41
  | `icons` | az **ikon-szótár**: `kulcs → ImageResource`, amit az ikon-slotok `data-sb4-icon-key`-jel címeznek — lásd *Ikon-slot* |
42
42
 
43
- A **feloldási kontextus** nézeten a `ComponentModel` (`data` + `valueSets`), grid-kártyán a sor
43
+ A **feloldási kontextus** nézeten a `ComponentModel` (`data` + `valueSets`), a `data`-ban a form
44
+ kontrolljainak még be nem küldött szerkesztéseivel, grid-kártyán a sor
44
45
  (`GridRow.data`) a gridet mutató oldal `valueSets`-ével. Ha a `root` listát, egyetlen értéket vagy
45
46
  semmit nevez, minden helyőrző üres marad, és a kliens egyszer `console.warn`-t ír.
46
47
 
@@ -4320,6 +4320,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
4320
4320
  args: [{ selector: '[smartEmbeddedSlot]' }]
4321
4321
  }], propDecorators: { containerId: [{ type: i0.Input, args: [{ isSignal: true, alias: "smartEmbeddedSlot", required: true }] }], containerUuid: [{ type: i0.Input, args: [{ isSignal: true, alias: "containerUuid", required: false }] }] } });
4322
4322
 
4323
+ /** Uri text as RFC 3986 gives it, the URI_PATTERN of UriUtils on the server. */
4324
+ const URI_PATTERN = new RegExp('^(?:(?:[a-zA-Z][a-zA-Z0-9+.-]*):)' + // scheme
4325
+ "(?://(?:[\\w\\-._~%!$&'()*+,;=:]+@)?" + // userinfo
4326
+ '(?:\\[[0-9A-Fa-f:.]+\\]|[\\w\\-._~%]+)' + // host (IP-literal / IPv4 / name)
4327
+ '(?::\\d{2,5})?)?' + // port
4328
+ "(?:/[\\w\\-._~%!$&'()*+,;=:@]*)*" + // path
4329
+ "(?:\\?[\\w\\-._~%!$&'()*+,;=:@/?]*)?" + // query
4330
+ "(?:#[\\w\\-._~%!$&'()*+,;=:@/?]*)?$" // fragment
4331
+ );
4332
+ /** The version suffix of the storage at the end of a path. */
4333
+ const VERSION_SUFFIX = /\.v(?:0|[1-9]\d*)$/;
4323
4334
  /**
4324
4335
  * The key and display property of a valueSet's values: the preferred name if the values carry it,
4325
4336
  * then the GenericValue name (`uri` / `name`), then the Value name (`objectUri` / `displayValue`).
@@ -4331,26 +4342,66 @@ function valueSetKeyProperty(values, preferred) {
4331
4342
  function valueSetDisplayProperty(values, preferred) {
4332
4343
  return firstExisting(values, [preferred ?? 'displayValue', 'name', 'displayValue']);
4333
4344
  }
4345
+ /**
4346
+ * The property of a value: a key the value has as it is, else a dotted path navigated part by
4347
+ * part (`data.name`); undefined when a part is missing.
4348
+ */
4349
+ function readValueSetProperty(value, property) {
4350
+ if (value === null || typeof value !== 'object' || property in value) {
4351
+ return value?.[property];
4352
+ }
4353
+ return property.split('.').reduce((part, name) => part?.[name], value);
4354
+ }
4334
4355
  /** The label of a code in a valueSet, or undefined when the valueSet does not know the code. */
4335
4356
  function valueSetLabelOf(valueSetData, code) {
4336
4357
  const values = valueSetData.values ?? [];
4337
4358
  const keyProperty = valueSetKeyProperty(values, valueSetData.keyProperty);
4338
- const hit = values.find((value) => String(value[keyProperty]) === String(code));
4339
- return hit?.[valueSetDisplayProperty(values)];
4359
+ const hit = values.find((value) => isSameKey(readValueSetProperty(value, keyProperty), code, valueSetData.versionedKeys));
4360
+ return hit && readValueSetProperty(hit, valueSetDisplayProperty(values));
4340
4361
  }
4341
4362
  function firstExisting(values, candidates) {
4342
4363
  if (!values?.length) {
4343
4364
  return candidates[0];
4344
4365
  }
4345
- return candidates.find((c) => c in values[0]) ?? candidates[candidates.length - 1];
4366
+ // A candidate exists when the first value reads it, also as a dotted path, or has it as a key.
4367
+ return (candidates.find((c) => readValueSetProperty(values[0], c) !== undefined || c in values[0]) ??
4368
+ candidates[candidates.length - 1]);
4346
4369
  }
4347
4370
  /** Whether a field holds no value: `''`, null, undefined or an empty list. */
4348
4371
  function isEmptyValue(value) {
4349
4372
  return value === '' || value == null || (Array.isArray(value) && value.length === 0);
4350
4373
  }
4351
- /** Whether two keys are equal; compared as text, as a control holds a boolean key as text. */
4352
- function isSameKey(key1, key2) {
4353
- return String(key1) === String(key2);
4374
+ /**
4375
+ * The key as the keys of a set compare: as text, a uri text (RFC 3986) without the version suffix
4376
+ * at the end of its path unless the set has Versioned Keys. Any other text stays as it is, a code
4377
+ * ending in a version too.
4378
+ */
4379
+ function createReferenceKey(key, versionedKeys) {
4380
+ const text = String(key);
4381
+ return versionedKeys || !URI_PATTERN.test(text) ? text : removeVersion(text);
4382
+ }
4383
+ /**
4384
+ * The uri text without the version suffix at the end of its path, as removeVersion of UriUtils on
4385
+ * the server; the scheme, the authority, the query and the fragment stay as they are.
4386
+ */
4387
+ function removeVersion(uri) {
4388
+ const schemeEnd = uri.indexOf(':');
4389
+ const queryOrFragment = uri.slice(schemeEnd + 1).search(/[?#]/);
4390
+ const pathEnd = queryOrFragment < 0 ? uri.length : schemeEnd + 1 + queryOrFragment;
4391
+ let pathStart = schemeEnd + 1;
4392
+ if (uri.startsWith('//', pathStart)) {
4393
+ const slash = uri.indexOf('/', pathStart + 2);
4394
+ pathStart = slash < 0 || slash > pathEnd ? pathEnd : slash;
4395
+ }
4396
+ const version = VERSION_SUFFIX.exec(uri.slice(pathStart, pathEnd));
4397
+ return version ? uri.slice(0, pathStart + version.index) + uri.slice(pathEnd) : uri;
4398
+ }
4399
+ /**
4400
+ * Whether two keys of a set are equal as {@link createReferenceKey} gives them; as text, as a
4401
+ * control holds a boolean key as text.
4402
+ */
4403
+ function isSameKey(key1, key2, versionedKeys) {
4404
+ return createReferenceKey(key1, versionedKeys) === createReferenceKey(key2, versionedKeys);
4354
4405
  }
4355
4406
 
4356
4407
  /**
@@ -4456,7 +4507,7 @@ class SmartValueSetMemoryStore extends SmartValueSetStore {
4456
4507
  }
4457
4508
  const held = this.viewEntries.get(viewUuid)?.get(key);
4458
4509
  const extras = {
4459
- extras: addReceivedExtras(held?.extras, valueSet.extraValues),
4510
+ extras: addReceivedExtras(held?.extras, valueSet.extraValues, this.findVersionedKeys(viewUuid, key, valueSet, held)),
4460
4511
  excludedKeys: valueSet.excludedKeys,
4461
4512
  };
4462
4513
  // A version alone keeps the values held on screen until the fetch brings the new ones.
@@ -4879,6 +4930,18 @@ class SmartValueSetMemoryStore extends SmartValueSetStore {
4879
4930
  });
4880
4931
  changedNames.forEach((name) => this.changeSource.next(name));
4881
4932
  }
4933
+ /**
4934
+ * The Versioned Keys of the view's set of the key: of the values it receives or holds, else of
4935
+ * the explicit set its selection reads; undefined while the client has none of them.
4936
+ */
4937
+ findVersionedKeys(viewUuid, key, valueSet, held) {
4938
+ const selection = this.viewSelections.get(viewUuid)?.get(key);
4939
+ const data = valueSet.valueSetData ??
4940
+ held?.data ??
4941
+ (selection &&
4942
+ this.findExplicitEntry(viewUuid, selection.valueSetNamespace, key)?.data);
4943
+ return data ? !!data.versionedKeys : undefined;
4944
+ }
4882
4945
  mergeViewExtras(data, viewEntry, selection) {
4883
4946
  return viewEntry?.extras?.length || viewEntry?.excludedKeys?.length
4884
4947
  ? mergeExtraValues(data, viewEntry, selection)
@@ -4911,31 +4974,46 @@ class SmartValueSetMemoryStore extends SmartValueSetStore {
4911
4974
  this.changeSource.next(key);
4912
4975
  }
4913
4976
  }
4914
- /** The held extras followed by the received ones of a code not held yet. */
4915
- function addReceivedExtras(held, received) {
4916
- const heldCodes = new Set((held ?? []).map((extra) => extra.value.code));
4977
+ /**
4978
+ * The held extras followed by the received ones of a code not held yet, compared as
4979
+ * {@link createReferenceKey} gives it. While the Versioned Keys of the set are unknown the codes
4980
+ * compare exactly, the merge with the set drops the second version of an object then.
4981
+ */
4982
+ function addReceivedExtras(held, received, versionedKeys) {
4983
+ const keepsVersion = versionedKeys !== false;
4984
+ const heldCodes = new Set((held ?? []).map((extra) => createReferenceKey(extra.value.code, keepsVersion)));
4917
4985
  const added = (received ?? [])
4918
- .filter((value) => !heldCodes.has(value.code))
4986
+ .filter((value) => !heldCodes.has(createReferenceKey(value.code, keepsVersion)))
4919
4987
  .map((value) => ({ value }));
4920
4988
  return [...(held ?? []), ...added];
4921
4989
  }
4922
4990
  /**
4923
- * The set without the values whose text at the set's key property is an excluded key, then the
4924
- * view's extras keyed by their code under the selection's key property, none the set has.
4991
+ * The set without the values whose key at the set's key property is an excluded key, then the
4992
+ * view's extras keyed by their code under the selection's key property, none the set or an extra
4993
+ * before has; the keys compare as {@link createReferenceKey} gives them.
4925
4994
  */
4926
4995
  function mergeExtraValues(data, extras, selection) {
4927
4996
  const values = data.values ?? [];
4928
4997
  const keyProperty = valueSetKeyProperty(values, selection.keyProperty ?? data.keyProperty);
4929
4998
  const displayProperty = valueSetDisplayProperty(values, selection.displayProperty);
4930
- const excludedKeys = new Set(extras.excludedKeys ?? []);
4999
+ const versionedKeys = data.versionedKeys;
5000
+ const excludedKeys = new Set((extras.excludedKeys ?? []).map((key) => createReferenceKey(key, versionedKeys)));
4931
5001
  const keptValues = values.filter((value) => {
4932
- const key = data.keyProperty ? value?.[data.keyProperty] : undefined;
4933
- return key === undefined || key === null || !excludedKeys.has(String(key));
5002
+ const key = data.keyProperty &&
5003
+ findReferenceKey(readValueSetProperty(value, data.keyProperty), versionedKeys);
5004
+ return key === undefined || !excludedKeys.has(key);
4934
5005
  });
4935
- const keys = new Set(keptValues.map((value) => convertKeyToText(value?.[keyProperty])));
5006
+ const keys = new Set(keptValues.map((value) => findReferenceKey(readValueSetProperty(value, keyProperty), versionedKeys)));
4936
5007
  const extraValues = (extras.extras ?? [])
4937
5008
  .map((extra) => extra.value)
4938
- .filter((extra) => extra.code !== undefined && !keys.has(extra.code))
5009
+ .filter((extra) => {
5010
+ const key = findReferenceKey(extra.code, versionedKeys);
5011
+ if (key === undefined || keys.has(key)) {
5012
+ return false;
5013
+ }
5014
+ keys.add(key);
5015
+ return true;
5016
+ })
4939
5017
  .map((extra) => {
4940
5018
  const merged = {
4941
5019
  ...extra,
@@ -4953,8 +5031,9 @@ const mergedExtras = new WeakSet();
4953
5031
  function isMergedExtra(value) {
4954
5032
  return typeof value === "object" && value !== null && mergedExtras.has(value);
4955
5033
  }
4956
- function convertKeyToText(key) {
4957
- return key === undefined || key === null ? undefined : String(key);
5034
+ /** The key as {@link createReferenceKey} gives it, undefined for no key. */
5035
+ function findReferenceKey(key, versionedKeys) {
5036
+ return key === undefined || key === null ? undefined : createReferenceKey(key, versionedKeys);
4958
5037
  }
4959
5038
  function createExplicitKey(module, namespace, name, branchUri) {
4960
5039
  return JSON.stringify([module ?? "", namespace ?? "", name, branchUri ?? ""]);
@@ -8045,6 +8124,13 @@ class SmartFormService {
8045
8124
  * mutating. See ADR-0007, decision 4.
8046
8125
  */
8047
8126
  this.revision = this.revisionState.asReadonly();
8127
+ this.controlRevisionState = signal(0, /* @ts-ignore */
8128
+ ...(ngDevMode ? [{ debugName: "controlRevisionState" }] : /* istanbul ignore next */ []));
8129
+ /**
8130
+ * Counts the value changes of the form's controls, which reach the data only at submit. A
8131
+ * widget showing the model as edited (the html widget) reads it to follow the user's edits.
8132
+ */
8133
+ this.controlRevision = this.controlRevisionState.asReadonly();
8048
8134
  this.emptyKey = 'e#m%p@t*y';
8049
8135
  this.group = {};
8050
8136
  /** The previous build's controls during a rebuild: a widget still shown keeps its control. */
@@ -8080,6 +8166,10 @@ class SmartFormService {
8080
8166
  widgetsChanged() {
8081
8167
  this.revisionState.update((revision) => revision + 1);
8082
8168
  }
8169
+ /** Whether the set the selection widget lists has Versioned Keys. */
8170
+ hasVersionedKeys(widget) {
8171
+ return !!this.filledValueSets.get(widget)?.versionedKeys;
8172
+ }
8083
8173
  /** Forgets the agreed values; the form is being rebuilt from the model. */
8084
8174
  resetExchangedValues() {
8085
8175
  this.exchanged.clear();
@@ -8318,8 +8408,9 @@ class SmartFormService {
8318
8408
  }
8319
8409
  /**
8320
8410
  * The values of the set the widget's parent fields admit: each dependency compares the parent
8321
- * control's value with the value object's property. A parent with no control in the form admits
8322
- * every value. An extra of the view is listed only while the parents hold the keys of its tag.
8411
+ * control's value with the value object's property, by the Versioned Keys of the widget's set. A
8412
+ * parent with no control in the form admits every value. An extra of the view is listed only
8413
+ * while the parents hold the keys of its tag.
8323
8414
  */
8324
8415
  narrowValueSetData(widget, valueSetData, form) {
8325
8416
  const dependencies = this.getDependencies(widget);
@@ -8327,16 +8418,18 @@ class SmartFormService {
8327
8418
  return valueSetData;
8328
8419
  }
8329
8420
  const keyProperty = this.readKeyProperty(widget, valueSetData);
8421
+ const versionedKeys = valueSetData.versionedKeys;
8330
8422
  const extras = this.viewExtras.get(widget) ?? [];
8331
8423
  const parentKeys = this.readDependencyKeys(widget, form);
8332
8424
  const values = valueSetData.values.filter((value) => {
8425
+ const key = readValueSetProperty(value, keyProperty);
8333
8426
  const sameKeyExtras = isMergedExtra(value)
8334
- ? extras.filter((extra) => isSameKey(extra.value.code, value[keyProperty]))
8427
+ ? extras.filter((extra) => isSameKey(extra.value.code, key, versionedKeys))
8335
8428
  : [];
8336
8429
  if (sameKeyExtras.length) {
8337
- return sameKeyExtras.some((extra) => !!extra.tag && this.isTaggedWith(extra.tag, parentKeys));
8430
+ return sameKeyExtras.some((extra) => !!extra.tag && this.isTaggedWith(extra.tag, parentKeys, versionedKeys));
8338
8431
  }
8339
- return dependencies.every((dependency) => this.isAdmittedBy(dependency, value, form));
8432
+ return dependencies.every((dependency) => this.isAdmittedBy(dependency, value, form, versionedKeys));
8340
8433
  });
8341
8434
  return { ...valueSetData, values };
8342
8435
  }
@@ -8349,17 +8442,17 @@ class SmartFormService {
8349
8442
  : this.readParentKeys(dependency.modelPath, control.value);
8350
8443
  });
8351
8444
  }
8352
- isTaggedWith(tag, parentKeys) {
8445
+ isTaggedWith(tag, parentKeys, versionedKeys) {
8353
8446
  return (tag.length === parentKeys.length &&
8354
8447
  tag.every((keys, index) => keys.length === parentKeys[index].length &&
8355
- keys.every((key, keyIndex) => isSameKey(key, parentKeys[index][keyIndex]))));
8448
+ keys.every((key, keyIndex) => isSameKey(key, parentKeys[index][keyIndex], versionedKeys))));
8356
8449
  }
8357
- isAdmittedBy(dependency, value, form) {
8450
+ isAdmittedBy(dependency, value, form, versionedKeys) {
8358
8451
  const control = form.controls[dependency.modelPath];
8359
8452
  if (!control || isEmptyValue(control.value)) {
8360
8453
  return !this.isBlockedByEmptyParent(dependency, form);
8361
8454
  }
8362
- return this.readParentKeys(dependency.modelPath, control.value).some((key) => isSameKey(key, value[dependency.property]));
8455
+ return this.readParentKeys(dependency.modelPath, control.value).some((key) => isSameKey(key, value[dependency.property], versionedKeys));
8363
8456
  }
8364
8457
  /**
8365
8458
  * The keys a parent field holds: an object value, the whole entry of an OBJECT selection, gives
@@ -8371,7 +8464,7 @@ class SmartFormService {
8371
8464
  return value;
8372
8465
  }
8373
8466
  const parent = this.findWidgetByKey(modelPath);
8374
- return value[valueSetKeyProperty([value], parent?.selection?.keyProperty)];
8467
+ return readValueSetProperty(value, valueSetKeyProperty([value], parent?.selection?.keyProperty));
8375
8468
  });
8376
8469
  }
8377
8470
  /**
@@ -8403,7 +8496,7 @@ class SmartFormService {
8403
8496
  }
8404
8497
  const keyProperty = this.readKeyProperty(widget, valueSetData);
8405
8498
  const readKey = this.createKeyReader(widget, valueSetData);
8406
- const isAdmitted = (item) => valueSetData.values.some((admitted) => isSameKey(admitted[keyProperty], readKey(item)));
8499
+ const isAdmitted = (item) => valueSetData.values.some((admitted) => isSameKey(readValueSetProperty(admitted, keyProperty), readKey(item), valueSetData.versionedKeys));
8407
8500
  if (Array.isArray(value)) {
8408
8501
  return value.filter(isAdmitted);
8409
8502
  }
@@ -8413,7 +8506,7 @@ class SmartFormService {
8413
8506
  createKeyReader(widget, valueSetData) {
8414
8507
  const keyProperty = this.readKeyProperty(widget, valueSetData);
8415
8508
  return (item) => widget.selection.type === SelectionDefinitionTypeEnum.OBJECT
8416
- ? item?.[keyProperty]
8509
+ ? readValueSetProperty(item, keyProperty)
8417
8510
  : item;
8418
8511
  }
8419
8512
  readKeyProperty(widget, valueSetData) {
@@ -8445,11 +8538,12 @@ class SmartFormService {
8445
8538
  !selection.mapping?.length) {
8446
8539
  let keyProperty = valueSetKeyProperty(values, selection.keyProperty ?? valueSetData?.keyProperty);
8447
8540
  select.valueList = values.map((value) => {
8541
+ const key = readValueSetProperty(value, keyProperty);
8448
8542
  return {
8449
- key: value[keyProperty],
8450
- label: value[displayProperty],
8543
+ key,
8544
+ label: readValueSetProperty(value, displayProperty),
8451
8545
  type: SmartFormWidgetType.ITEM,
8452
- value: value[keyProperty],
8546
+ value: key,
8453
8547
  };
8454
8548
  });
8455
8549
  if (!isRequired(select)) {
@@ -8461,7 +8555,7 @@ class SmartFormService {
8461
8555
  select.valueList = values.map((value, index) => {
8462
8556
  return {
8463
8557
  key: `${index}`,
8464
- label: value[displayProperty],
8558
+ label: readValueSetProperty(value, displayProperty),
8465
8559
  type: SmartFormWidgetType.ITEM,
8466
8560
  value: value,
8467
8561
  };
@@ -8493,6 +8587,8 @@ class SmartFormService {
8493
8587
  }
8494
8588
  else {
8495
8589
  this.formGroup = new UntypedFormGroup(this.group);
8590
+ // The group lives as long as this service: a rebuild reuses it, so one subscription holds.
8591
+ this.formGroup.valueChanges.subscribe(() => this.controlRevisionState.update((revision) => revision + 1));
8496
8592
  }
8497
8593
  if (this.smartForm.validators && this.smartForm.validators.length) {
8498
8594
  this.formGroup.setValidators(this.smartForm.validators.map((validator) => {
@@ -8638,24 +8734,40 @@ class SmartFormService {
8638
8734
  this.copyValueFromFormToWidgetBeans(this.smartForm.widgets, group);
8639
8735
  const data = this.smartForm.componentModel?.data;
8640
8736
  if (data) {
8641
- for (const key of this.getPropertyNamesDeeply(data)) {
8642
- if (!group.controls[key]) {
8643
- continue;
8644
- }
8645
- const agreed = this.exchanged.has(key)
8646
- ? this.exchanged.get(key)
8647
- : this.getValueDeeply(data, key);
8648
- const value = coerceToTypeOf(agreed, group.controls[key].value);
8649
- if (this.exchanged.has(key) && deepEqual(value, agreed)) {
8650
- continue;
8651
- }
8652
- this.setValueDeeply(data, key, value);
8653
- this.exchanged.set(key, structuredClone(this.getValueDeeply(data, key)));
8654
- }
8737
+ this.copyControlsIntoData(data, group, this.exchanged);
8655
8738
  }
8656
8739
  }
8657
8740
  return this.smartForm;
8658
8741
  }
8742
+ /**
8743
+ * The form's data with the controls' edits applied as the next submit would copy them (a
8744
+ * MATRIX widget excepted: its submit changes the data through the widget beans). The result is
8745
+ * read-only: it is a copy, but the live data itself when the form has no controls; undefined
8746
+ * without a data.
8747
+ */
8748
+ buildModelWithEdits() {
8749
+ const data = this.smartForm?.componentModel?.data;
8750
+ if (!data || !this.formGroup) {
8751
+ return data;
8752
+ }
8753
+ const model = structuredClone(data);
8754
+ this.copyControlsIntoData(model, this.formGroup, new Map(this.exchanged));
8755
+ return model;
8756
+ }
8757
+ copyControlsIntoData(data, group, exchanged) {
8758
+ for (const key of this.getPropertyNamesDeeply(data)) {
8759
+ if (!group.controls[key]) {
8760
+ continue;
8761
+ }
8762
+ const agreed = exchanged.has(key) ? exchanged.get(key) : this.getValueDeeply(data, key);
8763
+ const value = coerceToTypeOf(agreed, group.controls[key].value);
8764
+ if (exchanged.has(key) && deepEqual(value, agreed)) {
8765
+ continue;
8766
+ }
8767
+ this.setValueDeeply(data, key, value);
8768
+ exchanged.set(key, structuredClone(this.getValueDeeply(data, key)));
8769
+ }
8770
+ }
8659
8771
  getModel(group) {
8660
8772
  if (!this.smartForm?.componentModel) {
8661
8773
  return {};
@@ -10644,9 +10756,10 @@ function injectSmartHtmlRenderer(container) {
10644
10756
 
10645
10757
  /**
10646
10758
  * The HTML widget: server-sent HTML whose client placeholders are resolved against the model of
10647
- * the form it sits on, the widget's `root` applied. It has no control, no label and no value of
10648
- * its own; it follows the form's revision and the value sets it reads and re-resolves, which
10649
- * rewrites the html only when the result differs. See HTML-WIDGET.md.
10759
+ * the form it sits on, its controls' unsubmitted edits included, the widget's `root` applied. It
10760
+ * has no control, no label and no value of its own; it follows the form's revision, every edit of
10761
+ * a control and the value sets it reads and re-resolves, which rewrites the html only when the
10762
+ * result differs. See HTML-WIDGET.md.
10650
10763
  */
10651
10764
  class SmartHtmlWidgetComponent {
10652
10765
  constructor() {
@@ -10659,13 +10772,14 @@ class SmartHtmlWidgetComponent {
10659
10772
  ...(ngDevMode ? [{ debugName: "widgetInstance" }] : /* istanbul ignore next */ []));
10660
10773
  effect(() => {
10661
10774
  this.service.revision();
10775
+ this.service.controlRevision();
10662
10776
  this.valueSetReads.changed();
10663
10777
  const properties = this.widgetInstance().htmlProperties;
10664
10778
  const model = this.service.smartForm?.componentModel;
10665
10779
  this.warnUnloaded(properties);
10666
10780
  this.renderer.useCss(properties?.css);
10667
10781
  this.renderer.render(properties?.html ?? '', {
10668
- data: this.scope(model?.data, properties?.root, properties?.html),
10782
+ data: this.scope(this.service.buildModelWithEdits(), properties?.root, properties?.html),
10669
10783
  findValueSetData: this.valueSetReads.track(createValueSetLookup(model?.valueSets, this.valueSetStore, this.smartComponent?.uuid)),
10670
10784
  icons: properties?.icons,
10671
10785
  });
@@ -11230,6 +11344,16 @@ class SmartformwidgetComponent {
11230
11344
  this.addOnBlur = true;
11231
11345
  this.separatorKeysCodes = [ENTER, COMMA];
11232
11346
  this.emptySeparatorKeysCodes = [];
11347
+ /**
11348
+ * The mat-select comparison of the widget's keys, by the Versioned Keys of its set. An arrow
11349
+ * property, as the template hands it to mat-select unbound.
11350
+ */
11351
+ this.compareItems = (o1, o2) => {
11352
+ const versionedKeys = this.service.hasVersionedKeys(this.widgetInstance);
11353
+ const v1 = o1 && typeof o1 === 'string' ? createReferenceKey(o1, versionedKeys) : o1;
11354
+ const v2 = o2 && typeof o2 === 'string' ? createReferenceKey(o2, versionedKeys) : o2;
11355
+ return deepEqual(v1, v2);
11356
+ };
11233
11357
  // The form changes the widget objects it has already handed to this template — values,
11234
11358
  // constraints, the touched state of the controls — and a mutation of a plain object is
11235
11359
  // invisible to change detection. Registering here rather than being found by a view query
@@ -12076,19 +12200,6 @@ class SmartformwidgetComponent {
12076
12200
  }
12077
12201
  this.setDateFormControl(control, yearMonthValue, '00:00');
12078
12202
  }
12079
- compareItems(o1, o2) {
12080
- function convert(obj) {
12081
- if (obj && typeof obj === 'string') {
12082
- return obj.replace(/\.v[0-9]+$/, '');
12083
- }
12084
- return obj;
12085
- }
12086
- // calling this.truncateUri didn't work
12087
- // TODO: use widget.selection.parameter for ignoreUriVersion / strictUriComparison;
12088
- let v1 = convert(o1);
12089
- let v2 = convert(o2);
12090
- return deepEqual(v1, v2);
12091
- }
12092
12203
  truncateUri(uri) {
12093
12204
  if (uri && typeof uri === 'string') {
12094
12205
  return uri.replace(/\.v[0-9]+$/, '');