@mintjamsinc/ichigojs 0.1.1 → 0.1.2

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.
@@ -7050,34 +7050,46 @@
7050
7050
  */
7051
7051
  class ReactiveProxy {
7052
7052
  /**
7053
- * A WeakMap to store the original target for each proxy.
7054
- * This allows us to avoid creating multiple proxies for the same object.
7053
+ * A WeakMap to store the proxy for each target object and path combination.
7054
+ * This prevents creating multiple proxies for the same object accessed from different paths.
7055
7055
  */
7056
- static proxyMap = new WeakMap();
7056
+ static proxyCache = new WeakMap();
7057
7057
  /**
7058
7058
  * Creates a reactive proxy for the given object.
7059
7059
  * The proxy will call the onChange callback whenever a property is modified.
7060
7060
  *
7061
7061
  * @param target The object to make reactive.
7062
- * @param onChange Callback function to call when the object changes. Receives the changed key name.
7062
+ * @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
7063
+ * @param path The current path in the object tree (used internally for nested objects).
7063
7064
  * @returns A reactive proxy of the target object.
7064
7065
  */
7065
- static create(target, onChange) {
7066
+ static create(target, onChange, path = '') {
7066
7067
  // If the target is not an object or is null, return it as-is
7067
7068
  if (typeof target !== 'object' || target === null) {
7068
7069
  return target;
7069
7070
  }
7070
- // If this object already has a proxy, return the existing proxy
7071
- if (this.proxyMap.has(target)) {
7072
- return this.proxyMap.get(target);
7071
+ // Check if we already have a proxy for this target with this path
7072
+ let pathMap = this.proxyCache.get(target);
7073
+ if (pathMap) {
7074
+ const existingProxy = pathMap.get(path);
7075
+ if (existingProxy) {
7076
+ return existingProxy;
7077
+ }
7078
+ }
7079
+ else {
7080
+ pathMap = new Map();
7081
+ this.proxyCache.set(target, pathMap);
7073
7082
  }
7074
- // Create the proxy
7083
+ // Create the proxy with path captured in closure
7075
7084
  const proxy = new Proxy(target, {
7076
7085
  get(obj, key) {
7077
7086
  const value = Reflect.get(obj, key);
7078
7087
  // If the value is an object or array, make it reactive too
7079
7088
  if (typeof value === 'object' && value !== null) {
7080
- return ReactiveProxy.create(value, onChange);
7089
+ // Build the nested path
7090
+ const keyStr = String(key);
7091
+ const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7092
+ return ReactiveProxy.create(value, onChange, nestedPath);
7081
7093
  }
7082
7094
  // For arrays, intercept mutation methods
7083
7095
  if (Array.isArray(obj) && typeof value === 'function') {
@@ -7085,7 +7097,7 @@
7085
7097
  if (arrayMutationMethods.includes(key)) {
7086
7098
  return function (...args) {
7087
7099
  const result = value.apply(this, args);
7088
- onChange();
7100
+ onChange(path || undefined);
7089
7101
  return result;
7090
7102
  };
7091
7103
  }
@@ -7097,18 +7109,22 @@
7097
7109
  const result = Reflect.set(obj, key, value);
7098
7110
  // Only trigger onChange if the value actually changed
7099
7111
  if (oldValue !== value) {
7100
- onChange(key);
7112
+ const keyStr = String(key);
7113
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7114
+ onChange(fullPath);
7101
7115
  }
7102
7116
  return result;
7103
7117
  },
7104
7118
  deleteProperty(obj, key) {
7105
7119
  const result = Reflect.deleteProperty(obj, key);
7106
- onChange(key);
7120
+ const keyStr = String(key);
7121
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7122
+ onChange(fullPath);
7107
7123
  return result;
7108
7124
  }
7109
7125
  });
7110
- // Store the proxy so we can return it if requested again
7111
- this.proxyMap.set(target, proxy);
7126
+ // Cache the proxy for this path
7127
+ pathMap.set(path, proxy);
7112
7128
  return proxy;
7113
7129
  }
7114
7130
  /**
@@ -7118,7 +7134,7 @@
7118
7134
  * @returns True if the object is a reactive proxy, false otherwise.
7119
7135
  */
7120
7136
  static isReactive(obj) {
7121
- return this.proxyMap.has(obj);
7137
+ return this.proxyCache.has(obj);
7122
7138
  }
7123
7139
  /**
7124
7140
  * Unwraps a reactive proxy to get the original object.
@@ -7157,6 +7173,10 @@
7157
7173
  * The set of changed identifiers.
7158
7174
  */
7159
7175
  #changes = new Set();
7176
+ /**
7177
+ * Cache for array lengths to detect length changes when the same object reference is used.
7178
+ */
7179
+ #lengthCache = new Map();
7160
7180
  /**
7161
7181
  * Creates a new instance of VBindings.
7162
7182
  * @param parent The parent bindings, if any.
@@ -7184,14 +7204,29 @@
7184
7204
  let newValue = value;
7185
7205
  if (typeof value === 'object' && value !== null) {
7186
7206
  // Wrap objects/arrays with reactive proxy, tracking the root key
7187
- newValue = ReactiveProxy.create(value, () => {
7188
- this.#changes.add(key);
7189
- this.#onChange?.(key);
7190
- });
7207
+ newValue = ReactiveProxy.create(value, (changedPath) => {
7208
+ let path = '';
7209
+ for (const part of changedPath?.split('.') || []) {
7210
+ path = path ? `${path}.${part}` : part;
7211
+ this.#changes.add(path);
7212
+ }
7213
+ this.#onChange?.(changedPath);
7214
+ }, key);
7191
7215
  }
7192
7216
  const oldValue = Reflect.get(target, key);
7193
7217
  const result = Reflect.set(target, key, newValue);
7194
- if ((oldValue !== newValue) || (typeof oldValue === 'object' || typeof newValue === 'object')) {
7218
+ // Detect changes
7219
+ let hasChanged = oldValue !== newValue;
7220
+ // Special handling for arrays: check length changes even if same object reference
7221
+ if (!hasChanged && Array.isArray(newValue)) {
7222
+ const cachedLength = this.#lengthCache.get(key);
7223
+ const currentLength = newValue.length;
7224
+ if (cachedLength !== undefined && cachedLength !== currentLength) {
7225
+ hasChanged = true;
7226
+ }
7227
+ this.#lengthCache.set(key, currentLength);
7228
+ }
7229
+ if (hasChanged) {
7195
7230
  this.#changes.add(key);
7196
7231
  this.#onChange?.(key);
7197
7232
  }
@@ -7547,6 +7582,11 @@
7547
7582
  * The data bindings associated with this virtual node, if any.
7548
7583
  */
7549
7584
  #bindings;
7585
+ /**
7586
+ * The initial set of identifiers that this node depends on.
7587
+ * This is optional and may be undefined if there are no dependent identifiers.
7588
+ */
7589
+ #initDependentIdentifiers;
7550
7590
  /**
7551
7591
  * An evaluator for text nodes that contain expressions in {{...}}.
7552
7592
  * This is used to dynamically update the text content based on data bindings.
@@ -7591,6 +7631,7 @@
7591
7631
  this.#nodeName = args.node.nodeName;
7592
7632
  this.#parentVNode = args.parentVNode;
7593
7633
  this.#bindings = args.bindings;
7634
+ this.#initDependentIdentifiers = args.dependentIdentifiers;
7594
7635
  this.#parentVNode?.addChild(this);
7595
7636
  // If the node is a text node, check for expressions and create a text evaluator
7596
7637
  if (this.#nodeType === Node.TEXT_NODE) {
@@ -7742,6 +7783,8 @@
7742
7783
  }
7743
7784
  // Collect identifiers from text evaluator and directives
7744
7785
  const ids = [];
7786
+ // Include initial dependent identifiers, if any
7787
+ ids.push(...this.#initDependentIdentifiers ?? []);
7745
7788
  // If this is a text node with a text evaluator, include its identifiers
7746
7789
  if (this.#textEvaluator) {
7747
7790
  ids.push(...this.#textEvaluator.identifiers);
@@ -7773,6 +7816,29 @@
7773
7816
  this.#preparableIdentifiers = preparableIdentifiers.length === 0 ? [] : [...new Set(preparableIdentifiers)];
7774
7817
  return this.#preparableIdentifiers;
7775
7818
  }
7819
+ /**
7820
+ * The DOM path of this virtual node.
7821
+ * This is a string representation of the path from the root to this node,
7822
+ * using the node names and their indices among siblings with the same name.
7823
+ * For example: "DIV[0]/SPAN[1]/#text[0]"
7824
+ * @return The DOM path as a string.
7825
+ */
7826
+ get domPath() {
7827
+ const path = [];
7828
+ let node = this;
7829
+ while (node) {
7830
+ if (node.parentVNode && node.parentVNode.childVNodes) {
7831
+ const siblings = node.parentVNode.childVNodes.filter(v => v.nodeName === node?.nodeName);
7832
+ const index = siblings.indexOf(node);
7833
+ path.unshift(`${node.nodeName}[${index}]`);
7834
+ }
7835
+ else {
7836
+ path.unshift(node.nodeName);
7837
+ }
7838
+ node = node.parentVNode;
7839
+ }
7840
+ return path.join('/');
7841
+ }
7776
7842
  /**
7777
7843
  * Updates the virtual node and its children based on the current bindings.
7778
7844
  * This method evaluates any expressions in text nodes and applies effectors from directives.
@@ -7869,24 +7935,46 @@
7869
7935
  /**
7870
7936
  * Adds a dependent virtual node that relies on this node's bindings.
7871
7937
  * @param dependent The dependent virtual node to add.
7938
+ * @param dependentIdentifiers The identifiers that the dependent node relies on.
7939
+ * If not provided, the dependent node's own identifiers will be used.
7872
7940
  * @returns A list of closers to unregister the dependency, or undefined if no dependency was added.
7873
7941
  */
7874
- addDependent(dependent) {
7942
+ addDependent(dependent, dependentIdentifiers = undefined) {
7875
7943
  // List of closers to unregister the dependency
7876
7944
  const closers = [];
7877
- // Check if any of the dependent node's identifiers are in this node's identifiers
7878
- let hasIdentifier = dependent.dependentIdentifiers.some(id => this.preparableIdentifiers.includes(id));
7879
- if (!hasIdentifier) {
7880
- hasIdentifier = dependent.dependentIdentifiers.some(id => this.#bindings?.has(id, false) ?? false);
7945
+ // If dependent identifiers are not provided, use the dependent node's own identifiers
7946
+ if (!dependentIdentifiers) {
7947
+ dependentIdentifiers = [...dependent.dependentIdentifiers];
7948
+ }
7949
+ // Prepare alternative identifiers by stripping array indices (e.g., "items[0]" -> "items")
7950
+ const allDeps = new Set();
7951
+ dependentIdentifiers.forEach(id => {
7952
+ allDeps.add(id);
7953
+ const idx = id.indexOf('[');
7954
+ if (idx !== -1) {
7955
+ allDeps.add(id.substring(0, idx));
7956
+ }
7957
+ });
7958
+ // Get this node's identifiers
7959
+ const thisIds = [...this.preparableIdentifiers];
7960
+ if (this.#bindings) {
7961
+ thisIds.push(...this.#bindings?.raw ? Object.keys(this.#bindings.raw) : []);
7881
7962
  }
7882
7963
  // If the dependent node has an identifier in this node's identifiers, add it as a dependency
7883
- if (hasIdentifier) {
7964
+ if ([...allDeps].some(id => thisIds.includes(id))) {
7884
7965
  // If the dependencies list is not initialized, create it
7885
7966
  if (!this.#dependents) {
7886
7967
  this.#dependents = [];
7887
7968
  }
7888
7969
  // Add the dependent node to the list
7889
7970
  this.#dependents.push(dependent);
7971
+ // Remove the matched identifiers from the dependent node's identifiers to avoid duplicate dependencies
7972
+ thisIds.forEach(id => {
7973
+ const idx = dependentIdentifiers.indexOf(id);
7974
+ if (idx !== -1) {
7975
+ dependentIdentifiers.splice(idx, 1);
7976
+ }
7977
+ });
7890
7978
  // Create a closer to unregister the dependency
7891
7979
  closers.push({
7892
7980
  close: () => {
@@ -7899,7 +7987,7 @@
7899
7987
  });
7900
7988
  }
7901
7989
  // Recursively add the dependency to the parent node, if any
7902
- this.#parentVNode?.addDependent(dependent)?.forEach(closer => closers.push(closer));
7990
+ this.#parentVNode?.addDependent(dependent, dependentIdentifiers)?.forEach(closer => closers.push(closer));
7903
7991
  // Return a closer to unregister the dependency
7904
7992
  return closers.length > 0 ? closers : undefined;
7905
7993
  }
@@ -8571,7 +8659,8 @@
8571
8659
  node: clone,
8572
8660
  vApplication: this.#vNode.vApplication,
8573
8661
  parentVNode: this.#vNode.parentVNode,
8574
- bindings
8662
+ bindings,
8663
+ dependentIdentifiers: [`${this.#sourceName}[${context.index}]`]
8575
8664
  });
8576
8665
  return vNode;
8577
8666
  }
@@ -9601,7 +9690,7 @@
9601
9690
  #initializeBindings() {
9602
9691
  // Create bindings with change tracking
9603
9692
  this.#bindings = new VBindings({
9604
- onChange: (key) => {
9693
+ onChange: (identifier) => {
9605
9694
  this.#scheduleUpdate();
9606
9695
  }
9607
9696
  });
@@ -9665,6 +9754,15 @@
9665
9754
  }
9666
9755
  const computed = new Set();
9667
9756
  const processing = new Set();
9757
+ // Gather all changed identifiers, including parent properties for array items
9758
+ const allChanges = new Set();
9759
+ this.#bindings?.changes.forEach(id => {
9760
+ allChanges.add(id);
9761
+ const idx = id.indexOf('[');
9762
+ if (idx !== -1) {
9763
+ allChanges.add(id.substring(0, idx));
9764
+ }
9765
+ });
9668
9766
  // Helper function to recursively compute a property
9669
9767
  const compute = (key) => {
9670
9768
  // Skip if already computed in this update cycle
@@ -9680,7 +9778,7 @@
9680
9778
  // Get the dependencies for this computed property
9681
9779
  const deps = this.#computedDependencies[key] || [];
9682
9780
  // If none of the dependencies have changed, skip recomputation
9683
- if (!deps.some(dep => this.#bindings?.changes.includes(dep))) {
9781
+ if (!deps.some(dep => allChanges.has(dep))) {
9684
9782
  computed.add(key);
9685
9783
  return;
9686
9784
  }
@@ -9698,7 +9796,13 @@
9698
9796
  // Track if the computed value actually changed
9699
9797
  if (oldValue !== newValue) {
9700
9798
  this.#bindings?.set(key, newValue);
9701
- this.#logger.debug(`Computed property '${key}' changed: ${oldValue} -> ${newValue}`);
9799
+ this.#bindings?.changes.forEach(id => {
9800
+ allChanges.add(id);
9801
+ const idx = id.indexOf('[');
9802
+ if (idx !== -1) {
9803
+ allChanges.add(id.substring(0, idx));
9804
+ }
9805
+ });
9702
9806
  }
9703
9807
  }
9704
9808
  catch (error) {
@@ -9710,7 +9814,7 @@
9710
9814
  // Find all computed properties that need to be recomputed
9711
9815
  for (const [key, deps] of Object.entries(this.#computedDependencies)) {
9712
9816
  // Check if any dependency has changed
9713
- if (deps.some(dep => this.#bindings?.changes.includes(dep))) {
9817
+ if (deps.some(dep => allChanges.has(dep))) {
9714
9818
  compute(key);
9715
9819
  }
9716
9820
  }