@liedekef/ftable 1.1.24 → 1.1.25

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/ftable.umd.js CHANGED
@@ -42,6 +42,7 @@
42
42
  class FTableOptionsCache {
43
43
  constructor() {
44
44
  this.cache = new Map();
45
+ this.pendingRequests = new Map(); // Track ongoing requests
45
46
  }
46
47
 
47
48
  generateKey(url, params) {
@@ -81,6 +82,37 @@ class FTableOptionsCache {
81
82
  }
82
83
  }
83
84
 
85
+ async getOrCreate(url, params, fetchFn) {
86
+ const key = this.generateKey(url, params);
87
+
88
+ // Return cached result if available
89
+ const cached = this.cache.get(key);
90
+ if (cached) return cached;
91
+
92
+ // Check if same request is already in progress
93
+ if (this.pendingRequests.has(key)) {
94
+ // Wait for the existing request to complete
95
+ return await this.pendingRequests.get(key);
96
+ }
97
+
98
+ // Create new request
99
+ const requestPromise = (async () => {
100
+ try {
101
+ const result = await fetchFn();
102
+ this.cache.set(key, result);
103
+ return result;
104
+ } finally {
105
+ // Clean up pending request tracking
106
+ this.pendingRequests.delete(key);
107
+ }
108
+ })();
109
+
110
+ // Track this request
111
+ this.pendingRequests.set(key, requestPromise);
112
+
113
+ return await requestPromise;
114
+ }
115
+
84
116
  size() {
85
117
  return this.cache.size;
86
118
  }
@@ -146,6 +178,7 @@ class FTableLogger {
146
178
 
147
179
  const levelName = Object.keys(FTableLogger.LOG_LEVELS)
148
180
  .find(key => FTableLogger.LOG_LEVELS[key] === level);
181
+ console.trace();
149
182
  console.log(`fTable ${levelName}: ${message}`);
150
183
  }
151
184
 
@@ -572,7 +605,7 @@ class FTableFormBuilder {
572
605
  }
573
606
 
574
607
  // Determine if we should skip caching for this specific context
575
- const shouldSkipCache = this.shouldSkipCachingForContext(field, context, params);
608
+ const shouldSkipCache = this.shouldForceRefreshForContext(field, context, params);
576
609
  const cacheKey = this.generateOptionsCacheKey(context, params);
577
610
  // Skip cache if configured or forceRefresh requested
578
611
  if (!shouldSkipCache && !params.forceRefresh) {
@@ -587,10 +620,8 @@ class FTableFormBuilder {
587
620
  ...params
588
621
  }, context, shouldSkipCache);
589
622
 
590
- // Only cache if noCache is not enabled
591
- if (!shouldSkipCache) {
592
- this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
593
- }
623
+ // we store the resolved option always
624
+ this.resolvedFieldOptions.get(fieldName)[cacheKey] = resolved;
594
625
  return resolved;
595
626
  } catch (err) {
596
627
  console.error(`Failed to resolve options for ${fieldName} (${context}):`, err);
@@ -598,24 +629,49 @@ class FTableFormBuilder {
598
629
  }
599
630
  }
600
631
 
601
- // Helper method to determine caching behavior
602
- shouldSkipCachingForContext(field, context, params) {
603
- if (!field.noCache) return false;
604
-
605
- if (typeof field.noCache === 'boolean') {
606
- return field.noCache; // true = skip all contexts
632
+ /**
633
+ * Clear resolved options for specific field or all fields
634
+ * @param {string|null} fieldName - Field name to clear, or null for all fields
635
+ * @param {string|null} context - Context to clear ('table', 'create', 'edit'), or null for all contexts
636
+ */
637
+ clearResolvedOptions(fieldName = null, context = null) {
638
+ if (fieldName) {
639
+ // Clear specific field
640
+ if (this.resolvedFieldOptions.has(fieldName)) {
641
+ if (context) {
642
+ // Clear specific context for specific field
643
+ this.resolvedFieldOptions.get(fieldName)[context] = null;
644
+ } else {
645
+ // Clear all contexts for specific field
646
+ this.resolvedFieldOptions.set(fieldName, { table: null, create: null, edit: null });
647
+ }
648
+ }
649
+ } else {
650
+ // Clear all fields
651
+ if (context) {
652
+ // Clear specific context for all fields
653
+ this.resolvedFieldOptions.forEach((value, key) => {
654
+ this.resolvedFieldOptions.get(key)[context] = null;
655
+ });
656
+ } else {
657
+ // Clear everything
658
+ this.resolvedFieldOptions.forEach((value, key) => {
659
+ this.resolvedFieldOptions.set(key, { table: null, create: null, edit: null });
660
+ });
661
+ }
607
662
  }
663
+ }
608
664
 
609
- if (typeof field.noCache === 'function') {
610
- return field.noCache({ context, ...params });
611
- }
665
+ // Helper method to determine caching behavior
666
+ shouldForceRefreshForContext(field, context, params) {
667
+ // Rename to reflect what it actually does now
668
+ if (!field.noCache) return false;
612
669
 
613
- if (typeof field.noCache === 'object') {
614
- // Check if this specific context should skip cache
615
- return field.noCache[context] === true;
616
- }
670
+ if (typeof field.noCache === 'boolean') return field.noCache;
671
+ if (typeof field.noCache === 'function') return field.noCache({ context, ...params });
672
+ if (typeof field.noCache === 'object') return field.noCache[context] === true;
617
673
 
618
- return false; // Default to caching
674
+ return false;
619
675
  }
620
676
 
621
677
  generateOptionsCacheKey(context, params) {
@@ -644,18 +700,20 @@ class FTableFormBuilder {
644
700
  createFieldContainer(fieldName, field, record, formType) {
645
701
  // in this function, field.options already contains the resolved values
646
702
  const container = FTableDOMHelper.create('div', {
647
- className: 'ftable-input-field-container',
703
+ className: (field.type != 'hidden' ? 'ftable-input-field-container' : ''),
648
704
  attributes: {
649
705
  id: `ftable-input-field-container-div-${fieldName}`,
650
706
  }
651
707
  });
652
708
 
653
- // Label
654
- const label = FTableDOMHelper.create('div', {
655
- className: 'ftable-input-label',
656
- text: field.inputTitle || field.title,
657
- parent: container
658
- });
709
+ if (field.type != 'hidden') {
710
+ // Label
711
+ const label = FTableDOMHelper.create('div', {
712
+ className: 'ftable-input-label',
713
+ text: field.inputTitle || field.title,
714
+ parent: container
715
+ });
716
+ }
659
717
 
660
718
  // Input
661
719
  const inputContainer = this.createInput(fieldName, field, record[fieldName], formType);
@@ -668,9 +726,6 @@ class FTableFormBuilder {
668
726
 
669
727
  this.currentFormRecord = record;
670
728
 
671
- // Pre-resolve all options for fields depending on nothing, the others are handled down the road when dependancies are calculated
672
- await this.resolveFormFieldOptions(record, formType);
673
-
674
729
  const form = FTableDOMHelper.create('form', {
675
730
  className: `ftable-dialog-form ftable-${formType}-form`
676
731
  });
@@ -694,7 +749,6 @@ class FTableFormBuilder {
694
749
  fieldWithOptions.options = field.options;
695
750
  }
696
751
 
697
-
698
752
  const fieldContainer = this.createFieldContainer(fieldName, fieldWithOptions, record, formType);
699
753
  form.appendChild(fieldContainer);
700
754
  }
@@ -706,28 +760,6 @@ class FTableFormBuilder {
706
760
  return form;
707
761
  }
708
762
 
709
- async resolveFormFieldOptions(record, formType) {
710
- const promises = Object.entries(this.options.fields).map(async ([fieldName, field]) => {
711
- if (field.dependsOn) {
712
- // Dependent fields will be resolved when dependencies change
713
- return;
714
- }
715
-
716
- if (this.shouldResolveOptions(field.options)) {
717
- try {
718
- await this.getFieldOptions(fieldName, formType, {
719
- record,
720
- source: formType
721
- });
722
- } catch (err) {
723
- console.error(`Failed to resolve form options for ${fieldName}:`, err);
724
- }
725
- }
726
- });
727
-
728
- await Promise.all(promises);
729
- }
730
-
731
763
  shouldResolveOptions(options) {
732
764
  return options &&
733
765
  (typeof options === 'function' || typeof options === 'string') &&
@@ -820,27 +852,31 @@ class FTableFormBuilder {
820
852
  if (typeof url !== 'string') return [];
821
853
 
822
854
  // Only use cache if noCache is NOT set
823
- if (!noCache) {
824
- const cached = this.optionsCache.get(url, {});
825
- if (cached) return cached;
826
- }
827
-
828
- try {
829
- const response = this.options.forcePost
830
- ? await FTableHttpClient.post(url)
831
- : await FTableHttpClient.get(url);
832
- const options = response.Options || response.options || response || [];
833
-
834
- // Only cache if noCache is false
835
- if (!noCache) {
836
- this.optionsCache.set(url, {}, options);
855
+ if (noCache) {
856
+ try {
857
+ const response = this.options.forcePost
858
+ ? await FTableHttpClient.post(url)
859
+ : await FTableHttpClient.get(url);
860
+ return response.Options || response.options || response || [];
861
+ } catch (error) {
862
+ console.error(`Failed to load options from ${url}:`, error);
863
+ return [];
837
864
  }
838
-
839
- return options;
840
- } catch (error) {
841
- console.error(`Failed to load options from ${url}:`, error);
842
- return [];
865
+ } else {
866
+ // Use getOrCreate to prevent duplicate requests
867
+ return await this.optionsCache.getOrCreate(url, {}, async () => {
868
+ try {
869
+ const response = this.options.forcePost
870
+ ? await FTableHttpClient.post(url)
871
+ : await FTableHttpClient.get(url);
872
+ return response.Options || response.options || response || [];
873
+ } catch (error) {
874
+ console.error(`Failed to load options from ${url}:`, error);
875
+ return [];
876
+ }
877
+ });
843
878
  }
879
+
844
880
  }
845
881
 
846
882
  updateFieldCacheSetting(field, context, skipCache) {
@@ -940,6 +976,9 @@ class FTableFormBuilder {
940
976
  if (datalist) datalist.innerHTML = '';
941
977
  }
942
978
 
979
+ // Get current field value BEFORE resolving new options
980
+ const currentValue = input.value || record[fieldName] || '';
981
+
943
982
  // Resolve options with current context
944
983
  const params = {
945
984
  ...baseParams,
@@ -951,9 +990,11 @@ class FTableFormBuilder {
951
990
 
952
991
  // Populate the input
953
992
  if (input.tagName === 'SELECT') {
954
- this.populateSelectOptions(input, newOptions, '');
993
+ this.populateSelectOptions(input, newOptions, currentValue);
955
994
  } else if (input.tagName === 'INPUT' && input.list) {
956
995
  this.populateDatalistOptions(input.list, newOptions);
996
+ // For datalist, set the value directly
997
+ if (currentValue) input.value = currentValue;
957
998
  }
958
999
 
959
1000
  setTimeout(() => {
@@ -1275,7 +1316,6 @@ class FTableFormBuilder {
1275
1316
  const select = FTableDOMHelper.create('select', { attributes });
1276
1317
 
1277
1318
  if (field.options) {
1278
- // the field options are already the resolved ones
1279
1319
  this.populateSelectOptions(select, field.options, value);
1280
1320
  }
1281
1321
 
@@ -1593,6 +1633,11 @@ class FTable extends FTableEventEmitter {
1593
1633
  // Start resolving in background
1594
1634
  this.resolveAsyncFieldOptions().then(() => {
1595
1635
  // re-render dynamic options rows — no server call
1636
+ // this is needed so that once options are resolved, the table shows correct display values
1637
+ // why: load() can actually finish faster than option resolving (and calling refreshDisplayValues
1638
+ // there is then pointless, since the resolving hasn't finished yet),
1639
+ // so we need to do it when the options are actually resolved (here)
1640
+ // We could call await this.resolveAsyncFieldOptions() during load, but that would slow down the loading ...
1596
1641
  setTimeout(() => {
1597
1642
  this.refreshDisplayValues();
1598
1643
  }, 0);
@@ -1853,7 +1898,6 @@ class FTable extends FTableEventEmitter {
1853
1898
  });
1854
1899
 
1855
1900
  await Promise.all(promises);
1856
- // DON'T call refreshDisplayValues() here - let renderTableData do it
1857
1901
  }
1858
1902
 
1859
1903
  async refreshDisplayValues() {
@@ -1869,8 +1913,9 @@ class FTable extends FTableEventEmitter {
1869
1913
  if (!cell) continue;
1870
1914
 
1871
1915
  // Get table-specific options
1872
- const options = await this.formBuilder.getFieldOptions(fieldName, 'table');
1873
- const value = this.getDisplayText(row.recordData, fieldName, options);
1916
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
1917
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
1918
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
1874
1919
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
1875
1920
  }
1876
1921
  }
@@ -2229,7 +2274,7 @@ class FTable extends FTableEventEmitter {
2229
2274
  DisplayText: displayText
2230
2275
  }));
2231
2276
  } else if (field.options) {
2232
- optionsSource = await this.formBuilder.resolveOptions(field, {}, 'search');
2277
+ optionsSource = await this.formBuilder.getFieldOptions(fieldName);
2233
2278
  }
2234
2279
 
2235
2280
  // Add empty option only if first option is not already empty
@@ -3132,7 +3177,9 @@ class FTable extends FTableEventEmitter {
3132
3177
  });
3133
3178
 
3134
3179
  this.refreshRowStyles();
3135
- this.refreshDisplayValues(); // for options that uses functions/url's
3180
+ // the next call might not do anything if option resolving hasn't finished yet
3181
+ // in fact, it might even not be needed, since we call it when option resolving finishes (see init)
3182
+ //this.refreshDisplayValues(); // for options that uses functions/url's
3136
3183
  }
3137
3184
 
3138
3185
  createTableRow(record) {
@@ -3194,7 +3241,9 @@ class FTable extends FTableEventEmitter {
3194
3241
 
3195
3242
  addDataCell(row, record, fieldName) {
3196
3243
  const field = this.options.fields[fieldName];
3197
- const value = this.getDisplayText(record, fieldName);
3244
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3245
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3246
+ const value = this.getDisplayText(record, fieldName, resolvedOptions);
3198
3247
 
3199
3248
  const cell = FTableDOMHelper.create('td', {
3200
3249
  className: `${field.listClass || ''} ${field.listClassEntry || ''}`,
@@ -3673,7 +3722,9 @@ class FTable extends FTableEventEmitter {
3673
3722
  if (!cell) return;
3674
3723
 
3675
3724
  // Get display text
3676
- const value = this.getDisplayText(row.recordData, fieldName);
3725
+ const cacheKey = this.formBuilder.generateOptionsCacheKey('table', {});
3726
+ const resolvedOptions = this.formBuilder.resolvedFieldOptions.get(fieldName)?.[cacheKey];
3727
+ const value = this.getDisplayText(row.recordData, fieldName, resolvedOptions);
3677
3728
  cell.innerHTML = field.listEscapeHTML ? FTableDOMHelper.escapeHtml(value) : value;
3678
3729
  cell.className = `${field.listClass || ''} ${field.listClassEntry || ''}`.trim();
3679
3730
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@liedekef/ftable",
3
- "version": "1.1.24",
3
+ "version": "1.1.25",
4
4
  "description": "Modern, lightweight, jQuery-free CRUD table for dynamic AJAX-powered tables.",
5
5
  "main": "ftable.js",
6
6
  "module": "ftable.esm.js",