@jinntec/fore 3.1.1 → 3.2.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/fore-dev.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Version: 3.1.1 - May 20, 2026 17:16:48 */
1
+ /* Version: 3.2.0 - June 9, 2026 10:50:39 */
2
2
  function t$2(t, s, r, i) {
3
3
  const n = {
4
4
  op: s,
@@ -20124,7 +20124,7 @@ class Fore {
20124
20124
  return Fore.ACTION_ELEMENTS.includes(elementName);
20125
20125
  }
20126
20126
  static get UI_ELEMENTS() {
20127
- return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20127
+ return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-INCLUDE', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20128
20128
  }
20129
20129
 
20130
20130
  /**
@@ -21574,10 +21574,8 @@ class FxModel extends HTMLElement {
21574
21574
  const instance = model.getInstance(instanceId);
21575
21575
  const fore = model.formElement;
21576
21576
  if (fore?.createNodes && (node === null || node === undefined)) {
21577
- const mi = new ModelItem(undefined, ref, null, null, instanceId, fore);
21578
- mi.isSynthetic = true;
21579
- model.registerModelItem(mi);
21580
- return mi;
21577
+ // Do not create the model item. It may be exchanged later when create-nodes actually made the node
21578
+ return null;
21581
21579
  }
21582
21580
  if (node === null || node === undefined) return null;
21583
21581
  let targetNode = Array.isArray(node) ? node[0] : node;
@@ -25481,7 +25479,7 @@ class FxFore extends HTMLElement {
25481
25479
  this._createRepeatsFromAttributes();
25482
25480
  this.inited = true;
25483
25481
  };
25484
- this.version = 'Version: 3.1.1 - built on May 20, 2026 17:16:48';
25482
+ this.version = 'Version: 3.2.0 - built on June 9, 2026 10:50:39';
25485
25483
 
25486
25484
  /**
25487
25485
  * @type {import('./fx-model.js').FxModel}
@@ -26931,7 +26929,7 @@ class FxFore extends HTMLElement {
26931
26929
  bound.evalInContext();
26932
26930
  if (bound.nodeName !== 'FX-REPEAT') {
26933
26931
  // Do not try to get a bind for a nodeSET of a repeat. there are multiple.
26934
- bound.getModelItem().bind?.evalInContext();
26932
+ bound.getModelItem()?.bind?.evalInContext();
26935
26933
  }
26936
26934
  continue;
26937
26935
  }
@@ -26980,7 +26978,7 @@ class FxFore extends HTMLElement {
26980
26978
  }
26981
26979
  }
26982
26980
  bound.evalInContext();
26983
- bound.getModelItem().bind?.evalInContext();
26981
+ bound.getModelItem()?.bind?.evalInContext();
26984
26982
  if (!isResolvedBound(bound)) {
26985
26983
  console.warn('create-nodes: could not resolve bound after node creation, skipping', bound);
26986
26984
  continue;
@@ -27114,6 +27112,11 @@ class FxFore extends HTMLElement {
27114
27112
  continue;
27115
27113
  }
27116
27114
  const parsed = parseName(token);
27115
+ if (!isValidName(parsed.localName)) {
27116
+ // This did not result in a valid name. Stop.
27117
+ console.warn(`Creating node for the XPath ${xpath} failed because the part ${parsed.localName} is not a valid Name.`);
27118
+ return;
27119
+ }
27117
27120
  if (parsed.isAttribute) {
27118
27121
  if (!current) {
27119
27122
  const attr = ownerDoc.createAttribute(parsed.localName);
@@ -27122,11 +27125,6 @@ class FxFore extends HTMLElement {
27122
27125
  current.setAttribute(parsed.localName, '');
27123
27126
  continue;
27124
27127
  }
27125
- if (!isValidName(parsed.localName)) {
27126
- // This did not result in a valid name. Stop.
27127
- console.warn(`Creating node for the XPath ${xpath} failed because the part ${parsed.localName} is not a valid Name.`);
27128
- return;
27129
- }
27130
27128
  const element = parsed.namespaceURI ? ownerDoc.createElementNS(parsed.namespaceURI, parsed.localName) : ownerDoc.createElement(parsed.localName);
27131
27129
  for (const predicate of predicates) {
27132
27130
  const attrName = predicate.name.includes(':') ? predicate.name.split(':')[1] : predicate.name;
@@ -29637,6 +29635,211 @@ if (!customElements.get('fx-hint')) {
29637
29635
  customElements.define('fx-hint', FxHint);
29638
29636
  }
29639
29637
 
29638
+ /**
29639
+ * <fx-include>
29640
+ *
29641
+ * Lazy light-DOM include component.
29642
+ *
29643
+ * Loads markup either from:
29644
+ * - a direct child <template>
29645
+ * - an external HTML document via @src
29646
+ *
29647
+ * Default behavior:
29648
+ * - listens for an event
29649
+ * - includes content once
29650
+ * - removes the event listener afterwards
29651
+ *
29652
+ * With @reload:
29653
+ * - listens repeatedly
29654
+ * - clears previously included content
29655
+ * - includes fresh content again
29656
+ *
29657
+ * Attributes:
29658
+ * event Event name to listen for. Defaults to "click".
29659
+ * target CSS selector for the event target. Defaults to "self".
29660
+ * Special values: self, document, window.
29661
+ * src Optional external HTML source.
29662
+ * selector Optional selector inside external HTML.
29663
+ * replace Replace <fx-include> with the included content.
29664
+ * immediate Include immediately when connected.
29665
+ * reload Re-include on every matching event.
29666
+ */
29667
+ class FxInclude extends HTMLElement {
29668
+ constructor() {
29669
+ super();
29670
+ this._listener = this._handleEvent.bind(this);
29671
+ this._eventTarget = null;
29672
+ this._loaded = false;
29673
+ this._replaced = false;
29674
+ }
29675
+ connectedCallback() {
29676
+ this.eventName = this.getAttribute('event') || 'click';
29677
+ this.targetSelector = this.getAttribute('target') || 'self';
29678
+ this.src = this.getAttribute('src');
29679
+ this.selector = this.getAttribute('selector');
29680
+ this.replace = this.hasAttribute('replace');
29681
+ this.immediate = this.hasAttribute('immediate');
29682
+ this.reload = this.hasAttribute('reload');
29683
+ if (this.immediate) {
29684
+ queueMicrotask(() => this.include());
29685
+ return;
29686
+ }
29687
+ this._bindListener();
29688
+ }
29689
+ disconnectedCallback() {
29690
+ this._unbindListener();
29691
+ }
29692
+ _bindListener() {
29693
+ const target = this._resolveEventTarget();
29694
+ if (!target) {
29695
+ this._dispatchError(`fx-include: event target not found: '${this.targetSelector}'`);
29696
+ return;
29697
+ }
29698
+ target.addEventListener(this.eventName, this._listener);
29699
+ this._eventTarget = target;
29700
+ }
29701
+ _unbindListener() {
29702
+ if (this._eventTarget && this._listener) {
29703
+ this._eventTarget.removeEventListener(this.eventName, this._listener);
29704
+ }
29705
+ this._eventTarget = null;
29706
+ }
29707
+ _resolveEventTarget() {
29708
+ if (!this.targetSelector || this.targetSelector === 'self') {
29709
+ return this;
29710
+ }
29711
+ if (this.targetSelector === 'document') {
29712
+ return document;
29713
+ }
29714
+ if (this.targetSelector === 'window') {
29715
+ return window;
29716
+ }
29717
+ const fore = this.closest('fx-fore');
29718
+ return fore?.querySelector(this.targetSelector) || document.querySelector(this.targetSelector);
29719
+ }
29720
+ async _handleEvent(event) {
29721
+ await this.include(event);
29722
+ }
29723
+ async include(triggerEvent = null) {
29724
+ if (this._replaced) {
29725
+ return;
29726
+ }
29727
+ if (this._loaded && !this.reload) {
29728
+ return;
29729
+ }
29730
+ const fragment = this.src ? await this._loadExternalFragment() : this._loadTemplateFragment();
29731
+ if (!fragment) {
29732
+ return;
29733
+ }
29734
+ if (this.replace) {
29735
+ await this._replaceSelf(fragment, triggerEvent);
29736
+ return;
29737
+ }
29738
+ this._clearIncludedContent();
29739
+ const wrapper = document.createElement('span');
29740
+ wrapper.setAttribute('data-fx-include-content', '');
29741
+ wrapper.style.display = 'contents';
29742
+ wrapper.appendChild(fragment);
29743
+ this.appendChild(wrapper);
29744
+ this._loaded = true;
29745
+ await this._initializeInsertedContent(wrapper);
29746
+ await Fore.dispatch(this, 'include-done', {
29747
+ src: this.src || null,
29748
+ included: wrapper,
29749
+ replaced: false,
29750
+ triggerEvent
29751
+ });
29752
+ if (!this.reload) {
29753
+ this._unbindListener();
29754
+ }
29755
+ }
29756
+ _loadTemplateFragment() {
29757
+ const template = this.querySelector(':scope > template');
29758
+ if (!template) {
29759
+ this._dispatchError('fx-include: no src and no direct template child found');
29760
+ return null;
29761
+ }
29762
+ return template.content.cloneNode(true);
29763
+ }
29764
+ async _loadExternalFragment() {
29765
+ const html = await Fore.loadHtml(this.src);
29766
+ if (!html) {
29767
+ this._dispatchError(`fx-include: failed to load '${this.src}'`);
29768
+ return null;
29769
+ }
29770
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
29771
+ if (this.selector) {
29772
+ const selected = parsed.querySelector(this.selector);
29773
+ if (!selected) {
29774
+ this._dispatchError(`fx-include: selector '${this.selector}' not found in '${this.src}'`);
29775
+ return null;
29776
+ }
29777
+ const fragment = document.createDocumentFragment();
29778
+ fragment.appendChild(document.importNode(selected, true));
29779
+ return fragment;
29780
+ }
29781
+ const template = parsed.querySelector('template');
29782
+ if (template) {
29783
+ return document.importNode(template.content, true);
29784
+ }
29785
+ const fragment = document.createDocumentFragment();
29786
+ Array.from(parsed.body.childNodes).forEach(node => {
29787
+ fragment.appendChild(document.importNode(node, true));
29788
+ });
29789
+ return fragment;
29790
+ }
29791
+ async _replaceSelf(fragment, triggerEvent) {
29792
+ const parent = this.parentNode;
29793
+ if (!parent) {
29794
+ return;
29795
+ }
29796
+ const wrapper = document.createElement('span');
29797
+ wrapper.setAttribute('data-fx-include-replacement', '');
29798
+ wrapper.style.display = 'contents';
29799
+ wrapper.appendChild(fragment);
29800
+ this._unbindListener();
29801
+ this.replaceWith(wrapper);
29802
+ this._replaced = true;
29803
+ this._loaded = true;
29804
+ await this._initializeInsertedContent(wrapper);
29805
+ await Fore.dispatch(wrapper, 'include-done', {
29806
+ src: this.src || null,
29807
+ included: wrapper,
29808
+ replaced: true,
29809
+ triggerEvent
29810
+ });
29811
+ }
29812
+ _clearIncludedContent() {
29813
+ this.querySelectorAll(':scope > [data-fx-include-content]').forEach(node => {
29814
+ node.remove();
29815
+ });
29816
+ }
29817
+ async _initializeInsertedContent(startElement) {
29818
+ this._initForeUiDescendants(startElement);
29819
+ await Fore.refreshChildren(startElement, true);
29820
+ }
29821
+ _initForeUiDescendants(startElement) {
29822
+ Array.from(startElement.children || []).forEach(element => {
29823
+ if (element.nodeName.toUpperCase() === 'FX-FORE') {
29824
+ return;
29825
+ }
29826
+ if (Fore.isUiElement(element.nodeName) && typeof element.init === 'function') {
29827
+ element.init();
29828
+ }
29829
+ this._initForeUiDescendants(element);
29830
+ });
29831
+ }
29832
+ _dispatchError(message) {
29833
+ Fore.dispatch(this, 'error', {
29834
+ level: 'Error',
29835
+ message
29836
+ });
29837
+ }
29838
+ }
29839
+ if (!customElements.get('fx-include')) {
29840
+ customElements.define('fx-include', FxInclude);
29841
+ }
29842
+
29640
29843
  /**
29641
29844
  * todo: review placing of value. should probably work with value attribute and not allow slotted content.
29642
29845
  */
@@ -31155,6 +31358,13 @@ class FxCase extends FxContainer {
31155
31358
  }
31156
31359
  await parentNode.replaceCase(this, replacement);
31157
31360
  target = replacement;
31361
+ // Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
31362
+ // The replacement's handlers are registered after async loading; they miss the
31363
+ // initial 'select' that triggered the load.
31364
+ if (parentNode.selectedCase === replacement) {
31365
+ await Fore.dispatch(replacement, 'select', {});
31366
+ return;
31367
+ }
31158
31368
  }
31159
31369
  ownerForm.getModel();
31160
31370
  ownerForm.addToBatchedNotifications(target);
package/dist/fore.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Version: 3.1.1 - May 20, 2026 17:16:47 */
1
+ /* Version: 3.2.0 - June 9, 2026 10:50:37 */
2
2
  function t$2(t, s, r, i) {
3
3
  const n = {
4
4
  op: s,
@@ -20121,7 +20121,7 @@ class Fore {
20121
20121
  return Fore.ACTION_ELEMENTS.includes(elementName);
20122
20122
  }
20123
20123
  static get UI_ELEMENTS() {
20124
- return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20124
+ return new Set(['FX-ALERT', 'FX-CONTROL', 'FX-DIALOG', 'FX-FILENAME', 'FX-MEDIATYPE', 'FX-GROUP', 'FX-HINT', 'FX-ITEMS', 'FX-INCLUDE', 'FX-OUTPUT', 'FX-RANGE', 'FX-REPEAT', 'FX-REPEATITEM', 'FX-REPEAT-ATTRIBUTES', 'FX-SWITCH', 'FX-SECRET', 'FX-SELECT', 'FX-SUBMIT', 'FX-TEXTAREA', 'FX-TRIGGER', 'FX-UPLOAD', 'FX-VAR']);
20125
20125
  }
20126
20126
 
20127
20127
  /**
@@ -21553,10 +21553,8 @@ class FxModel extends HTMLElement {
21553
21553
  const instance = model.getInstance(instanceId);
21554
21554
  const fore = model.formElement;
21555
21555
  if (fore?.createNodes && (node === null || node === undefined)) {
21556
- const mi = new ModelItem(undefined, ref, null, null, instanceId, fore);
21557
- mi.isSynthetic = true;
21558
- model.registerModelItem(mi);
21559
- return mi;
21556
+ // Do not create the model item. It may be exchanged later when create-nodes actually made the node
21557
+ return null;
21560
21558
  }
21561
21559
  if (node === null || node === undefined) return null;
21562
21560
  let targetNode = Array.isArray(node) ? node[0] : node;
@@ -25427,7 +25425,7 @@ class FxFore extends HTMLElement {
25427
25425
  this._createRepeatsFromAttributes();
25428
25426
  this.inited = true;
25429
25427
  };
25430
- this.version = 'Version: 3.1.1 - built on May 20, 2026 17:16:47';
25428
+ this.version = 'Version: 3.2.0 - built on June 9, 2026 10:50:37';
25431
25429
 
25432
25430
  /**
25433
25431
  * @type {import('./fx-model.js').FxModel}
@@ -26861,7 +26859,7 @@ class FxFore extends HTMLElement {
26861
26859
  bound.evalInContext();
26862
26860
  if (bound.nodeName !== 'FX-REPEAT') {
26863
26861
  // Do not try to get a bind for a nodeSET of a repeat. there are multiple.
26864
- bound.getModelItem().bind?.evalInContext();
26862
+ bound.getModelItem()?.bind?.evalInContext();
26865
26863
  }
26866
26864
  continue;
26867
26865
  }
@@ -26910,7 +26908,7 @@ class FxFore extends HTMLElement {
26910
26908
  }
26911
26909
  }
26912
26910
  bound.evalInContext();
26913
- bound.getModelItem().bind?.evalInContext();
26911
+ bound.getModelItem()?.bind?.evalInContext();
26914
26912
  if (!isResolvedBound(bound)) {
26915
26913
  continue;
26916
26914
  }
@@ -27043,6 +27041,10 @@ class FxFore extends HTMLElement {
27043
27041
  continue;
27044
27042
  }
27045
27043
  const parsed = parseName(token);
27044
+ if (!isValidName(parsed.localName)) {
27045
+ // This did not result in a valid name. Stop.
27046
+ return;
27047
+ }
27046
27048
  if (parsed.isAttribute) {
27047
27049
  if (!current) {
27048
27050
  const attr = ownerDoc.createAttribute(parsed.localName);
@@ -27051,10 +27053,6 @@ class FxFore extends HTMLElement {
27051
27053
  current.setAttribute(parsed.localName, '');
27052
27054
  continue;
27053
27055
  }
27054
- if (!isValidName(parsed.localName)) {
27055
- // This did not result in a valid name. Stop.
27056
- return;
27057
- }
27058
27056
  const element = parsed.namespaceURI ? ownerDoc.createElementNS(parsed.namespaceURI, parsed.localName) : ownerDoc.createElement(parsed.localName);
27059
27057
  for (const predicate of predicates) {
27060
27058
  const attrName = predicate.name.includes(':') ? predicate.name.split(':')[1] : predicate.name;
@@ -31055,6 +31053,13 @@ class FxCase extends FxContainer {
31055
31053
  }
31056
31054
  await parentNode.replaceCase(this, replacement);
31057
31055
  target = replacement;
31056
+ // Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
31057
+ // The replacement's handlers are registered after async loading; they miss the
31058
+ // initial 'select' that triggered the load.
31059
+ if (parentNode.selectedCase === replacement) {
31060
+ await Fore.dispatch(replacement, 'select', {});
31061
+ return;
31062
+ }
31058
31063
  }
31059
31064
  ownerForm.getModel();
31060
31065
  ownerForm.addToBatchedNotifications(target);
package/index.js CHANGED
@@ -14,6 +14,7 @@ import './src/ui/fx-control.js';
14
14
  import './src/ui/fx-container.js';
15
15
  import './src/ui/fx-group.js';
16
16
  import './src/ui/fx-hint.js';
17
+ import './src/ui/fx-include.js';
17
18
  import './src/ui/fx-output.js';
18
19
  import './src/ui/fx-repeat.js';
19
20
  import './src/ui/fx-repeat-attributes.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinntec/fore",
3
- "version": "3.1.1",
3
+ "version": "3.2.0",
4
4
  "description": "Fore - declarative user interfaces in plain HTML",
5
5
  "module": "./index.js",
6
6
  "publishConfig": {
package/src/fore.js CHANGED
@@ -239,6 +239,7 @@ export class Fore {
239
239
  'FX-GROUP',
240
240
  'FX-HINT',
241
241
  'FX-ITEMS',
242
+ 'FX-INCLUDE',
242
243
  'FX-OUTPUT',
243
244
  'FX-RANGE',
244
245
  'FX-REPEAT',
package/src/fx-fore.js CHANGED
@@ -1819,7 +1819,7 @@ export class FxFore extends HTMLElement {
1819
1819
  bound.evalInContext();
1820
1820
  if (bound.nodeName !== 'FX-REPEAT') {
1821
1821
  // Do not try to get a bind for a nodeSET of a repeat. there are multiple.
1822
- bound.getModelItem().bind?.evalInContext();
1822
+ bound.getModelItem()?.bind?.evalInContext();
1823
1823
  }
1824
1824
  continue;
1825
1825
  }
@@ -1878,7 +1878,7 @@ export class FxFore extends HTMLElement {
1878
1878
  }
1879
1879
 
1880
1880
  bound.evalInContext();
1881
- bound.getModelItem().bind?.evalInContext();
1881
+ bound.getModelItem()?.bind?.evalInContext();
1882
1882
 
1883
1883
  if (!isResolvedBound(bound)) {
1884
1884
  console.warn('create-nodes: could not resolve bound after node creation, skipping', bound);
@@ -2016,6 +2016,14 @@ export class FxFore extends HTMLElement {
2016
2016
 
2017
2017
  const parsed = parseName(token);
2018
2018
 
2019
+ if (!isValidName(parsed.localName)) {
2020
+ // This did not result in a valid name. Stop.
2021
+ console.warn(
2022
+ `Creating node for the XPath ${xpath} failed because the part ${parsed.localName} is not a valid Name.`,
2023
+ );
2024
+ return;
2025
+ }
2026
+
2019
2027
  if (parsed.isAttribute) {
2020
2028
  if (!current) {
2021
2029
  const attr = ownerDoc.createAttribute(parsed.localName);
@@ -2025,14 +2033,6 @@ export class FxFore extends HTMLElement {
2025
2033
  continue;
2026
2034
  }
2027
2035
 
2028
- if (!isValidName(parsed.localName)) {
2029
- // This did not result in a valid name. Stop.
2030
- console.warn(
2031
- `Creating node for the XPath ${xpath} failed because the part ${parsed.localName} is not a valid Name.`,
2032
- );
2033
- return;
2034
- }
2035
-
2036
2036
  const element = parsed.namespaceURI
2037
2037
  ? ownerDoc.createElementNS(parsed.namespaceURI, parsed.localName)
2038
2038
  : ownerDoc.createElement(parsed.localName);
package/src/fx-model.js CHANGED
@@ -133,10 +133,8 @@ export class FxModel extends HTMLElement {
133
133
  const fore = model.formElement;
134
134
 
135
135
  if (fore?.createNodes && (node === null || node === undefined)) {
136
- const mi = new ModelItem(undefined, ref, null, null, instanceId, fore);
137
- mi.isSynthetic = true;
138
- model.registerModelItem(mi);
139
- return mi;
136
+ // Do not create the model item. It may be exchanged later when create-nodes actually made the node
137
+ return null;
140
138
  }
141
139
  if (node === null || node === undefined) return null;
142
140
 
@@ -159,8 +157,10 @@ export class FxModel extends HTMLElement {
159
157
  }
160
158
 
161
159
  const isLensObject =
162
- !!targetNode && typeof targetNode === 'object' &&
163
- typeof targetNode.get === 'function' && typeof targetNode.set === 'function';
160
+ !!targetNode &&
161
+ typeof targetNode === 'object' &&
162
+ typeof targetNode.get === 'function' &&
163
+ typeof targetNode.set === 'function';
164
164
 
165
165
  // If ModelItem for same path exists, RETARGET it (node OR lens)
166
166
  if (path) {
@@ -182,12 +182,12 @@ export class FxModel extends HTMLElement {
182
182
  }
183
183
 
184
184
  const mi = new ModelItem(
185
- path,
186
- ref,
187
- targetNode,
188
- model.getBindForElement(targetNode),
189
- instanceId,
190
- fore,
185
+ path,
186
+ ref,
187
+ targetNode,
188
+ model.getBindForElement(targetNode),
189
+ instanceId,
190
+ fore,
191
191
  );
192
192
  mi.isSynthetic = true;
193
193
 
@@ -231,7 +231,8 @@ export class FxModel extends HTMLElement {
231
231
  const defInst = this.getDefaultInstance();
232
232
  if (defInst) {
233
233
  const t = (defInst.getAttribute && defInst.getAttribute('type')) || defInst.type;
234
- bindings.default = t === 'json' ? defInst.getInstanceData() : defInst.getDefaultContext();
234
+ bindings.default =
235
+ t === 'json' ? defInst.getInstanceData() : defInst.getDefaultContext();
235
236
  }
236
237
  } catch (_e) {
237
238
  // ignore
@@ -734,14 +735,14 @@ export class FxModel extends HTMLElement {
734
735
 
735
736
  // Path lookup
736
737
  if (typeof nodeOrPath === 'string') {
737
- const key = nodeOrPath.includes(':') ? nodeOrPath.substring(0, nodeOrPath.indexOf(':')) : nodeOrPath;
738
+ const key = nodeOrPath.includes(':')
739
+ ? nodeOrPath.substring(0, nodeOrPath.indexOf(':'))
740
+ : nodeOrPath;
738
741
  return this.modelItems.find(mi => mi.path === key) || null;
739
742
  }
740
743
 
741
744
  // Node/lens lookup
742
- return (
743
- this.modelItems.find(mi => mi.node === nodeOrPath || mi.lens === nodeOrPath) || null
744
- );
745
+ return this.modelItems.find(mi => mi.node === nodeOrPath || mi.lens === nodeOrPath) || null;
745
746
  }
746
747
 
747
748
  /**
@@ -791,9 +792,9 @@ export class FxModel extends HTMLElement {
791
792
  // ### lookup in parent Fore if present (shared instances)
792
793
  if (!found) {
793
794
  const parentFore =
794
- this.fore.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
795
- ? this.fore.parentNode.host.closest('fx-fore')
796
- : this.fore.parentNode.closest('fx-fore');
795
+ this.fore.parentNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE
796
+ ? this.fore.parentNode.host.closest('fx-fore')
797
+ : this.fore.parentNode.closest('fx-fore');
797
798
 
798
799
  if (parentFore) {
799
800
  const parentInstances = parentFore.getModel().instances;
package/src/ui/fx-case.js CHANGED
@@ -81,6 +81,13 @@ export class FxCase extends FxContainer {
81
81
  }
82
82
  await parentNode.replaceCase(this, replacement);
83
83
  target = replacement;
84
+ // Re-dispatch 'select' on the loaded replacement so its fx-action handlers fire.
85
+ // The replacement's handlers are registered after async loading; they miss the
86
+ // initial 'select' that triggered the load.
87
+ if (parentNode.selectedCase === replacement) {
88
+ await Fore.dispatch(replacement, 'select', {});
89
+ return;
90
+ }
84
91
  }
85
92
  const model = ownerForm.getModel();
86
93
  ownerForm.addToBatchedNotifications(target);
@@ -0,0 +1,264 @@
1
+ import { Fore } from '../fore.js';
2
+
3
+ /**
4
+ * <fx-include>
5
+ *
6
+ * Lazy light-DOM include component.
7
+ *
8
+ * Loads markup either from:
9
+ * - a direct child <template>
10
+ * - an external HTML document via @src
11
+ *
12
+ * Default behavior:
13
+ * - listens for an event
14
+ * - includes content once
15
+ * - removes the event listener afterwards
16
+ *
17
+ * With @reload:
18
+ * - listens repeatedly
19
+ * - clears previously included content
20
+ * - includes fresh content again
21
+ *
22
+ * Attributes:
23
+ * event Event name to listen for. Defaults to "click".
24
+ * target CSS selector for the event target. Defaults to "self".
25
+ * Special values: self, document, window.
26
+ * src Optional external HTML source.
27
+ * selector Optional selector inside external HTML.
28
+ * replace Replace <fx-include> with the included content.
29
+ * immediate Include immediately when connected.
30
+ * reload Re-include on every matching event.
31
+ */
32
+ export class FxInclude extends HTMLElement {
33
+ constructor() {
34
+ super();
35
+
36
+ this._listener = this._handleEvent.bind(this);
37
+ this._eventTarget = null;
38
+ this._loaded = false;
39
+ this._replaced = false;
40
+ }
41
+
42
+ connectedCallback() {
43
+ this.eventName = this.getAttribute('event') || 'click';
44
+ this.targetSelector = this.getAttribute('target') || 'self';
45
+ this.src = this.getAttribute('src');
46
+ this.selector = this.getAttribute('selector');
47
+
48
+ this.replace = this.hasAttribute('replace');
49
+ this.immediate = this.hasAttribute('immediate');
50
+ this.reload = this.hasAttribute('reload');
51
+
52
+ if (this.immediate) {
53
+ queueMicrotask(() => this.include());
54
+ return;
55
+ }
56
+
57
+ this._bindListener();
58
+ }
59
+
60
+ disconnectedCallback() {
61
+ this._unbindListener();
62
+ }
63
+
64
+ _bindListener() {
65
+ const target = this._resolveEventTarget();
66
+
67
+ if (!target) {
68
+ this._dispatchError(`fx-include: event target not found: '${this.targetSelector}'`);
69
+ return;
70
+ }
71
+
72
+ target.addEventListener(this.eventName, this._listener);
73
+ this._eventTarget = target;
74
+ }
75
+
76
+ _unbindListener() {
77
+ if (this._eventTarget && this._listener) {
78
+ this._eventTarget.removeEventListener(this.eventName, this._listener);
79
+ }
80
+
81
+ this._eventTarget = null;
82
+ }
83
+
84
+ _resolveEventTarget() {
85
+ if (!this.targetSelector || this.targetSelector === 'self') {
86
+ return this;
87
+ }
88
+
89
+ if (this.targetSelector === 'document') {
90
+ return document;
91
+ }
92
+
93
+ if (this.targetSelector === 'window') {
94
+ return window;
95
+ }
96
+
97
+ const fore = this.closest('fx-fore');
98
+
99
+ return fore?.querySelector(this.targetSelector) || document.querySelector(this.targetSelector);
100
+ }
101
+
102
+ async _handleEvent(event) {
103
+ await this.include(event);
104
+ }
105
+
106
+ async include(triggerEvent = null) {
107
+ if (this._replaced) {
108
+ return;
109
+ }
110
+
111
+ if (this._loaded && !this.reload) {
112
+ return;
113
+ }
114
+
115
+ const fragment = this.src ? await this._loadExternalFragment() : this._loadTemplateFragment();
116
+
117
+ if (!fragment) {
118
+ return;
119
+ }
120
+
121
+ if (this.replace) {
122
+ await this._replaceSelf(fragment, triggerEvent);
123
+ return;
124
+ }
125
+
126
+ this._clearIncludedContent();
127
+
128
+ const wrapper = document.createElement('span');
129
+ wrapper.setAttribute('data-fx-include-content', '');
130
+ wrapper.style.display = 'contents';
131
+ wrapper.appendChild(fragment);
132
+
133
+ this.appendChild(wrapper);
134
+
135
+ this._loaded = true;
136
+
137
+ await this._initializeInsertedContent(wrapper);
138
+
139
+ await Fore.dispatch(this, 'include-done', {
140
+ src: this.src || null,
141
+ included: wrapper,
142
+ replaced: false,
143
+ triggerEvent,
144
+ });
145
+
146
+ if (!this.reload) {
147
+ this._unbindListener();
148
+ }
149
+ }
150
+
151
+ _loadTemplateFragment() {
152
+ const template = this.querySelector(':scope > template');
153
+
154
+ if (!template) {
155
+ this._dispatchError('fx-include: no src and no direct template child found');
156
+ return null;
157
+ }
158
+
159
+ return template.content.cloneNode(true);
160
+ }
161
+
162
+ async _loadExternalFragment() {
163
+ const html = await Fore.loadHtml(this.src);
164
+
165
+ if (!html) {
166
+ this._dispatchError(`fx-include: failed to load '${this.src}'`);
167
+ return null;
168
+ }
169
+
170
+ const parsed = new DOMParser().parseFromString(html, 'text/html');
171
+
172
+ if (this.selector) {
173
+ const selected = parsed.querySelector(this.selector);
174
+
175
+ if (!selected) {
176
+ this._dispatchError(`fx-include: selector '${this.selector}' not found in '${this.src}'`);
177
+ return null;
178
+ }
179
+
180
+ const fragment = document.createDocumentFragment();
181
+ fragment.appendChild(document.importNode(selected, true));
182
+ return fragment;
183
+ }
184
+
185
+ const template = parsed.querySelector('template');
186
+
187
+ if (template) {
188
+ return document.importNode(template.content, true);
189
+ }
190
+
191
+ const fragment = document.createDocumentFragment();
192
+
193
+ Array.from(parsed.body.childNodes).forEach(node => {
194
+ fragment.appendChild(document.importNode(node, true));
195
+ });
196
+
197
+ return fragment;
198
+ }
199
+
200
+ async _replaceSelf(fragment, triggerEvent) {
201
+ const parent = this.parentNode;
202
+
203
+ if (!parent) {
204
+ return;
205
+ }
206
+
207
+ const wrapper = document.createElement('span');
208
+ wrapper.setAttribute('data-fx-include-replacement', '');
209
+ wrapper.style.display = 'contents';
210
+ wrapper.appendChild(fragment);
211
+
212
+ this._unbindListener();
213
+
214
+ this.replaceWith(wrapper);
215
+
216
+ this._replaced = true;
217
+ this._loaded = true;
218
+
219
+ await this._initializeInsertedContent(wrapper);
220
+
221
+ await Fore.dispatch(wrapper, 'include-done', {
222
+ src: this.src || null,
223
+ included: wrapper,
224
+ replaced: true,
225
+ triggerEvent,
226
+ });
227
+ }
228
+
229
+ _clearIncludedContent() {
230
+ this.querySelectorAll(':scope > [data-fx-include-content]').forEach(node => {
231
+ node.remove();
232
+ });
233
+ }
234
+
235
+ async _initializeInsertedContent(startElement) {
236
+ this._initForeUiDescendants(startElement);
237
+ await Fore.refreshChildren(startElement, true);
238
+ }
239
+
240
+ _initForeUiDescendants(startElement) {
241
+ Array.from(startElement.children || []).forEach(element => {
242
+ if (element.nodeName.toUpperCase() === 'FX-FORE') {
243
+ return;
244
+ }
245
+
246
+ if (Fore.isUiElement(element.nodeName) && typeof element.init === 'function') {
247
+ element.init();
248
+ }
249
+
250
+ this._initForeUiDescendants(element);
251
+ });
252
+ }
253
+
254
+ _dispatchError(message) {
255
+ Fore.dispatch(this, 'error', {
256
+ level: 'Error',
257
+ message,
258
+ });
259
+ }
260
+ }
261
+
262
+ if (!customElements.get('fx-include')) {
263
+ customElements.define('fx-include', FxInclude);
264
+ }