@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.
@@ -7044,34 +7044,46 @@ class VBindDirective {
7044
7044
  */
7045
7045
  class ReactiveProxy {
7046
7046
  /**
7047
- * A WeakMap to store the original target for each proxy.
7048
- * This allows us to avoid creating multiple proxies for the same object.
7047
+ * A WeakMap to store the proxy for each target object and path combination.
7048
+ * This prevents creating multiple proxies for the same object accessed from different paths.
7049
7049
  */
7050
- static proxyMap = new WeakMap();
7050
+ static proxyCache = new WeakMap();
7051
7051
  /**
7052
7052
  * Creates a reactive proxy for the given object.
7053
7053
  * The proxy will call the onChange callback whenever a property is modified.
7054
7054
  *
7055
7055
  * @param target The object to make reactive.
7056
- * @param onChange Callback function to call when the object changes. Receives the changed key name.
7056
+ * @param onChange Callback function to call when the object changes. Receives the full path of the changed property.
7057
+ * @param path The current path in the object tree (used internally for nested objects).
7057
7058
  * @returns A reactive proxy of the target object.
7058
7059
  */
7059
- static create(target, onChange) {
7060
+ static create(target, onChange, path = '') {
7060
7061
  // If the target is not an object or is null, return it as-is
7061
7062
  if (typeof target !== 'object' || target === null) {
7062
7063
  return target;
7063
7064
  }
7064
- // If this object already has a proxy, return the existing proxy
7065
- if (this.proxyMap.has(target)) {
7066
- return this.proxyMap.get(target);
7065
+ // Check if we already have a proxy for this target with this path
7066
+ let pathMap = this.proxyCache.get(target);
7067
+ if (pathMap) {
7068
+ const existingProxy = pathMap.get(path);
7069
+ if (existingProxy) {
7070
+ return existingProxy;
7071
+ }
7072
+ }
7073
+ else {
7074
+ pathMap = new Map();
7075
+ this.proxyCache.set(target, pathMap);
7067
7076
  }
7068
- // Create the proxy
7077
+ // Create the proxy with path captured in closure
7069
7078
  const proxy = new Proxy(target, {
7070
7079
  get(obj, key) {
7071
7080
  const value = Reflect.get(obj, key);
7072
7081
  // If the value is an object or array, make it reactive too
7073
7082
  if (typeof value === 'object' && value !== null) {
7074
- return ReactiveProxy.create(value, onChange);
7083
+ // Build the nested path
7084
+ const keyStr = String(key);
7085
+ const nestedPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7086
+ return ReactiveProxy.create(value, onChange, nestedPath);
7075
7087
  }
7076
7088
  // For arrays, intercept mutation methods
7077
7089
  if (Array.isArray(obj) && typeof value === 'function') {
@@ -7079,7 +7091,7 @@ class ReactiveProxy {
7079
7091
  if (arrayMutationMethods.includes(key)) {
7080
7092
  return function (...args) {
7081
7093
  const result = value.apply(this, args);
7082
- onChange();
7094
+ onChange(path || undefined);
7083
7095
  return result;
7084
7096
  };
7085
7097
  }
@@ -7091,18 +7103,22 @@ class ReactiveProxy {
7091
7103
  const result = Reflect.set(obj, key, value);
7092
7104
  // Only trigger onChange if the value actually changed
7093
7105
  if (oldValue !== value) {
7094
- onChange(key);
7106
+ const keyStr = String(key);
7107
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7108
+ onChange(fullPath);
7095
7109
  }
7096
7110
  return result;
7097
7111
  },
7098
7112
  deleteProperty(obj, key) {
7099
7113
  const result = Reflect.deleteProperty(obj, key);
7100
- onChange(key);
7114
+ const keyStr = String(key);
7115
+ const fullPath = path ? (Array.isArray(obj) ? `${path}[${keyStr}]` : `${path}.${keyStr}`) : keyStr;
7116
+ onChange(fullPath);
7101
7117
  return result;
7102
7118
  }
7103
7119
  });
7104
- // Store the proxy so we can return it if requested again
7105
- this.proxyMap.set(target, proxy);
7120
+ // Cache the proxy for this path
7121
+ pathMap.set(path, proxy);
7106
7122
  return proxy;
7107
7123
  }
7108
7124
  /**
@@ -7112,7 +7128,7 @@ class ReactiveProxy {
7112
7128
  * @returns True if the object is a reactive proxy, false otherwise.
7113
7129
  */
7114
7130
  static isReactive(obj) {
7115
- return this.proxyMap.has(obj);
7131
+ return this.proxyCache.has(obj);
7116
7132
  }
7117
7133
  /**
7118
7134
  * Unwraps a reactive proxy to get the original object.
@@ -7151,6 +7167,10 @@ class VBindings {
7151
7167
  * The set of changed identifiers.
7152
7168
  */
7153
7169
  #changes = new Set();
7170
+ /**
7171
+ * Cache for array lengths to detect length changes when the same object reference is used.
7172
+ */
7173
+ #lengthCache = new Map();
7154
7174
  /**
7155
7175
  * Creates a new instance of VBindings.
7156
7176
  * @param parent The parent bindings, if any.
@@ -7178,14 +7198,29 @@ class VBindings {
7178
7198
  let newValue = value;
7179
7199
  if (typeof value === 'object' && value !== null) {
7180
7200
  // Wrap objects/arrays with reactive proxy, tracking the root key
7181
- newValue = ReactiveProxy.create(value, () => {
7182
- this.#changes.add(key);
7183
- this.#onChange?.(key);
7184
- });
7201
+ newValue = ReactiveProxy.create(value, (changedPath) => {
7202
+ let path = '';
7203
+ for (const part of changedPath?.split('.') || []) {
7204
+ path = path ? `${path}.${part}` : part;
7205
+ this.#changes.add(path);
7206
+ }
7207
+ this.#onChange?.(changedPath);
7208
+ }, key);
7185
7209
  }
7186
7210
  const oldValue = Reflect.get(target, key);
7187
7211
  const result = Reflect.set(target, key, newValue);
7188
- if ((oldValue !== newValue) || (typeof oldValue === 'object' || typeof newValue === 'object')) {
7212
+ // Detect changes
7213
+ let hasChanged = oldValue !== newValue;
7214
+ // Special handling for arrays: check length changes even if same object reference
7215
+ if (!hasChanged && Array.isArray(newValue)) {
7216
+ const cachedLength = this.#lengthCache.get(key);
7217
+ const currentLength = newValue.length;
7218
+ if (cachedLength !== undefined && cachedLength !== currentLength) {
7219
+ hasChanged = true;
7220
+ }
7221
+ this.#lengthCache.set(key, currentLength);
7222
+ }
7223
+ if (hasChanged) {
7189
7224
  this.#changes.add(key);
7190
7225
  this.#onChange?.(key);
7191
7226
  }
@@ -7541,6 +7576,11 @@ class VNode {
7541
7576
  * The data bindings associated with this virtual node, if any.
7542
7577
  */
7543
7578
  #bindings;
7579
+ /**
7580
+ * The initial set of identifiers that this node depends on.
7581
+ * This is optional and may be undefined if there are no dependent identifiers.
7582
+ */
7583
+ #initDependentIdentifiers;
7544
7584
  /**
7545
7585
  * An evaluator for text nodes that contain expressions in {{...}}.
7546
7586
  * This is used to dynamically update the text content based on data bindings.
@@ -7585,6 +7625,7 @@ class VNode {
7585
7625
  this.#nodeName = args.node.nodeName;
7586
7626
  this.#parentVNode = args.parentVNode;
7587
7627
  this.#bindings = args.bindings;
7628
+ this.#initDependentIdentifiers = args.dependentIdentifiers;
7588
7629
  this.#parentVNode?.addChild(this);
7589
7630
  // If the node is a text node, check for expressions and create a text evaluator
7590
7631
  if (this.#nodeType === Node.TEXT_NODE) {
@@ -7736,6 +7777,8 @@ class VNode {
7736
7777
  }
7737
7778
  // Collect identifiers from text evaluator and directives
7738
7779
  const ids = [];
7780
+ // Include initial dependent identifiers, if any
7781
+ ids.push(...this.#initDependentIdentifiers ?? []);
7739
7782
  // If this is a text node with a text evaluator, include its identifiers
7740
7783
  if (this.#textEvaluator) {
7741
7784
  ids.push(...this.#textEvaluator.identifiers);
@@ -7767,6 +7810,29 @@ class VNode {
7767
7810
  this.#preparableIdentifiers = preparableIdentifiers.length === 0 ? [] : [...new Set(preparableIdentifiers)];
7768
7811
  return this.#preparableIdentifiers;
7769
7812
  }
7813
+ /**
7814
+ * The DOM path of this virtual node.
7815
+ * This is a string representation of the path from the root to this node,
7816
+ * using the node names and their indices among siblings with the same name.
7817
+ * For example: "DIV[0]/SPAN[1]/#text[0]"
7818
+ * @return The DOM path as a string.
7819
+ */
7820
+ get domPath() {
7821
+ const path = [];
7822
+ let node = this;
7823
+ while (node) {
7824
+ if (node.parentVNode && node.parentVNode.childVNodes) {
7825
+ const siblings = node.parentVNode.childVNodes.filter(v => v.nodeName === node?.nodeName);
7826
+ const index = siblings.indexOf(node);
7827
+ path.unshift(`${node.nodeName}[${index}]`);
7828
+ }
7829
+ else {
7830
+ path.unshift(node.nodeName);
7831
+ }
7832
+ node = node.parentVNode;
7833
+ }
7834
+ return path.join('/');
7835
+ }
7770
7836
  /**
7771
7837
  * Updates the virtual node and its children based on the current bindings.
7772
7838
  * This method evaluates any expressions in text nodes and applies effectors from directives.
@@ -7863,24 +7929,46 @@ class VNode {
7863
7929
  /**
7864
7930
  * Adds a dependent virtual node that relies on this node's bindings.
7865
7931
  * @param dependent The dependent virtual node to add.
7932
+ * @param dependentIdentifiers The identifiers that the dependent node relies on.
7933
+ * If not provided, the dependent node's own identifiers will be used.
7866
7934
  * @returns A list of closers to unregister the dependency, or undefined if no dependency was added.
7867
7935
  */
7868
- addDependent(dependent) {
7936
+ addDependent(dependent, dependentIdentifiers = undefined) {
7869
7937
  // List of closers to unregister the dependency
7870
7938
  const closers = [];
7871
- // Check if any of the dependent node's identifiers are in this node's identifiers
7872
- let hasIdentifier = dependent.dependentIdentifiers.some(id => this.preparableIdentifiers.includes(id));
7873
- if (!hasIdentifier) {
7874
- hasIdentifier = dependent.dependentIdentifiers.some(id => this.#bindings?.has(id, false) ?? false);
7939
+ // If dependent identifiers are not provided, use the dependent node's own identifiers
7940
+ if (!dependentIdentifiers) {
7941
+ dependentIdentifiers = [...dependent.dependentIdentifiers];
7942
+ }
7943
+ // Prepare alternative identifiers by stripping array indices (e.g., "items[0]" -> "items")
7944
+ const allDeps = new Set();
7945
+ dependentIdentifiers.forEach(id => {
7946
+ allDeps.add(id);
7947
+ const idx = id.indexOf('[');
7948
+ if (idx !== -1) {
7949
+ allDeps.add(id.substring(0, idx));
7950
+ }
7951
+ });
7952
+ // Get this node's identifiers
7953
+ const thisIds = [...this.preparableIdentifiers];
7954
+ if (this.#bindings) {
7955
+ thisIds.push(...this.#bindings?.raw ? Object.keys(this.#bindings.raw) : []);
7875
7956
  }
7876
7957
  // If the dependent node has an identifier in this node's identifiers, add it as a dependency
7877
- if (hasIdentifier) {
7958
+ if ([...allDeps].some(id => thisIds.includes(id))) {
7878
7959
  // If the dependencies list is not initialized, create it
7879
7960
  if (!this.#dependents) {
7880
7961
  this.#dependents = [];
7881
7962
  }
7882
7963
  // Add the dependent node to the list
7883
7964
  this.#dependents.push(dependent);
7965
+ // Remove the matched identifiers from the dependent node's identifiers to avoid duplicate dependencies
7966
+ thisIds.forEach(id => {
7967
+ const idx = dependentIdentifiers.indexOf(id);
7968
+ if (idx !== -1) {
7969
+ dependentIdentifiers.splice(idx, 1);
7970
+ }
7971
+ });
7884
7972
  // Create a closer to unregister the dependency
7885
7973
  closers.push({
7886
7974
  close: () => {
@@ -7893,7 +7981,7 @@ class VNode {
7893
7981
  });
7894
7982
  }
7895
7983
  // Recursively add the dependency to the parent node, if any
7896
- this.#parentVNode?.addDependent(dependent)?.forEach(closer => closers.push(closer));
7984
+ this.#parentVNode?.addDependent(dependent, dependentIdentifiers)?.forEach(closer => closers.push(closer));
7897
7985
  // Return a closer to unregister the dependency
7898
7986
  return closers.length > 0 ? closers : undefined;
7899
7987
  }
@@ -8565,7 +8653,8 @@ class VForDirective {
8565
8653
  node: clone,
8566
8654
  vApplication: this.#vNode.vApplication,
8567
8655
  parentVNode: this.#vNode.parentVNode,
8568
- bindings
8656
+ bindings,
8657
+ dependentIdentifiers: [`${this.#sourceName}[${context.index}]`]
8569
8658
  });
8570
8659
  return vNode;
8571
8660
  }
@@ -9595,7 +9684,7 @@ class VApplication {
9595
9684
  #initializeBindings() {
9596
9685
  // Create bindings with change tracking
9597
9686
  this.#bindings = new VBindings({
9598
- onChange: (key) => {
9687
+ onChange: (identifier) => {
9599
9688
  this.#scheduleUpdate();
9600
9689
  }
9601
9690
  });
@@ -9659,6 +9748,15 @@ class VApplication {
9659
9748
  }
9660
9749
  const computed = new Set();
9661
9750
  const processing = new Set();
9751
+ // Gather all changed identifiers, including parent properties for array items
9752
+ const allChanges = new Set();
9753
+ this.#bindings?.changes.forEach(id => {
9754
+ allChanges.add(id);
9755
+ const idx = id.indexOf('[');
9756
+ if (idx !== -1) {
9757
+ allChanges.add(id.substring(0, idx));
9758
+ }
9759
+ });
9662
9760
  // Helper function to recursively compute a property
9663
9761
  const compute = (key) => {
9664
9762
  // Skip if already computed in this update cycle
@@ -9674,7 +9772,7 @@ class VApplication {
9674
9772
  // Get the dependencies for this computed property
9675
9773
  const deps = this.#computedDependencies[key] || [];
9676
9774
  // If none of the dependencies have changed, skip recomputation
9677
- if (!deps.some(dep => this.#bindings?.changes.includes(dep))) {
9775
+ if (!deps.some(dep => allChanges.has(dep))) {
9678
9776
  computed.add(key);
9679
9777
  return;
9680
9778
  }
@@ -9692,7 +9790,13 @@ class VApplication {
9692
9790
  // Track if the computed value actually changed
9693
9791
  if (oldValue !== newValue) {
9694
9792
  this.#bindings?.set(key, newValue);
9695
- this.#logger.debug(`Computed property '${key}' changed: ${oldValue} -> ${newValue}`);
9793
+ this.#bindings?.changes.forEach(id => {
9794
+ allChanges.add(id);
9795
+ const idx = id.indexOf('[');
9796
+ if (idx !== -1) {
9797
+ allChanges.add(id.substring(0, idx));
9798
+ }
9799
+ });
9696
9800
  }
9697
9801
  }
9698
9802
  catch (error) {
@@ -9704,7 +9808,7 @@ class VApplication {
9704
9808
  // Find all computed properties that need to be recomputed
9705
9809
  for (const [key, deps] of Object.entries(this.#computedDependencies)) {
9706
9810
  // Check if any dependency has changed
9707
- if (deps.some(dep => this.#bindings?.changes.includes(dep))) {
9811
+ if (deps.some(dep => allChanges.has(dep))) {
9708
9812
  compute(key);
9709
9813
  }
9710
9814
  }