@mintjamsinc/ichigojs 0.1.0 → 0.1.1

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.
@@ -6765,7 +6765,7 @@
6765
6765
  /**
6766
6766
  * A list of variable and function names used in the directive's expression.
6767
6767
  */
6768
- #identifiers;
6768
+ #dependentIdentifiers;
6769
6769
  /**
6770
6770
  * A function that evaluates the directive's expression.
6771
6771
  * It returns the evaluated value of the expression.
@@ -6779,6 +6779,16 @@
6779
6779
  * The original expression string from the directive.
6780
6780
  */
6781
6781
  #expression;
6782
+ /**
6783
+ * The set of class names managed by this directive (used when binding to the "class" attribute).
6784
+ * This helps in tracking which classes were added by this directive to avoid conflicts with other class manipulations.
6785
+ */
6786
+ #managedClasses = new Set();
6787
+ /**
6788
+ * The set of style properties managed by this directive (used when binding to the "style" attribute).
6789
+ * This helps in tracking which styles were added by this directive to avoid conflicts with other style manipulations.
6790
+ */
6791
+ #managedStyles = new Set();
6782
6792
  /**
6783
6793
  * @param context The context for parsing the directive.
6784
6794
  */
@@ -6796,7 +6806,7 @@
6796
6806
  // Parse the expression to extract identifiers and create the evaluator
6797
6807
  this.#expression = context.attribute.value;
6798
6808
  if (this.#expression) {
6799
- this.#identifiers = ExpressionUtils.extractIdentifiers(this.#expression, context.vNode.vApplication.functionDependencies);
6809
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(this.#expression, context.vNode.vApplication.functionDependencies);
6800
6810
  this.#evaluate = this.#createEvaluator(this.#expression);
6801
6811
  }
6802
6812
  // Remove the directive attribute from the element
@@ -6830,11 +6840,11 @@
6830
6840
  * @inheritdoc
6831
6841
  */
6832
6842
  get domUpdater() {
6833
- const identifiers = this.#identifiers ?? [];
6843
+ const identifiers = this.#dependentIdentifiers ?? [];
6834
6844
  const render = () => this.#render();
6835
6845
  // Create an updater that handles the attribute binding
6836
6846
  const updater = {
6837
- get identifiers() {
6847
+ get dependentIdentifiers() {
6838
6848
  return identifiers;
6839
6849
  },
6840
6850
  applyToDOM() {
@@ -6843,6 +6853,18 @@
6843
6853
  };
6844
6854
  return updater;
6845
6855
  }
6856
+ /**
6857
+ * @inheritdoc
6858
+ */
6859
+ get templatize() {
6860
+ return false;
6861
+ }
6862
+ /**
6863
+ * @inheritdoc
6864
+ */
6865
+ get dependentIdentifiers() {
6866
+ return this.#dependentIdentifiers ?? [];
6867
+ }
6846
6868
  /**
6847
6869
  * Indicates if this directive is binding the "key" attribute.
6848
6870
  * The "key" attribute is special and is used for optimizing rendering of lists.
@@ -6900,39 +6922,54 @@
6900
6922
  * Updates the class attribute with support for string, array, and object formats.
6901
6923
  */
6902
6924
  #updateClass(element, value) {
6903
- // Clear existing classes
6904
- element.className = '';
6925
+ // Determine the new set of classes to apply
6926
+ let newClasses = [];
6905
6927
  if (typeof value === 'string') {
6906
- element.className = value;
6928
+ newClasses = value.split(/\s+/).filter(Boolean);
6907
6929
  }
6908
6930
  else if (Array.isArray(value)) {
6909
- element.className = value.filter(Boolean).join(' ');
6931
+ newClasses = value.filter(Boolean);
6910
6932
  }
6911
6933
  else if (typeof value === 'object' && value !== null) {
6912
- const classes = Object.keys(value).filter(key => value[key]);
6913
- element.className = classes.join(' ');
6934
+ newClasses = Object.keys(value).filter(key => value[key]);
6914
6935
  }
6936
+ // Remove previously managed classes
6937
+ this.#managedClasses.forEach(cls => element.classList.remove(cls));
6938
+ // Add newly managed classes
6939
+ newClasses.forEach(cls => element.classList.add(cls));
6940
+ // Update managed classes list
6941
+ this.#managedClasses = new Set(newClasses);
6915
6942
  }
6916
6943
  /**
6917
6944
  * Updates the style attribute with support for object format.
6918
6945
  */
6919
6946
  #updateStyle(element, value) {
6947
+ let newStyles = [];
6920
6948
  if (typeof value === 'string') {
6949
+ // Directly set the style string
6921
6950
  element.style.cssText = value;
6951
+ // Extract managed properties
6952
+ newStyles = value.split(';').map(s => s.split(':')[0].trim()).filter(Boolean);
6922
6953
  }
6923
6954
  else if (typeof value === 'object' && value !== null) {
6924
- // Clear existing inline styles
6925
- element.style.cssText = '';
6955
+ // Remove all previously managed properties
6956
+ this.#managedStyles.forEach(prop => {
6957
+ element.style.removeProperty(this.#camelToKebab(prop));
6958
+ });
6959
+ // Add newly managed properties
6926
6960
  for (const key in value) {
6927
6961
  if (Object.prototype.hasOwnProperty.call(value, key)) {
6928
6962
  const cssKey = this.#camelToKebab(key);
6929
6963
  const cssValue = value[key];
6930
6964
  if (cssValue != null) {
6931
6965
  element.style.setProperty(cssKey, String(cssValue));
6966
+ newStyles.push(key);
6932
6967
  }
6933
6968
  }
6934
6969
  }
6935
6970
  }
6971
+ // Update managed properties list
6972
+ this.#managedStyles = new Set(newStyles);
6936
6973
  }
6937
6974
  /**
6938
6975
  * Converts camelCase to kebab-case for CSS properties.
@@ -6992,7 +7029,7 @@
6992
7029
  * @returns A function that evaluates the directive's condition.
6993
7030
  */
6994
7031
  #createEvaluator(expression) {
6995
- const identifiers = this.#identifiers ?? [];
7032
+ const identifiers = this.#dependentIdentifiers ?? [];
6996
7033
  const args = identifiers.join(", ");
6997
7034
  const funcBody = `return (${expression});`;
6998
7035
  // Create a dynamic function with the identifiers as parameters
@@ -7000,7 +7037,7 @@
7000
7037
  // Return a function that calls the dynamic function with the current values from the virtual node's bindings
7001
7038
  return () => {
7002
7039
  // Gather the current values of the identifiers from the bindings
7003
- const values = identifiers.map(id => this.#vNode.bindings?.[id]);
7040
+ const values = identifiers.map(id => this.#vNode.bindings?.get(id));
7004
7041
  // Call the dynamic function with the gathered values and return the result as a boolean
7005
7042
  return func(...values);
7006
7043
  };
@@ -7009,311 +7046,245 @@
7009
7046
 
7010
7047
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7011
7048
  /**
7012
- * Context for managing related conditional directives (v-if, v-else-if, v-else).
7049
+ * Utility class for creating reactive proxies that automatically track changes.
7013
7050
  */
7014
- class VConditionalDirectiveContext {
7015
- #directives = [];
7051
+ class ReactiveProxy {
7016
7052
  /**
7017
- * Adds a directive (v-else-if or v-else) to the conditional context.
7018
- * @param directive The directive to add.
7053
+ * A WeakMap to store the original target for each proxy.
7054
+ * This allows us to avoid creating multiple proxies for the same object.
7019
7055
  */
7020
- addDirective(directive) {
7021
- this.#directives.push(directive);
7022
- }
7056
+ static proxyMap = new WeakMap();
7023
7057
  /**
7024
- * Checks if any preceding directive's condition is met.
7025
- * This is used to determine if a v-else-if or v-else directive should be rendered.
7026
- * @param directive The directive to check against.
7027
- * @returns True if any preceding directive's condition is met, otherwise false.
7058
+ * Creates a reactive proxy for the given object.
7059
+ * The proxy will call the onChange callback whenever a property is modified.
7060
+ *
7061
+ * @param target The object to make reactive.
7062
+ * @param onChange Callback function to call when the object changes. Receives the changed key name.
7063
+ * @returns A reactive proxy of the target object.
7028
7064
  */
7029
- isPrecedingConditionMet(directive) {
7030
- const index = this.#directives.indexOf(directive);
7031
- if (index === -1) {
7032
- throw new Error("Directive not found in context.");
7065
+ static create(target, onChange) {
7066
+ // If the target is not an object or is null, return it as-is
7067
+ if (typeof target !== 'object' || target === null) {
7068
+ return target;
7033
7069
  }
7034
- // Check if all previous directives are met
7035
- for (let i = 0; i < index; i++) {
7036
- const d = this.#directives[i];
7037
- if (d.conditionIsMet === true) {
7038
- return true;
7039
- }
7070
+ // If this object already has a proxy, return the existing proxy
7071
+ if (this.proxyMap.has(target)) {
7072
+ return this.proxyMap.get(target);
7040
7073
  }
7041
- return false;
7074
+ // Create the proxy
7075
+ const proxy = new Proxy(target, {
7076
+ get(obj, key) {
7077
+ const value = Reflect.get(obj, key);
7078
+ // If the value is an object or array, make it reactive too
7079
+ if (typeof value === 'object' && value !== null) {
7080
+ return ReactiveProxy.create(value, onChange);
7081
+ }
7082
+ // For arrays, intercept mutation methods
7083
+ if (Array.isArray(obj) && typeof value === 'function') {
7084
+ const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
7085
+ if (arrayMutationMethods.includes(key)) {
7086
+ return function (...args) {
7087
+ const result = value.apply(this, args);
7088
+ onChange();
7089
+ return result;
7090
+ };
7091
+ }
7092
+ }
7093
+ return value;
7094
+ },
7095
+ set(obj, key, value) {
7096
+ const oldValue = Reflect.get(obj, key);
7097
+ const result = Reflect.set(obj, key, value);
7098
+ // Only trigger onChange if the value actually changed
7099
+ if (oldValue !== value) {
7100
+ onChange(key);
7101
+ }
7102
+ return result;
7103
+ },
7104
+ deleteProperty(obj, key) {
7105
+ const result = Reflect.deleteProperty(obj, key);
7106
+ onChange(key);
7107
+ return result;
7108
+ }
7109
+ });
7110
+ // Store the proxy so we can return it if requested again
7111
+ this.proxyMap.set(target, proxy);
7112
+ return proxy;
7113
+ }
7114
+ /**
7115
+ * Checks if the given object is a reactive proxy.
7116
+ *
7117
+ * @param obj The object to check.
7118
+ * @returns True if the object is a reactive proxy, false otherwise.
7119
+ */
7120
+ static isReactive(obj) {
7121
+ return this.proxyMap.has(obj);
7122
+ }
7123
+ /**
7124
+ * Unwraps a reactive proxy to get the original object.
7125
+ * If the object is not a proxy, returns it as-is.
7126
+ *
7127
+ * @param obj The object to unwrap.
7128
+ * @returns The original object.
7129
+ */
7130
+ static unwrap(obj) {
7131
+ // This is a simplified implementation
7132
+ // In a full implementation, we'd need to store a reverse mapping
7133
+ return obj;
7042
7134
  }
7043
7135
  }
7044
7136
 
7045
7137
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7046
- class VConditionalDirective {
7138
+ /**
7139
+ * A dictionary representing bindings for a virtual node.
7140
+ * The key is the binding name, and the value is the binding value.
7141
+ * Supports hierarchical lookup through parent bindings.
7142
+ */
7143
+ class VBindings {
7047
7144
  /**
7048
- * The virtual node to which this directive is applied.
7145
+ * The parent bindings, if any.
7049
7146
  */
7050
- #vNode;
7147
+ #parent;
7051
7148
  /**
7052
- * A list of variable and function names used in the directive's expression.
7053
- * This may be undefined if the directive does not have an expression (e.g., v-else).
7054
- */
7055
- #identifiers;
7056
- /*
7057
- * A function that evaluates the directive's condition.
7058
- * It returns true if the condition is met, otherwise false.
7059
- * This may be undefined if the directive does not have an expression (e.g., v-else).
7149
+ * The key is the binding name, and the value is the binding value.
7060
7150
  */
7061
- #evaluate;
7151
+ #local;
7062
7152
  /**
7063
- * The context for managing related conditional directives (v-if, v-else-if, v-else).
7153
+ * The change tracker, if any.
7064
7154
  */
7065
- #conditionalContext;
7155
+ #onChange;
7066
7156
  /**
7067
- * @param context The context for parsing the directive.
7157
+ * The set of changed identifiers.
7068
7158
  */
7069
- constructor(context) {
7070
- this.#vNode = context.vNode;
7071
- // Parse the expression to extract identifiers and create the evaluator
7072
- const expression = context.attribute.value;
7073
- if (expression) {
7074
- this.#identifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
7075
- this.#evaluate = this.#createEvaluator(expression);
7076
- }
7077
- // Remove the directive attribute from the element
7078
- this.#vNode.node.removeAttribute(context.attribute.name);
7079
- // Initialize the conditional context for managing related directives
7080
- this.#conditionalContext = this.#initializeConditionalContext();
7081
- this.#conditionalContext.addDirective(this);
7082
- }
7159
+ #changes = new Set();
7083
7160
  /**
7084
- * @inheritdoc
7161
+ * Creates a new instance of VBindings.
7162
+ * @param parent The parent bindings, if any.
7085
7163
  */
7086
- get vNode() {
7087
- return this.#vNode;
7164
+ constructor(args = {}) {
7165
+ this.#parent = args.parent;
7166
+ this.#onChange = args.onChange;
7167
+ this.#local = new Proxy({}, {
7168
+ get: (obj, key) => {
7169
+ if (Reflect.has(obj, key)) {
7170
+ return Reflect.get(obj, key);
7171
+ }
7172
+ return this.#parent?.raw[key];
7173
+ },
7174
+ set: (obj, key, value) => {
7175
+ let target = obj;
7176
+ if (!Reflect.has(target, key)) {
7177
+ for (let parent = this.#parent; parent; parent = parent.#parent) {
7178
+ if (Reflect.has(parent.#local, key)) {
7179
+ target = parent.#local;
7180
+ break;
7181
+ }
7182
+ }
7183
+ }
7184
+ let newValue = value;
7185
+ if (typeof value === 'object' && value !== null) {
7186
+ // 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
+ });
7191
+ }
7192
+ const oldValue = Reflect.get(target, key);
7193
+ const result = Reflect.set(target, key, newValue);
7194
+ if ((oldValue !== newValue) || (typeof oldValue === 'object' || typeof newValue === 'object')) {
7195
+ this.#changes.add(key);
7196
+ this.#onChange?.(key);
7197
+ }
7198
+ return result;
7199
+ },
7200
+ deleteProperty: (obj, key) => {
7201
+ const result = Reflect.deleteProperty(obj, key);
7202
+ this.#changes.add(key);
7203
+ this.#onChange?.(key);
7204
+ return result;
7205
+ }
7206
+ });
7088
7207
  }
7089
7208
  /**
7090
- * @inheritdoc
7209
+ * Gets the raw bindings.
7210
+ * If a key is not found locally, it searches parent bindings recursively.
7091
7211
  */
7092
- get needsAnchor() {
7093
- return true;
7212
+ get raw() {
7213
+ return this.#local;
7094
7214
  }
7095
7215
  /**
7096
- * @inheritdoc
7216
+ * Indicates whether there are any changed identifiers.
7097
7217
  */
7098
- get bindingsPreparer() {
7099
- return undefined;
7218
+ get hasChanges() {
7219
+ if (this.#parent?.hasChanges) {
7220
+ return true;
7221
+ }
7222
+ return this.#changes.size > 0;
7100
7223
  }
7101
7224
  /**
7102
- * @inheritdoc
7225
+ * Gets the list of changed identifiers.
7103
7226
  */
7104
- get domUpdater() {
7105
- const identifiers = this.#identifiers ?? [];
7106
- const render = () => this.#render();
7107
- // Create an updater that handles the conditional rendering
7108
- const updater = {
7109
- get identifiers() {
7110
- return identifiers;
7111
- },
7112
- applyToDOM() {
7113
- render();
7114
- }
7115
- };
7116
- return updater;
7227
+ get changes() {
7228
+ const changes = new Set(this.#parent?.changes || []);
7229
+ this.#changes.forEach(id => changes.add(id));
7230
+ return Array.from(changes);
7117
7231
  }
7118
7232
  /**
7119
- * The context for managing related conditional directives (v-if, v-else-if, v-else).
7233
+ * Indicates whether this is the root bindings (i.e., has no parent).
7120
7234
  */
7121
- get conditionalContext() {
7122
- return this.#conditionalContext;
7235
+ get isRoot() {
7236
+ return !this.#parent;
7123
7237
  }
7124
7238
  /**
7125
- * Indicates whether the condition for this directive is currently met.
7126
- * For v-if and v-else-if, this depends on the evaluation of their expressions.
7127
- * For v-else, this is always true.
7239
+ * Clears the set of changed identifiers.
7128
7240
  */
7129
- get conditionIsMet() {
7130
- if (!this.#evaluate) {
7131
- // No expression means always true (e.g., v-else)
7132
- return true;
7133
- }
7134
- return this.#evaluate();
7241
+ clearChanges() {
7242
+ this.#changes.clear();
7135
7243
  }
7136
7244
  /**
7137
- * @inheritdoc
7245
+ * Sets a binding value.
7246
+ * @param key The binding name.
7247
+ * @param value The binding value.
7138
7248
  */
7139
- destroy() {
7140
- // Default implementation does nothing. Override in subclasses if needed.
7249
+ set(key, value) {
7250
+ this.#local[key] = value;
7141
7251
  }
7142
7252
  /**
7143
- * Renders the node based on the evaluation of the directive's condition.
7144
- * Inserts or removes the node from the DOM as needed.
7253
+ * Gets a binding value.
7254
+ * @param key The binding name.
7255
+ * @returns The binding value, or undefined if not found.
7145
7256
  */
7146
- #render() {
7147
- // Check if any preceding directive's condition is met
7148
- if (this.#conditionalContext.isPrecedingConditionMet(this)) {
7149
- // Previous condition met, ensure node is removed
7150
- this.#removedNode();
7151
- return;
7152
- }
7153
- if (!this.#evaluate) {
7154
- // No expression means always true (e.g., v-else)
7155
- this.#insertNode();
7156
- return;
7157
- }
7158
- // Evaluate the condition and insert or remove the node accordingly
7159
- const shouldRender = this.#evaluate();
7160
- if (shouldRender) {
7161
- this.#insertNode();
7162
- }
7163
- else {
7164
- this.#removedNode();
7165
- }
7257
+ get(key) {
7258
+ return this.#local[key];
7166
7259
  }
7167
7260
  /**
7168
- * Inserts the node into the DOM at the position marked by the anchor node, if any.
7169
- * If there is no anchor node, the node is inserted as a child of its parent node.
7170
- * If the node is already in the DOM, no action is taken.
7261
+ * Checks if a binding exists.
7262
+ * @param key The binding name.
7263
+ * @param recursive Whether to search parent bindings. Default is true.
7264
+ * @returns True if the binding exists, false otherwise.
7171
7265
  */
7172
- #insertNode() {
7173
- if (this.#vNode.isInDOM) {
7174
- // Already in DOM, no action needed
7175
- return;
7176
- }
7177
- if (this.#vNode?.anchorNode) {
7178
- // Insert after the anchor node
7179
- this.#vNode.anchorNode.parentNode?.insertBefore(this.#vNode.node, this.#vNode.anchorNode.nextSibling);
7180
- }
7181
- else if (this.#vNode.parentVNode) {
7182
- // Append to the parent node
7183
- const parentElement = this.#vNode.parentVNode.node;
7184
- parentElement.appendChild(this.#vNode.node);
7266
+ has(key, recursive = true) {
7267
+ if (key in this.#local) {
7268
+ return true;
7185
7269
  }
7186
- else {
7187
- // No anchor or parent VNode available
7188
- throw new Error("Cannot insert node: No anchor or parent VNode available.");
7270
+ if (!recursive) {
7271
+ return false;
7189
7272
  }
7273
+ return this.#parent?.has(key) ?? false;
7190
7274
  }
7191
7275
  /**
7192
- * Removes the node from the DOM.
7193
- * If the node is not in the DOM, no action is taken.
7276
+ * Removes a local binding.
7277
+ * @param key The binding name.
7194
7278
  */
7195
- #removedNode() {
7196
- if (!this.#vNode.isInDOM) {
7197
- // Already removed from DOM, no action needed
7198
- return;
7199
- }
7200
- this.#vNode.node.parentNode?.removeChild(this.#vNode.node);
7279
+ remove(key) {
7280
+ delete this.#local[key];
7201
7281
  }
7282
+ }
7283
+
7284
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7285
+ class VDirectiveManager {
7202
7286
  /**
7203
- * Creates a function to evaluate the directive's condition.
7204
- * @param expression The expression string to evaluate.
7205
- * @returns A function that evaluates the directive's condition.
7206
- */
7207
- #createEvaluator(expression) {
7208
- const identifiers = this.#identifiers ?? [];
7209
- const args = identifiers.join(", ");
7210
- const funcBody = `return (${expression});`;
7211
- // Create a dynamic function with the identifiers as parameters
7212
- const func = new Function(args, funcBody);
7213
- // Return a function that calls the dynamic function with the current values from the virtual node's bindings
7214
- return () => {
7215
- // Gather the current values of the identifiers from the bindings
7216
- const values = identifiers.map(id => this.#vNode.bindings?.[id]);
7217
- // Call the dynamic function with the gathered values and return the result as a boolean
7218
- return Boolean(func(...values));
7219
- };
7220
- }
7221
- /**
7222
- * Initializes the conditional context for managing related directives.
7223
- */
7224
- #initializeConditionalContext() {
7225
- // Create a new context if this is a v-if directive
7226
- if (this.name === StandardDirectiveName.V_IF) {
7227
- return new VConditionalDirectiveContext();
7228
- }
7229
- // Link to the existing conditional context from the preceding v-if or v-else-if directive
7230
- const precedingDirective = this.vNode.previousSibling?.directiveManager?.directives?.find(d => d.name === StandardDirectiveName.V_IF || d.name === StandardDirectiveName.V_ELSE_IF);
7231
- if (!precedingDirective) {
7232
- throw new Error("preceding v-if or v-else-if directive not found.");
7233
- }
7234
- // Cast to VConditionalDirective to access conditionalContext
7235
- const conditionalContext = precedingDirective.conditionalContext;
7236
- return conditionalContext;
7237
- }
7238
- }
7239
-
7240
- // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7241
- /**
7242
- * Directive for conditional rendering in the virtual DOM.
7243
- * This directive renders an element if the preceding v-if or v-else-if directive evaluated to false.
7244
- * For example:
7245
- * <div v-else>This div is rendered if the previous v-if or v-else-if was false.</div>
7246
- * The element and its children are included in the DOM only if the preceding v-if or v-else-if expression evaluates to false.
7247
- * If the preceding expression is true, this element and its children are not rendered.
7248
- * This directive must be used immediately after a v-if or v-else-if directive.
7249
- */
7250
- class VElseDirective extends VConditionalDirective {
7251
- /**
7252
- * @param context The context for parsing the directive.
7253
- */
7254
- constructor(context) {
7255
- super(context);
7256
- }
7257
- /**
7258
- * @inheritdoc
7259
- */
7260
- get name() {
7261
- return StandardDirectiveName.V_ELSE;
7262
- }
7263
- }
7264
-
7265
- // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7266
- /**
7267
- * Directive for conditional rendering in the virtual DOM.
7268
- * This directive renders an element based on a boolean expression, but only if preceding v-if or v-else-if directives were false.
7269
- * For example:
7270
- * <div v-else-if="isAlternativeVisible">This div is conditionally rendered.</div>
7271
- * The element and its children are included in the DOM only if the expression evaluates to true AND no preceding condition was met.
7272
- * This directive must be used after a v-if or another v-else-if directive.
7273
- */
7274
- class VElseIfDirective extends VConditionalDirective {
7275
- /**
7276
- * @param context The context for parsing the directive.
7277
- */
7278
- constructor(context) {
7279
- super(context);
7280
- }
7281
- /**
7282
- * @inheritdoc
7283
- */
7284
- get name() {
7285
- return StandardDirectiveName.V_ELSE_IF;
7286
- }
7287
- }
7288
-
7289
- // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7290
- class BindingsUtils {
7291
- /**
7292
- * Gets the identifiers that have changed between two sets of bindings.
7293
- * @param oldBindings The old set of bindings.
7294
- * @param newBindings The new set of bindings.
7295
- * @returns An array of identifiers that have changed.
7296
- */
7297
- static getChangedIdentifiers(oldBindings, newBindings) {
7298
- const changed = [];
7299
- for (const key of Object.keys(newBindings)) {
7300
- if (!Object.hasOwn(oldBindings, key) || oldBindings[key] !== newBindings[key]) {
7301
- changed.push(key);
7302
- }
7303
- }
7304
- for (const key of Object.keys(oldBindings)) {
7305
- if (!Object.hasOwn(newBindings, key)) {
7306
- changed.push(key);
7307
- }
7308
- }
7309
- return Array.from(new Set(changed));
7310
- }
7311
- }
7312
-
7313
- // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7314
- class VDirectiveManager {
7315
- /**
7316
- * The virtual node to which this directive handler is associated.
7287
+ * The virtual node to which this directive handler is associated.
7317
7288
  */
7318
7289
  #vNode;
7319
7290
  #directives;
@@ -7396,9 +7367,32 @@
7396
7367
  }
7397
7368
  #parseDirectives() {
7398
7369
  const element = this.#vNode.node;
7370
+ // Collect relevant attributes
7371
+ const attributes = [];
7372
+ if (element.hasAttribute(StandardDirectiveName.V_FOR)) {
7373
+ attributes.push(element.getAttributeNode(StandardDirectiveName.V_FOR));
7374
+ for (const attr of Array.from(element.attributes)) {
7375
+ if (['v-bind:key', ':key'].includes(attr.name)) {
7376
+ attributes.push(attr);
7377
+ break;
7378
+ }
7379
+ }
7380
+ }
7381
+ else if (element.hasAttribute(StandardDirectiveName.V_IF)) {
7382
+ attributes.push(element.getAttributeNode(StandardDirectiveName.V_IF));
7383
+ }
7384
+ else if (element.hasAttribute(StandardDirectiveName.V_ELSE_IF)) {
7385
+ attributes.push(element.getAttributeNode(StandardDirectiveName.V_ELSE_IF));
7386
+ }
7387
+ else if (element.hasAttribute(StandardDirectiveName.V_ELSE)) {
7388
+ attributes.push(element.getAttributeNode(StandardDirectiveName.V_ELSE));
7389
+ }
7390
+ else {
7391
+ attributes.push(...Array.from(element.attributes));
7392
+ }
7399
7393
  // Parse directives from attributes
7400
7394
  const directives = [];
7401
- for (const attribute of Array.from(element.attributes)) {
7395
+ for (const attribute of attributes) {
7402
7396
  // Create a context for parsing the directive
7403
7397
  const context = {
7404
7398
  vNode: this.#vNode,
@@ -7485,7 +7479,7 @@
7485
7479
  let result = text;
7486
7480
  evaluators.forEach((evaluator, i) => {
7487
7481
  // Gather the current values of the identifiers from the bindings
7488
- const values = evaluator.ids.map(id => bindings[id]);
7482
+ const values = evaluator.ids.map(id => bindings.get(id));
7489
7483
  // Evaluate the expression and replace {{...}} in the text
7490
7484
  result = result.replace(matches[i][0], String(evaluator.func(...values)));
7491
7485
  });
@@ -7517,6 +7511,11 @@
7517
7511
  }
7518
7512
 
7519
7513
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7514
+ /**
7515
+ * Represents a virtual node in the virtual DOM.
7516
+ * A virtual node corresponds to a real DOM node and contains additional information for data binding and directives.
7517
+ * This class is responsible for managing the state and behavior of the virtual node, including its bindings, directives, and child nodes.
7518
+ */
7520
7519
  class VNode {
7521
7520
  /**
7522
7521
  * The application instance associated with this virtual node.
@@ -7548,12 +7547,6 @@
7548
7547
  * The data bindings associated with this virtual node, if any.
7549
7548
  */
7550
7549
  #bindings;
7551
- /**
7552
- * The bindings preparer associated with this virtual node, if any.
7553
- * This is used to prepare the bindings before applying them to the DOM.
7554
- * This is optional and may be undefined if there are no preparers.
7555
- */
7556
- #bindingsPreparer;
7557
7550
  /**
7558
7551
  * An evaluator for text nodes that contain expressions in {{...}}.
7559
7552
  * This is used to dynamically update the text content based on data bindings.
@@ -7566,15 +7559,16 @@
7566
7559
  */
7567
7560
  #directiveManager;
7568
7561
  /**
7569
- * The list of dependencies for this virtual node.
7570
- * This is optional and may be undefined if there are no dependencies.
7562
+ * The list of dependents for this virtual node.
7563
+ * This is optional and may be undefined if there are no dependents.
7571
7564
  */
7572
- #dependencies;
7565
+ #dependents;
7573
7566
  /**
7574
7567
  * The list of identifiers for this virtual node.
7575
7568
  * This includes variable and function names used in expressions.
7569
+ * This is optional and may be undefined if there are no identifiers.
7576
7570
  */
7577
- #identifiers;
7571
+ #dependentIdentifiers;
7578
7572
  /**
7579
7573
  * The list of preparable identifiers for this virtual node.
7580
7574
  * This includes variable and function names used in directive bindings preparers.
@@ -7597,7 +7591,7 @@
7597
7591
  this.#nodeName = args.node.nodeName;
7598
7592
  this.#parentVNode = args.parentVNode;
7599
7593
  this.#bindings = args.bindings;
7600
- this.#bindingsPreparer = args.bindingsPreparer;
7594
+ this.#parentVNode?.addChild(this);
7601
7595
  // If the node is a text node, check for expressions and create a text evaluator
7602
7596
  if (this.#nodeType === Node.TEXT_NODE) {
7603
7597
  const text = this.#node;
@@ -7609,23 +7603,24 @@
7609
7603
  // If the node is an element, initialize directives and child nodes
7610
7604
  if (this.#nodeType === Node.ELEMENT_NODE && this.#node.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
7611
7605
  this.#node;
7612
- // Initialize directive manager
7613
- this.#directiveManager = new VDirectiveManager(this);
7614
7606
  // Initialize child virtual nodes
7615
7607
  this.#childVNodes = [];
7616
- // Recursively create VNode instances for child nodes
7617
- for (const childNode of Array.from(this.#node.childNodes)) {
7618
- this.#childVNodes.push(new VNode({
7619
- node: childNode,
7620
- vApplication: this.#vApplication,
7621
- parentVNode: this,
7622
- bindings: this.#bindings
7623
- }));
7608
+ // Initialize directive manager
7609
+ this.#directiveManager = new VDirectiveManager(this);
7610
+ // For non-v-for elements, recursively create VNode instances for child nodes
7611
+ if (!this.#directiveManager.directives?.some(d => d.templatize)) {
7612
+ for (const childNode of Array.from(this.#node.childNodes)) {
7613
+ new VNode({
7614
+ node: childNode,
7615
+ vApplication: this.#vApplication,
7616
+ parentVNode: this
7617
+ });
7618
+ }
7624
7619
  }
7625
7620
  }
7626
7621
  // If there is a parent virtual node, add this node as a dependency
7627
7622
  if (this.#parentVNode) {
7628
- this.#closers = this.#parentVNode.addDependency(this);
7623
+ this.#closers = this.#parentVNode.addDependent(this);
7629
7624
  }
7630
7625
  }
7631
7626
  /**
@@ -7700,7 +7695,10 @@
7700
7695
  * The data bindings associated with this virtual node, if any.
7701
7696
  */
7702
7697
  get bindings() {
7703
- return this.#bindings;
7698
+ if (this.#bindings) {
7699
+ return this.#bindings;
7700
+ }
7701
+ return this.#parentVNode?.bindings;
7704
7702
  }
7705
7703
  /**
7706
7704
  * The directive manager associated with this virtual node.
@@ -7737,28 +7735,28 @@
7737
7735
  * The list of identifiers for this virtual node.
7738
7736
  * This includes variable and function names used in expressions.
7739
7737
  */
7740
- get identifiers() {
7741
- // If already computed, return the cached identifiers
7742
- if (this.#identifiers) {
7743
- return this.#identifiers;
7738
+ get dependentIdentifiers() {
7739
+ // If already computed, return the cached dependent identifiers
7740
+ if (this.#dependentIdentifiers) {
7741
+ return this.#dependentIdentifiers;
7744
7742
  }
7745
7743
  // Collect identifiers from text evaluator and directives
7746
- const identifiers = [];
7744
+ const ids = [];
7747
7745
  // If this is a text node with a text evaluator, include its identifiers
7748
7746
  if (this.#textEvaluator) {
7749
- identifiers.push(...this.#textEvaluator.identifiers);
7747
+ ids.push(...this.#textEvaluator.identifiers);
7750
7748
  }
7751
7749
  // Include identifiers from directive bindings preparers
7752
7750
  this.#directiveManager?.bindingsPreparers?.forEach(preparer => {
7753
- identifiers.push(...preparer.identifiers);
7751
+ ids.push(...preparer.dependentIdentifiers);
7754
7752
  });
7755
7753
  // Include identifiers from directive DOM updaters
7756
7754
  this.#directiveManager?.domUpdaters?.forEach(updater => {
7757
- identifiers.push(...updater.identifiers);
7755
+ ids.push(...updater.dependentIdentifiers);
7758
7756
  });
7759
7757
  // Remove duplicates by converting to a Set and back to an array
7760
- this.#identifiers = identifiers.length === 0 ? [] : [...new Set(identifiers)];
7761
- return this.#identifiers;
7758
+ this.#dependentIdentifiers = [...new Set(ids)];
7759
+ return this.#dependentIdentifiers;
7762
7760
  }
7763
7761
  get preparableIdentifiers() {
7764
7762
  // If already computed, return the cached preparable identifiers
@@ -7767,8 +7765,6 @@
7767
7765
  }
7768
7766
  // Collect preparable identifiers from directive bindings preparers
7769
7767
  const preparableIdentifiers = [];
7770
- // Include preparable identifiers from this node's bindings preparer, if any
7771
- preparableIdentifiers.push(...(this.#bindingsPreparer?.preparableIdentifiers ?? []));
7772
7768
  // Include preparable identifiers from directive bindings preparers
7773
7769
  this.#directiveManager?.bindingsPreparers?.forEach(preparer => {
7774
7770
  preparableIdentifiers.push(...preparer.preparableIdentifiers);
@@ -7782,129 +7778,128 @@
7782
7778
  * This method evaluates any expressions in text nodes and applies effectors from directives.
7783
7779
  * It also recursively updates child virtual nodes.
7784
7780
  * @param context The context for the update operation.
7781
+ * This includes the current bindings and a list of identifiers that have changed.
7785
7782
  */
7786
- update(context) {
7787
- // Extract context properties
7788
- const { bindings, changedIdentifiers, isInitial } = context;
7783
+ update() {
7784
+ const changes = this.bindings?.changes || [];
7789
7785
  // If this is a text node with a text evaluator, update its content if needed
7790
7786
  if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
7791
- if (isInitial) {
7792
- // Initial update: always set the text content
7787
+ // Check if any of the identifiers are in the changed identifiers
7788
+ const changed = this.#textEvaluator.identifiers.some(id => changes.includes(id));
7789
+ // If the text node has changed, update its content
7790
+ if (changed) {
7793
7791
  const text = this.#node;
7794
- text.data = this.#textEvaluator.evaluate(bindings);
7792
+ text.data = this.#textEvaluator.evaluate(this.bindings);
7795
7793
  }
7796
- else {
7797
- // Subsequent update: only update the text content if relevant identifiers have changed
7798
- // Check if any of the identifiers are in the changed identifiers
7799
- const changed = this.#textEvaluator.identifiers.some(id => changedIdentifiers.includes(id));
7800
- // If the text node has changed, update its content
7794
+ return;
7795
+ }
7796
+ // Prepare new bindings using directive bindings preparers, if any
7797
+ if (this.#directiveManager?.bindingsPreparers) {
7798
+ // Ensure local bindings are initialized
7799
+ if (!this.#bindings) {
7800
+ this.#bindings = new VBindings({ parent: this.bindings });
7801
+ }
7802
+ // Prepare bindings for each preparer if relevant identifiers have changed
7803
+ for (const preparer of this.#directiveManager.bindingsPreparers) {
7804
+ const changed = preparer.dependentIdentifiers.some(id => changes.includes(id));
7801
7805
  if (changed) {
7802
- const text = this.#node;
7803
- text.data = this.#textEvaluator.evaluate(bindings);
7806
+ preparer.prepareBindings();
7804
7807
  }
7805
7808
  }
7806
- return;
7807
7809
  }
7808
- if (isInitial) {
7809
- // Initial update: prepare bindings and apply all directive updaters
7810
- if (this.#directiveManager?.domUpdaters) {
7811
- for (const updater of this.#directiveManager.domUpdaters) {
7810
+ // Apply DOM updaters from directives, if any
7811
+ if (this.#directiveManager?.domUpdaters) {
7812
+ for (const updater of this.#directiveManager.domUpdaters) {
7813
+ const changed = updater.dependentIdentifiers.some(id => changes.includes(id));
7814
+ if (changed) {
7812
7815
  updater.applyToDOM();
7813
7816
  }
7814
7817
  }
7815
- // Recursively update dependent virtual nodes
7816
- this.#dependencies?.forEach(dependentNode => {
7817
- // Update the dependent node
7818
- dependentNode.update({
7819
- bindings: this.#bindings,
7820
- changedIdentifiers: [],
7821
- isInitial: true
7822
- });
7823
- });
7824
7818
  }
7825
- else {
7826
- // Subsequent update: only prepare bindings and apply directive updaters if relevant identifiers have changed
7827
- // Save the original bindings for comparison
7828
- const oldBindings = this.#bindings;
7829
- // Prepare new bindings using directive bindings preparers, if any
7830
- const newBindings = { ...bindings };
7831
- const changes = new Set([...changedIdentifiers]);
7832
- this.#bindingsPreparer?.prepareBindings(newBindings);
7833
- if (this.#directiveManager?.bindingsPreparers) {
7834
- for (const preparer of this.#directiveManager.bindingsPreparers) {
7835
- const changed = preparer.identifiers.some(id => changedIdentifiers.includes(id));
7836
- if (changed) {
7837
- preparer.prepareBindings(newBindings);
7838
- }
7839
- }
7819
+ // Recursively update dependent virtual nodes
7820
+ this.#dependents?.forEach(dependentNode => {
7821
+ const changed = dependentNode.dependentIdentifiers.some(id => changes.includes(id));
7822
+ if (changed) {
7823
+ dependentNode.update();
7840
7824
  }
7841
- BindingsUtils.getChangedIdentifiers(oldBindings, newBindings).forEach(id => changes.add(id));
7842
- // Update the bindings for this node
7843
- this.#bindings = newBindings;
7844
- // If there are no changes in bindings, skip further processing
7845
- if (changes.size === 0) {
7846
- return;
7825
+ });
7826
+ }
7827
+ /**
7828
+ * Forces an update of the virtual node and its children, regardless of changed identifiers.
7829
+ * This method evaluates any expressions in text nodes and applies effectors from directives.
7830
+ * It also recursively updates child virtual nodes.
7831
+ * This is useful when an immediate update is needed, bypassing the usual change detection.
7832
+ */
7833
+ forceUpdate() {
7834
+ // If this is a text node with a text evaluator, update its content if needed
7835
+ if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
7836
+ const text = this.#node;
7837
+ text.data = this.#textEvaluator.evaluate(this.bindings);
7838
+ return;
7839
+ }
7840
+ // Prepare new bindings using directive bindings preparers, if any
7841
+ if (this.#directiveManager?.bindingsPreparers) {
7842
+ // Ensure local bindings are initialized
7843
+ if (!this.#bindings) {
7844
+ this.#bindings = new VBindings({ parent: this.bindings });
7847
7845
  }
7848
- // Apply DOM updaters from directives, if any
7849
- if (this.#directiveManager?.domUpdaters) {
7850
- for (const updater of this.#directiveManager.domUpdaters) {
7851
- const changed = updater.identifiers.some(id => changes.has(id));
7852
- if (changed) {
7853
- updater.applyToDOM();
7854
- }
7855
- }
7846
+ // Prepare bindings for each preparer if relevant identifiers have changed
7847
+ for (const preparer of this.#directiveManager.bindingsPreparers) {
7848
+ preparer.prepareBindings();
7856
7849
  }
7857
- // Recursively update dependent virtual nodes
7858
- this.#dependencies?.forEach(dependentNode => {
7859
- // Check if any of the dependent node's identifiers are in the changed identifiers
7860
- if (dependentNode.identifiers.filter(id => changes.has(id)).length === 0) {
7861
- return;
7862
- }
7863
- // Update the dependent node
7864
- dependentNode.update({
7865
- bindings: this.#bindings,
7866
- changedIdentifiers: Array.from(changes),
7867
- });
7868
- });
7869
7850
  }
7851
+ // Apply DOM updaters from directives, if any
7852
+ if (this.#directiveManager?.domUpdaters) {
7853
+ for (const updater of this.#directiveManager.domUpdaters) {
7854
+ updater.applyToDOM();
7855
+ }
7856
+ }
7857
+ // Recursively update child virtual nodes
7858
+ this.#childVNodes?.forEach(childVNode => {
7859
+ childVNode.forceUpdate();
7860
+ });
7870
7861
  }
7871
7862
  /**
7872
- * Adds a dependency on the specified virtual node.
7873
- * This means that if the specified node's bindings change, this node may need to be updated.
7874
- * @param dependentNode The virtual node to add as a dependency.
7863
+ * Adds a child virtual node to this virtual node.
7864
+ * @param child The child virtual node to add.
7865
+ */
7866
+ addChild(child) {
7867
+ this.#childVNodes?.push(child);
7868
+ }
7869
+ /**
7870
+ * Adds a dependent virtual node that relies on this node's bindings.
7871
+ * @param dependent The dependent virtual node to add.
7875
7872
  * @returns A list of closers to unregister the dependency, or undefined if no dependency was added.
7876
7873
  */
7877
- addDependency(dependentNode) {
7878
- // List of closers to unregister dependencies
7874
+ addDependent(dependent) {
7875
+ // List of closers to unregister the dependency
7879
7876
  const closers = [];
7880
7877
  // Check if any of the dependent node's identifiers are in this node's identifiers
7881
- let hasIdentifier = dependentNode.identifiers.some(id => this.preparableIdentifiers.includes(id));
7878
+ let hasIdentifier = dependent.dependentIdentifiers.some(id => this.preparableIdentifiers.includes(id));
7882
7879
  if (!hasIdentifier) {
7883
- if (!this.#parentVNode) {
7884
- hasIdentifier = dependentNode.identifiers.some(id => this.#vApplication.preparableIdentifiers.includes(id));
7885
- }
7880
+ hasIdentifier = dependent.dependentIdentifiers.some(id => this.#bindings?.has(id, false) ?? false);
7886
7881
  }
7887
7882
  // If the dependent node has an identifier in this node's identifiers, add it as a dependency
7888
7883
  if (hasIdentifier) {
7889
7884
  // If the dependencies list is not initialized, create it
7890
- if (!this.#dependencies) {
7891
- this.#dependencies = [];
7885
+ if (!this.#dependents) {
7886
+ this.#dependents = [];
7892
7887
  }
7893
7888
  // Add the dependent node to the list
7894
- this.#dependencies.push(dependentNode);
7889
+ this.#dependents.push(dependent);
7895
7890
  // Create a closer to unregister the dependency
7896
7891
  closers.push({
7897
7892
  close: () => {
7898
7893
  // Remove the dependent node from the dependencies list
7899
- const index = this.#dependencies?.indexOf(dependentNode) ?? -1;
7894
+ const index = this.#dependents?.indexOf(dependent) ?? -1;
7900
7895
  if (index !== -1) {
7901
- this.#dependencies?.splice(index, 1);
7896
+ this.#dependents?.splice(index, 1);
7902
7897
  }
7903
7898
  }
7904
7899
  });
7905
7900
  }
7906
7901
  // Recursively add the dependency to the parent node, if any
7907
- this.#parentVNode?.addDependency(dependentNode)?.forEach(closer => closers.push(closer));
7902
+ this.#parentVNode?.addDependent(dependent)?.forEach(closer => closers.push(closer));
7908
7903
  // Return a closer to unregister the dependency
7909
7904
  return closers.length > 0 ? closers : undefined;
7910
7905
  }
@@ -7940,6 +7935,333 @@
7940
7935
  }
7941
7936
  }
7942
7937
 
7938
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7939
+ /**
7940
+ * Context for managing related conditional directives (v-if, v-else-if, v-else).
7941
+ */
7942
+ class VConditionalDirectiveContext {
7943
+ /**
7944
+ * A list of directives (v-if, v-else-if, v-else) in the order they appear in the template.
7945
+ */
7946
+ #directives = [];
7947
+ /**
7948
+ * A cached list of all variable and function names used in the expressions of the associated directives.
7949
+ */
7950
+ #allDependentIdentifiers = [];
7951
+ /**
7952
+ * Gets a list of all variable and function names used in the expressions of the associated directives.
7953
+ * This is useful for determining dependencies for re-evaluation when data changes.
7954
+ */
7955
+ get allDependentIdentifiers() {
7956
+ return this.#allDependentIdentifiers;
7957
+ }
7958
+ /**
7959
+ * Adds a directive (v-else-if or v-else) to the conditional context.
7960
+ * @param directive The directive to add.
7961
+ */
7962
+ addDirective(directive) {
7963
+ this.#directives.push(directive);
7964
+ // Update the cached list of all dependent identifiers
7965
+ if (directive.dependentIdentifiers) {
7966
+ for (const id of directive.dependentIdentifiers) {
7967
+ if (!this.#allDependentIdentifiers.includes(id)) {
7968
+ this.#allDependentIdentifiers.push(id);
7969
+ }
7970
+ }
7971
+ }
7972
+ }
7973
+ /**
7974
+ * Checks if any preceding directive's condition is met.
7975
+ * This is used to determine if a v-else-if or v-else directive should be rendered.
7976
+ * @param directive The directive to check against.
7977
+ * @returns True if any preceding directive's condition is met, otherwise false.
7978
+ */
7979
+ isPrecedingConditionMet(directive) {
7980
+ const index = this.#directives.indexOf(directive);
7981
+ if (index === -1) {
7982
+ throw new Error("Directive not found in context.");
7983
+ }
7984
+ // Check if all previous directives are met
7985
+ for (let i = 0; i < index; i++) {
7986
+ const d = this.#directives[i];
7987
+ if (d.conditionIsMet === true) {
7988
+ return true;
7989
+ }
7990
+ }
7991
+ return false;
7992
+ }
7993
+ }
7994
+
7995
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7996
+ class VConditionalDirective {
7997
+ /**
7998
+ * The virtual node to which this directive is applied.
7999
+ */
8000
+ #vNode;
8001
+ /**
8002
+ * A list of variable and function names used in the directive's expression.
8003
+ * This may be undefined if the directive does not have an expression (e.g., v-else).
8004
+ */
8005
+ #dependentIdentifiers;
8006
+ /*
8007
+ * A function that evaluates the directive's condition.
8008
+ * It returns true if the condition is met, otherwise false.
8009
+ * This may be undefined if the directive does not have an expression (e.g., v-else).
8010
+ */
8011
+ #evaluate;
8012
+ /**
8013
+ * The context for managing related conditional directives (v-if, v-else-if, v-else).
8014
+ */
8015
+ #conditionalContext;
8016
+ /**
8017
+ * The currently rendered virtual node, if any.
8018
+ */
8019
+ #renderedVNode;
8020
+ /**
8021
+ * @param context The context for parsing the directive.
8022
+ */
8023
+ constructor(context) {
8024
+ this.#vNode = context.vNode;
8025
+ // Parse the expression to extract identifiers and create the evaluator
8026
+ const expression = context.attribute.value;
8027
+ if (expression) {
8028
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
8029
+ this.#evaluate = this.#createEvaluator(expression);
8030
+ }
8031
+ // Remove the directive attribute from the element
8032
+ this.#vNode.node.removeAttribute(context.attribute.name);
8033
+ // Initialize the conditional context for managing related directives
8034
+ this.#conditionalContext = this.#initializeConditionalContext();
8035
+ this.#conditionalContext.addDirective(this);
8036
+ }
8037
+ /**
8038
+ * @inheritdoc
8039
+ */
8040
+ get vNode() {
8041
+ return this.#vNode;
8042
+ }
8043
+ /**
8044
+ * @inheritdoc
8045
+ */
8046
+ get needsAnchor() {
8047
+ return true;
8048
+ }
8049
+ /**
8050
+ * @inheritdoc
8051
+ */
8052
+ get bindingsPreparer() {
8053
+ return undefined;
8054
+ }
8055
+ /**
8056
+ * @inheritdoc
8057
+ */
8058
+ get domUpdater() {
8059
+ const identifiers = this.#conditionalContext.allDependentIdentifiers;
8060
+ const render = () => this.#render();
8061
+ // Create an updater that handles the conditional rendering
8062
+ const updater = {
8063
+ get dependentIdentifiers() {
8064
+ return identifiers;
8065
+ },
8066
+ applyToDOM() {
8067
+ render();
8068
+ }
8069
+ };
8070
+ return updater;
8071
+ }
8072
+ /**
8073
+ * @inheritdoc
8074
+ */
8075
+ get templatize() {
8076
+ return true;
8077
+ }
8078
+ /**
8079
+ * @inheritdoc
8080
+ */
8081
+ get dependentIdentifiers() {
8082
+ return this.#dependentIdentifiers ?? [];
8083
+ }
8084
+ /**
8085
+ * The context for managing related conditional directives (v-if, v-else-if, v-else).
8086
+ */
8087
+ get conditionalContext() {
8088
+ return this.#conditionalContext;
8089
+ }
8090
+ /**
8091
+ * Indicates whether the condition for this directive is currently met.
8092
+ * For v-if and v-else-if, this depends on the evaluation of their expressions.
8093
+ * For v-else, this is always true.
8094
+ */
8095
+ get conditionIsMet() {
8096
+ if (!this.#evaluate) {
8097
+ // No expression means always true (e.g., v-else)
8098
+ return true;
8099
+ }
8100
+ return this.#evaluate();
8101
+ }
8102
+ /**
8103
+ * @inheritdoc
8104
+ */
8105
+ destroy() {
8106
+ // Default implementation does nothing. Override in subclasses if needed.
8107
+ }
8108
+ /**
8109
+ * Renders the node based on the evaluation of the directive's condition.
8110
+ * Inserts or removes the node from the DOM as needed.
8111
+ */
8112
+ #render() {
8113
+ // Check if any preceding directive's condition is met
8114
+ if (this.#conditionalContext.isPrecedingConditionMet(this)) {
8115
+ // Previous condition met, ensure node is removed
8116
+ this.#removedNode();
8117
+ return;
8118
+ }
8119
+ if (!this.#evaluate) {
8120
+ // No expression means always true (e.g., v-else)
8121
+ this.#insertNode();
8122
+ return;
8123
+ }
8124
+ // Evaluate the condition and insert or remove the node accordingly
8125
+ const shouldRender = this.#evaluate();
8126
+ if (shouldRender) {
8127
+ this.#insertNode();
8128
+ }
8129
+ else {
8130
+ this.#removedNode();
8131
+ }
8132
+ }
8133
+ /**
8134
+ * Inserts the node into the DOM at the position marked by the anchor node, if any.
8135
+ * If there is no anchor node, the node is inserted as a child of its parent node.
8136
+ * If the node is already in the DOM, no action is taken.
8137
+ */
8138
+ #insertNode() {
8139
+ if (this.#renderedVNode) {
8140
+ // Already rendered, no action needed
8141
+ return;
8142
+ }
8143
+ this.#renderedVNode = this.#cloneTemplate();
8144
+ this.#vNode.anchorNode?.parentNode?.insertBefore(this.#renderedVNode.node, this.#vNode.anchorNode.nextSibling);
8145
+ this.#renderedVNode.forceUpdate();
8146
+ }
8147
+ /**
8148
+ * Removes the node from the DOM.
8149
+ * If the node is not in the DOM, no action is taken.
8150
+ */
8151
+ #removedNode() {
8152
+ if (!this.#renderedVNode) {
8153
+ // Not rendered, no action needed
8154
+ return;
8155
+ }
8156
+ this.#renderedVNode.node.parentNode?.removeChild(this.#renderedVNode.node);
8157
+ this.#renderedVNode.destroy();
8158
+ this.#renderedVNode = undefined;
8159
+ }
8160
+ /**
8161
+ * Clones the template element and creates a new VNode for the cloned element.
8162
+ */
8163
+ #cloneTemplate() {
8164
+ const element = this.#vNode.node;
8165
+ const clone = element.cloneNode(true);
8166
+ // Create a new VNode for the cloned element
8167
+ const vNode = new VNode({
8168
+ node: clone,
8169
+ vApplication: this.#vNode.vApplication,
8170
+ parentVNode: this.#vNode.parentVNode
8171
+ });
8172
+ return vNode;
8173
+ }
8174
+ /**
8175
+ * Creates a function to evaluate the directive's condition.
8176
+ * @param expression The expression string to evaluate.
8177
+ * @returns A function that evaluates the directive's condition.
8178
+ */
8179
+ #createEvaluator(expression) {
8180
+ const identifiers = this.#dependentIdentifiers ?? [];
8181
+ const args = identifiers.join(", ");
8182
+ const funcBody = `return (${expression});`;
8183
+ // Create a dynamic function with the identifiers as parameters
8184
+ const func = new Function(args, funcBody);
8185
+ // Return a function that calls the dynamic function with the current values from the virtual node's bindings
8186
+ return () => {
8187
+ // Gather the current values of the identifiers from the bindings
8188
+ const values = identifiers.map(id => this.#vNode.bindings?.get(id));
8189
+ // Call the dynamic function with the gathered values and return the result as a boolean
8190
+ return Boolean(func(...values));
8191
+ };
8192
+ }
8193
+ /**
8194
+ * Initializes the conditional context for managing related directives.
8195
+ */
8196
+ #initializeConditionalContext() {
8197
+ // Create a new context if this is a v-if directive
8198
+ if (this.name === StandardDirectiveName.V_IF) {
8199
+ return new VConditionalDirectiveContext();
8200
+ }
8201
+ // Link to the existing conditional context from the preceding v-if or v-else-if directive
8202
+ let prevVNode = this.vNode.previousSibling;
8203
+ while (prevVNode && prevVNode.nodeType !== Node.ELEMENT_NODE) {
8204
+ prevVNode = prevVNode.previousSibling;
8205
+ }
8206
+ const precedingDirective = prevVNode?.directiveManager?.directives?.find(d => d.name === StandardDirectiveName.V_IF || d.name === StandardDirectiveName.V_ELSE_IF);
8207
+ if (!precedingDirective) {
8208
+ throw new Error("preceding v-if or v-else-if directive not found.");
8209
+ }
8210
+ // Cast to VConditionalDirective to access conditionalContext
8211
+ const conditionalContext = precedingDirective.conditionalContext;
8212
+ return conditionalContext;
8213
+ }
8214
+ }
8215
+
8216
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
8217
+ /**
8218
+ * Directive for conditional rendering in the virtual DOM.
8219
+ * This directive renders an element if the preceding v-if or v-else-if directive evaluated to false.
8220
+ * For example:
8221
+ * <div v-else>This div is rendered if the previous v-if or v-else-if was false.</div>
8222
+ * The element and its children are included in the DOM only if the preceding v-if or v-else-if expression evaluates to false.
8223
+ * If the preceding expression is true, this element and its children are not rendered.
8224
+ * This directive must be used immediately after a v-if or v-else-if directive.
8225
+ */
8226
+ class VElseDirective extends VConditionalDirective {
8227
+ /**
8228
+ * @param context The context for parsing the directive.
8229
+ */
8230
+ constructor(context) {
8231
+ super(context);
8232
+ }
8233
+ /**
8234
+ * @inheritdoc
8235
+ */
8236
+ get name() {
8237
+ return StandardDirectiveName.V_ELSE;
8238
+ }
8239
+ }
8240
+
8241
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
8242
+ /**
8243
+ * Directive for conditional rendering in the virtual DOM.
8244
+ * This directive renders an element based on a boolean expression, but only if preceding v-if or v-else-if directives were false.
8245
+ * For example:
8246
+ * <div v-else-if="isAlternativeVisible">This div is conditionally rendered.</div>
8247
+ * The element and its children are included in the DOM only if the expression evaluates to true AND no preceding condition was met.
8248
+ * This directive must be used after a v-if or another v-else-if directive.
8249
+ */
8250
+ class VElseIfDirective extends VConditionalDirective {
8251
+ /**
8252
+ * @param context The context for parsing the directive.
8253
+ */
8254
+ constructor(context) {
8255
+ super(context);
8256
+ }
8257
+ /**
8258
+ * @inheritdoc
8259
+ */
8260
+ get name() {
8261
+ return StandardDirectiveName.V_ELSE_IF;
8262
+ }
8263
+ }
8264
+
7943
8265
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
7944
8266
  /**
7945
8267
  * Directive for rendering a list of items using a loop.
@@ -7964,7 +8286,7 @@
7964
8286
  /**
7965
8287
  * A list of variable and function names used in the directive's expression.
7966
8288
  */
7967
- #identifiers;
8289
+ #dependentIdentifiers;
7968
8290
  /**
7969
8291
  * A function that evaluates the directive's expression to get the source data.
7970
8292
  * It returns the collection to iterate over.
@@ -8002,7 +8324,7 @@
8002
8324
  this.#indexName = parsed.indexName;
8003
8325
  this.#sourceName = parsed.sourceName;
8004
8326
  // Extract identifiers from the source expression
8005
- this.#identifiers = ExpressionUtils.extractIdentifiers(parsed.sourceName, context.vNode.vApplication.functionDependencies);
8327
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(parsed.sourceName, context.vNode.vApplication.functionDependencies);
8006
8328
  this.#evaluateSource = this.#createSourceEvaluator(parsed.sourceName);
8007
8329
  }
8008
8330
  // Remove the directive attribute from the element
@@ -8036,11 +8358,11 @@
8036
8358
  * @inheritdoc
8037
8359
  */
8038
8360
  get domUpdater() {
8039
- const identifiers = this.#identifiers ?? [];
8361
+ const identifiers = this.#dependentIdentifiers ?? [];
8040
8362
  const render = () => this.#render();
8041
8363
  // Create and return the DOM updater
8042
8364
  const updater = {
8043
- get identifiers() {
8365
+ get dependentIdentifiers() {
8044
8366
  return identifiers;
8045
8367
  },
8046
8368
  applyToDOM() {
@@ -8049,6 +8371,18 @@
8049
8371
  };
8050
8372
  return updater;
8051
8373
  }
8374
+ /**
8375
+ * @inheritdoc
8376
+ */
8377
+ get templatize() {
8378
+ return true;
8379
+ }
8380
+ /**
8381
+ * @inheritdoc
8382
+ */
8383
+ get dependentIdentifiers() {
8384
+ return this.#dependentIdentifiers ?? [];
8385
+ }
8052
8386
  /**
8053
8387
  * @inheritdoc
8054
8388
  */
@@ -8085,12 +8419,9 @@
8085
8419
  if (this.#evaluateKey && this.#itemName) {
8086
8420
  iterations = iterations.map(iter => {
8087
8421
  // Create bindings for this iteration
8088
- const itemBindings = new Map();
8089
- if (this.#vNode.bindings) {
8090
- for (const key in this.#vNode.bindings) {
8091
- itemBindings.set(key, this.#vNode.bindings[key]);
8092
- }
8093
- }
8422
+ const itemBindings = new VBindings({
8423
+ parent: this.#vNode.bindings
8424
+ });
8094
8425
  itemBindings.set(this.#itemName, iter.item);
8095
8426
  if (this.#indexName) {
8096
8427
  itemBindings.set(this.#indexName, iter.index);
@@ -8142,11 +8473,7 @@
8142
8473
  else {
8143
8474
  parent.appendChild(vNode.node);
8144
8475
  }
8145
- vNode.update({
8146
- bindings: this.#vNode.bindings || {},
8147
- changedIdentifiers: [],
8148
- isInitial: true
8149
- });
8476
+ vNode.forceUpdate();
8150
8477
  }
8151
8478
  else {
8152
8479
  // Reuse existing item
@@ -8197,12 +8524,12 @@
8197
8524
  * Creates a function to evaluate the source data expression.
8198
8525
  */
8199
8526
  #createSourceEvaluator(expression) {
8200
- const identifiers = this.#identifiers ?? [];
8527
+ const identifiers = this.#dependentIdentifiers ?? [];
8201
8528
  const args = identifiers.join(", ");
8202
8529
  const funcBody = `return (${expression});`;
8203
8530
  const func = new Function(args, funcBody);
8204
8531
  return () => {
8205
- const values = identifiers.map(id => this.#vNode.bindings?.[id]);
8532
+ const values = identifiers.map(id => this.#vNode.bindings?.get(id));
8206
8533
  return func(...values);
8207
8534
  };
8208
8535
  }
@@ -8227,80 +8554,38 @@
8227
8554
  const element = this.#vNode.node;
8228
8555
  const clone = element.cloneNode(true);
8229
8556
  // Prepare identifiers for the item
8230
- const itemName = this.#itemName;
8231
- const indexName = this.#indexName;
8557
+ this.#itemName;
8558
+ this.#indexName;
8232
8559
  // Create bindings for this iteration
8233
- const bindings = { ...this.#vNode.bindings };
8560
+ const bindings = new VBindings({
8561
+ parent: this.#vNode.bindings
8562
+ });
8234
8563
  if (this.#itemName) {
8235
- bindings[this.#itemName] = context.item;
8564
+ bindings.set(this.#itemName, context.item);
8236
8565
  }
8237
8566
  if (this.#indexName) {
8238
- bindings[this.#indexName] = context.index;
8567
+ bindings.set(this.#indexName, context.index);
8239
8568
  }
8240
- const itemBindingsPreparer = {
8241
- get identifiers() {
8242
- return []; // No specific identifiers for item
8243
- },
8244
- get preparableIdentifiers() {
8245
- // Return item and index names if defined
8246
- const ids = [];
8247
- if (itemName)
8248
- ids.push(itemName);
8249
- if (indexName)
8250
- ids.push(indexName);
8251
- return ids;
8252
- },
8253
- prepareBindings(bindings) {
8254
- // Prepare bindings for the current item
8255
- if (itemName) {
8256
- bindings[itemName] = context.item;
8257
- }
8258
- if (indexName) {
8259
- bindings[indexName] = context.index;
8260
- }
8261
- }
8262
- };
8263
8569
  // Create a new VNode for the cloned element
8264
8570
  const vNode = new VNode({
8265
8571
  node: clone,
8266
8572
  vApplication: this.#vNode.vApplication,
8267
8573
  parentVNode: this.#vNode.parentVNode,
8268
- bindings,
8269
- bindingsPreparer: itemBindingsPreparer,
8574
+ bindings
8270
8575
  });
8271
- // Set data attributes for debugging
8272
- clone.setAttribute('data-v-for-key', String(context.key));
8273
- clone.setAttribute('data-v-for-index', String(context.index));
8274
8576
  return vNode;
8275
8577
  }
8276
8578
  /**
8277
8579
  * Update bindings for an existing item
8278
8580
  */
8279
8581
  #updateItemBindings(vNode, context) {
8280
- const bindings = vNode.bindings || {};
8281
- const updatedBindings = { ...bindings };
8282
8582
  if (this.#itemName) {
8283
- updatedBindings[this.#itemName] = context.item;
8583
+ vNode.bindings?.set(this.#itemName, context.item);
8284
8584
  }
8285
8585
  if (this.#indexName) {
8286
- updatedBindings[this.#indexName] = context.index;
8287
- }
8288
- // Update data attributes
8289
- const element = vNode.node;
8290
- element.setAttribute('data-v-for-key', String(context.key));
8291
- element.setAttribute('data-v-for-index', String(context.index));
8292
- // Trigger reactivity update by calling update with the new bindings
8293
- const changedIdentifiers = [];
8294
- if (this.#itemName) {
8295
- changedIdentifiers.push(this.#itemName);
8296
- }
8297
- if (this.#indexName) {
8298
- changedIdentifiers.push(this.#indexName);
8586
+ vNode.bindings?.set(this.#indexName, context.index);
8299
8587
  }
8300
- vNode.update({
8301
- bindings: updatedBindings,
8302
- changedIdentifiers
8303
- });
8588
+ vNode.update();
8304
8589
  }
8305
8590
  /**
8306
8591
  * Get iterations from various data types
@@ -8388,7 +8673,7 @@
8388
8673
  /**
8389
8674
  * A list of variable and function names used in the directive's expression.
8390
8675
  */
8391
- #identifiers;
8676
+ #dependentIdentifiers;
8392
8677
  /**
8393
8678
  * A function that evaluates the directive's expression.
8394
8679
  * It returns the evaluated value of the expression.
@@ -8422,7 +8707,7 @@
8422
8707
  const expression = context.attribute.value;
8423
8708
  if (expression) {
8424
8709
  this.#expression = expression;
8425
- this.#identifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
8710
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
8426
8711
  this.#evaluate = this.#createEvaluator(expression);
8427
8712
  // Attach event listener for two-way binding
8428
8713
  this.#attachEventListener();
@@ -8458,11 +8743,11 @@
8458
8743
  * @inheritdoc
8459
8744
  */
8460
8745
  get domUpdater() {
8461
- const identifiers = this.#identifiers ?? [];
8746
+ const identifiers = this.#dependentIdentifiers ?? [];
8462
8747
  const render = () => this.#render();
8463
8748
  // Create and return the DOM updater
8464
8749
  const updater = {
8465
- get identifiers() {
8750
+ get dependentIdentifiers() {
8466
8751
  return identifiers;
8467
8752
  },
8468
8753
  applyToDOM() {
@@ -8471,6 +8756,18 @@
8471
8756
  };
8472
8757
  return updater;
8473
8758
  }
8759
+ /**
8760
+ * @inheritdoc
8761
+ */
8762
+ get templatize() {
8763
+ return false;
8764
+ }
8765
+ /**
8766
+ * @inheritdoc
8767
+ */
8768
+ get dependentIdentifiers() {
8769
+ return this.#dependentIdentifiers ?? [];
8770
+ }
8474
8771
  /**
8475
8772
  * @inheritdoc
8476
8773
  */
@@ -8541,8 +8838,6 @@
8541
8838
  newValue = this.#applyModifiers(newValue);
8542
8839
  // Update the binding
8543
8840
  this.#updateBinding(newValue);
8544
- // Schedule a DOM update
8545
- this.#vNode.vApplication.scheduleUpdate();
8546
8841
  };
8547
8842
  element.addEventListener(eventName, this.#listener);
8548
8843
  }
@@ -8599,21 +8894,12 @@
8599
8894
  if (!this.#expression) {
8600
8895
  return;
8601
8896
  }
8602
- const bindings = this.#vNode.vApplication.bindings;
8603
- if (!bindings) {
8604
- return;
8605
- }
8606
- // Simple property assignment (e.g., "message")
8607
- // For now, only support simple identifiers
8608
- const trimmed = this.#expression.trim();
8609
- if (trimmed && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(trimmed)) {
8610
- bindings[trimmed] = newValue;
8611
- }
8612
- else {
8613
- // For complex expressions like "user.name", we'd need more sophisticated parsing
8614
- this.#vNode.vApplication.logManager.getLogger('VModelDirective')
8615
- .warn(`v-model only supports simple identifiers for now: ${this.#expression}`);
8616
- }
8897
+ const expression = this.#expression.trim();
8898
+ const values = [newValue];
8899
+ const args = ['$newValue'].join(", ");
8900
+ const funcBody = `(this.${expression} = $newValue);`;
8901
+ const func = new Function(args, funcBody);
8902
+ func.call(this.#vNode.bindings?.raw, ...values);
8617
8903
  }
8618
8904
  /**
8619
8905
  * Creates a function to evaluate the directive's condition.
@@ -8621,15 +8907,15 @@
8621
8907
  * @returns A function that evaluates the directive's condition.
8622
8908
  */
8623
8909
  #createEvaluator(expression) {
8624
- const identifiers = this.#identifiers ?? [];
8910
+ const identifiers = this.#dependentIdentifiers ?? [];
8625
8911
  const args = identifiers.join(", ");
8626
8912
  const funcBody = `return (${expression});`;
8627
8913
  // Create a dynamic function with the identifiers as parameters
8628
8914
  const func = new Function(args, funcBody);
8629
- // Return a function that calls the dynamic function with the current values from the virtual node's bindings
8915
+ // Return a function that calls the dynamic function with the current values from bindings
8630
8916
  return () => {
8631
8917
  // Gather the current values of the identifiers from the bindings
8632
- const values = identifiers.map(id => this.#vNode.bindings?.[id]);
8918
+ const values = identifiers.map(id => this.#vNode.bindings?.get(id));
8633
8919
  // Call the dynamic function with the gathered values
8634
8920
  return func(...values);
8635
8921
  };
@@ -8658,7 +8944,7 @@
8658
8944
  /**
8659
8945
  * A list of variable and function names used in the directive's expression.
8660
8946
  */
8661
- #identifiers;
8947
+ #dependentIdentifiers;
8662
8948
  /**
8663
8949
  * The event handler wrapper function, generated once and reused.
8664
8950
  */
@@ -8697,7 +8983,7 @@
8697
8983
  // Parse the expression to extract identifiers and create the handler wrapper
8698
8984
  const expression = context.attribute.value;
8699
8985
  if (expression) {
8700
- this.#identifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
8986
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
8701
8987
  this.#handlerWrapper = this.#createHandlerWrapper(expression);
8702
8988
  }
8703
8989
  // Create and attach the event listener
@@ -8737,6 +9023,18 @@
8737
9023
  get domUpdater() {
8738
9024
  return undefined;
8739
9025
  }
9026
+ /**
9027
+ * @inheritdoc
9028
+ */
9029
+ get templatize() {
9030
+ return false;
9031
+ }
9032
+ /**
9033
+ * @inheritdoc
9034
+ */
9035
+ get dependentIdentifiers() {
9036
+ return this.#dependentIdentifiers ?? [];
9037
+ }
8740
9038
  /**
8741
9039
  * @inheritdoc
8742
9040
  */
@@ -8761,6 +9059,35 @@
8761
9059
  const isOnce = this.#modifiers.has('once');
8762
9060
  // Create the event listener function
8763
9061
  this.#listener = (event) => {
9062
+ // Check key modifiers for keyboard events
9063
+ if (event instanceof KeyboardEvent) {
9064
+ const keyModifiers = ['enter', 'tab', 'delete', 'esc', 'space', 'up', 'down', 'left', 'right'];
9065
+ const hasKeyModifier = keyModifiers.some(key => this.#modifiers.has(key));
9066
+ if (hasKeyModifier) {
9067
+ const keyMap = {
9068
+ 'enter': 'Enter',
9069
+ 'tab': 'Tab',
9070
+ 'delete': 'Delete',
9071
+ 'esc': 'Escape',
9072
+ 'space': ' ',
9073
+ 'up': 'ArrowUp',
9074
+ 'down': 'ArrowDown',
9075
+ 'left': 'ArrowLeft',
9076
+ 'right': 'ArrowRight'
9077
+ };
9078
+ let keyMatched = false;
9079
+ for (const [modifier, keyValue] of Object.entries(keyMap)) {
9080
+ if (this.#modifiers.has(modifier) && event.key === keyValue) {
9081
+ keyMatched = true;
9082
+ break;
9083
+ }
9084
+ }
9085
+ // If key modifier specified but key doesn't match, return early
9086
+ if (!keyMatched) {
9087
+ return;
9088
+ }
9089
+ }
9090
+ }
8764
9091
  // Apply event modifiers
8765
9092
  if (this.#modifiers.has('stop')) {
8766
9093
  event.stopPropagation();
@@ -8789,27 +9116,26 @@
8789
9116
  * @returns A function that handles the event.
8790
9117
  */
8791
9118
  #createHandlerWrapper(expression) {
8792
- const identifiers = this.#identifiers ?? [];
9119
+ const identifiers = this.#dependentIdentifiers ?? [];
8793
9120
  const vNode = this.#vNode;
8794
9121
  // Return a function that handles the event with proper scope
8795
9122
  return (event) => {
8796
- // Gather the current values of the identifiers from the bindings
8797
- const bindings = vNode.bindings ?? {};
9123
+ const bindings = vNode.bindings;
8798
9124
  // If the expression is just a method name, call it with bindings as 'this'
8799
9125
  const trimmedExpr = expression.trim();
8800
- if (identifiers.includes(trimmedExpr) && typeof bindings[trimmedExpr] === 'function') {
9126
+ if (identifiers.includes(trimmedExpr) && typeof bindings?.get(trimmedExpr) === 'function') {
8801
9127
  const methodName = trimmedExpr;
8802
- const originalMethod = bindings[methodName];
9128
+ const originalMethod = bindings?.get(methodName);
8803
9129
  // Call the method with bindings as 'this' context
8804
9130
  // This allows the method to access and modify bindings properties via 'this'
8805
- return originalMethod.call(bindings, event);
9131
+ return originalMethod(event);
8806
9132
  }
8807
9133
  // For inline expressions, evaluate normally
8808
- const values = identifiers.map(id => bindings[id]);
9134
+ const values = identifiers.map(id => vNode.bindings?.get(id));
8809
9135
  const args = identifiers.join(", ");
8810
9136
  const funcBody = `return (${expression});`;
8811
9137
  const func = new Function(args, funcBody);
8812
- return func(...values);
9138
+ return func.call(bindings?.raw, ...values, event);
8813
9139
  };
8814
9140
  }
8815
9141
  }
@@ -8833,7 +9159,7 @@
8833
9159
  /**
8834
9160
  * A list of variable and function names used in the directive's expression.
8835
9161
  */
8836
- #identifiers;
9162
+ #dependentIdentifiers;
8837
9163
  /*
8838
9164
  * A function that evaluates the directive's condition.
8839
9165
  * It returns true if the condition is met, otherwise false.
@@ -8850,7 +9176,7 @@
8850
9176
  this.#vNode = context.vNode;
8851
9177
  // Parse the expression to extract identifiers and create the evaluator
8852
9178
  const expression = context.attribute.value;
8853
- this.#identifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
9179
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
8854
9180
  this.#evaluate = this.#createEvaluator(expression);
8855
9181
  // Remove the directive attribute from the element
8856
9182
  this.#vNode.node.removeAttribute(context.attribute.name);
@@ -8886,13 +9212,13 @@
8886
9212
  * @inheritdoc
8887
9213
  */
8888
9214
  get domUpdater() {
8889
- const identifiers = this.#identifiers ?? [];
9215
+ const identifiers = this.#dependentIdentifiers ?? [];
8890
9216
  const evaluate = this.#evaluate;
8891
9217
  const visibleNode = () => this.visibleNode();
8892
9218
  const invisibleNode = () => this.invisibleNode();
8893
9219
  // Create an updater that handles the conditional rendering
8894
9220
  const updater = {
8895
- get identifiers() {
9221
+ get dependentIdentifiers() {
8896
9222
  return identifiers;
8897
9223
  },
8898
9224
  applyToDOM() {
@@ -8907,6 +9233,18 @@
8907
9233
  };
8908
9234
  return updater;
8909
9235
  }
9236
+ /**
9237
+ * @inheritdoc
9238
+ */
9239
+ get templatize() {
9240
+ return false;
9241
+ }
9242
+ /**
9243
+ * @inheritdoc
9244
+ */
9245
+ get dependentIdentifiers() {
9246
+ return this.#dependentIdentifiers ?? [];
9247
+ }
8910
9248
  /**
8911
9249
  * Makes the node visible by resetting its display style.
8912
9250
  * If the node is already visible, no action is taken.
@@ -8948,7 +9286,7 @@
8948
9286
  * @returns A function that evaluates the directive's condition.
8949
9287
  */
8950
9288
  #createEvaluator(expression) {
8951
- const identifiers = this.#identifiers ?? [];
9289
+ const identifiers = this.#dependentIdentifiers ?? [];
8952
9290
  const args = identifiers.join(", ");
8953
9291
  const funcBody = `return (${expression});`;
8954
9292
  // Create a dynamic function with the identifiers as parameters
@@ -8956,7 +9294,7 @@
8956
9294
  // Return a function that calls the dynamic function with the current values from the virtual node's bindings
8957
9295
  return () => {
8958
9296
  // Gather the current values of the identifiers from the bindings
8959
- const values = identifiers.map(id => this.#vNode.bindings?.[id]);
9297
+ const values = identifiers.map(id => this.#vNode.bindings?.get(id));
8960
9298
  // Call the dynamic function with the gathered values and return the result as a boolean
8961
9299
  return Boolean(func(...values));
8962
9300
  };
@@ -9104,96 +9442,6 @@
9104
9442
  }
9105
9443
  }
9106
9444
 
9107
- // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9108
- /**
9109
- * Utility class for creating reactive proxies that automatically track changes.
9110
- */
9111
- class ReactiveProxy {
9112
- /**
9113
- * A WeakMap to store the original target for each proxy.
9114
- * This allows us to avoid creating multiple proxies for the same object.
9115
- */
9116
- static proxyMap = new WeakMap();
9117
- /**
9118
- * Creates a reactive proxy for the given object.
9119
- * The proxy will call the onChange callback whenever a property is modified.
9120
- *
9121
- * @param target The object to make reactive.
9122
- * @param onChange Callback function to call when the object changes. Receives the changed key name.
9123
- * @returns A reactive proxy of the target object.
9124
- */
9125
- static create(target, onChange) {
9126
- // If the target is not an object or is null, return it as-is
9127
- if (typeof target !== 'object' || target === null) {
9128
- return target;
9129
- }
9130
- // If this object already has a proxy, return the existing proxy
9131
- if (this.proxyMap.has(target)) {
9132
- return this.proxyMap.get(target);
9133
- }
9134
- // Create the proxy
9135
- const proxy = new Proxy(target, {
9136
- get(obj, key) {
9137
- const value = Reflect.get(obj, key);
9138
- // If the value is an object or array, make it reactive too
9139
- if (typeof value === 'object' && value !== null) {
9140
- return ReactiveProxy.create(value, onChange);
9141
- }
9142
- // For arrays, intercept mutation methods
9143
- if (Array.isArray(obj) && typeof value === 'function') {
9144
- const arrayMutationMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
9145
- if (arrayMutationMethods.includes(key)) {
9146
- return function (...args) {
9147
- const result = value.apply(this, args);
9148
- onChange();
9149
- return result;
9150
- };
9151
- }
9152
- }
9153
- return value;
9154
- },
9155
- set(obj, key, value) {
9156
- const oldValue = Reflect.get(obj, key);
9157
- const result = Reflect.set(obj, key, value);
9158
- // Only trigger onChange if the value actually changed
9159
- if (oldValue !== value) {
9160
- onChange(key);
9161
- }
9162
- return result;
9163
- },
9164
- deleteProperty(obj, key) {
9165
- const result = Reflect.deleteProperty(obj, key);
9166
- onChange(key);
9167
- return result;
9168
- }
9169
- });
9170
- // Store the proxy so we can return it if requested again
9171
- this.proxyMap.set(target, proxy);
9172
- return proxy;
9173
- }
9174
- /**
9175
- * Checks if the given object is a reactive proxy.
9176
- *
9177
- * @param obj The object to check.
9178
- * @returns True if the object is a reactive proxy, false otherwise.
9179
- */
9180
- static isReactive(obj) {
9181
- return this.proxyMap.has(obj);
9182
- }
9183
- /**
9184
- * Unwraps a reactive proxy to get the original object.
9185
- * If the object is not a proxy, returns it as-is.
9186
- *
9187
- * @param obj The object to unwrap.
9188
- * @returns The original object.
9189
- */
9190
- static unwrap(obj) {
9191
- // This is a simplified implementation
9192
- // In a full implementation, we'd need to store a reverse mapping
9193
- return obj;
9194
- }
9195
- }
9196
-
9197
9445
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9198
9446
  /**
9199
9447
  * Represents a virtual application instance.
@@ -9235,18 +9483,10 @@
9235
9483
  * A dictionary mapping computed property names to their dependencies.
9236
9484
  */
9237
9485
  #computedDependencies;
9238
- /**
9239
- * Gets the list of identifiers that can trigger updates.
9240
- */
9241
- #preparableIdentifiers;
9242
9486
  /**
9243
9487
  * Flag to indicate if an update is already scheduled.
9244
9488
  */
9245
9489
  #updateScheduled = false;
9246
- /**
9247
- * Set of keys that have changed since the last update.
9248
- */
9249
- #changedKeys = new Set();
9250
9490
  /**
9251
9491
  * Creates an instance of the virtual application.
9252
9492
  * @param options The application options.
@@ -9265,65 +9505,7 @@
9265
9505
  // Analyze computed dependencies
9266
9506
  this.#computedDependencies = ExpressionUtils.analyzeFunctionDependencies(options.computed || {});
9267
9507
  // Initialize bindings from data, computed, and methods
9268
- this.#bindings = this.#initializeBindings();
9269
- // Prepare the list of identifiers that can trigger updates
9270
- this.#preparableIdentifiers = [...Object.keys(this.#bindings)];
9271
- }
9272
- /**
9273
- * Initializes bindings from data, computed properties, and methods.
9274
- * @returns The initialized bindings object.
9275
- */
9276
- #initializeBindings() {
9277
- const bindings = {};
9278
- // 1. Add data properties with reactive proxy for each property
9279
- if (this.#options.data) {
9280
- const data = this.#options.data();
9281
- if (data && typeof data === 'object') {
9282
- for (const [key, value] of Object.entries(data)) {
9283
- if (typeof value === 'object' && value !== null) {
9284
- // Wrap objects/arrays with reactive proxy, tracking the root key
9285
- bindings[key] = ReactiveProxy.create(value, () => {
9286
- this.#changedKeys.add(key);
9287
- this.scheduleUpdate();
9288
- });
9289
- }
9290
- else {
9291
- // Primitive values are added as-is
9292
- bindings[key] = value;
9293
- }
9294
- }
9295
- }
9296
- }
9297
- // 2. Add computed properties
9298
- if (this.#options.computed) {
9299
- for (const [key, computedFn] of Object.entries(this.#options.computed)) {
9300
- try {
9301
- // Evaluate computed property with bindings as 'this' context
9302
- bindings[key] = computedFn.call(bindings);
9303
- }
9304
- catch (error) {
9305
- this.#logger.error(`Error evaluating computed property '${key}': ${error}`);
9306
- bindings[key] = undefined;
9307
- }
9308
- }
9309
- }
9310
- // 3. Add methods
9311
- if (this.#options.methods) {
9312
- Object.assign(bindings, this.#options.methods);
9313
- }
9314
- // 4. Wrap the entire bindings object with a proxy for primitive value changes
9315
- return new Proxy(bindings, {
9316
- set: (obj, key, value) => {
9317
- const oldValue = Reflect.get(obj, key);
9318
- const result = Reflect.set(obj, key, value);
9319
- // Track changes to primitive values
9320
- if (oldValue !== value) {
9321
- this.#changedKeys.add(key);
9322
- this.scheduleUpdate();
9323
- }
9324
- return result;
9325
- }
9326
- });
9508
+ this.#initializeBindings();
9327
9509
  }
9328
9510
  /**
9329
9511
  * Gets the global directive parser registry.
@@ -9361,12 +9543,6 @@
9361
9543
  get functionDependencies() {
9362
9544
  return this.#functionDependencies;
9363
9545
  }
9364
- /**
9365
- * Gets the list of identifiers that can trigger updates.
9366
- */
9367
- get preparableIdentifiers() {
9368
- return this.#preparableIdentifiers;
9369
- }
9370
9546
  /**
9371
9547
  * Mounts the application.
9372
9548
  * @param selectors The CSS selectors to identify the root element.
@@ -9376,8 +9552,8 @@
9376
9552
  if (!element) {
9377
9553
  throw new Error(`Element not found for selectors: ${selectors}`);
9378
9554
  }
9379
- // Inject utility methods into bindings
9380
- this.#bindings.$nextTick = (callback) => this.nextTick(callback);
9555
+ // Clean the element by removing unnecessary whitespace text nodes
9556
+ this.#cleanElement(element);
9381
9557
  // Create the root virtual node
9382
9558
  this.#vNode = new VNode({
9383
9559
  node: element,
@@ -9385,70 +9561,165 @@
9385
9561
  bindings: this.#bindings
9386
9562
  });
9387
9563
  // Initial rendering
9388
- this.#vNode.update({
9389
- bindings: this.#bindings,
9390
- changedIdentifiers: [],
9391
- isInitial: true
9392
- });
9564
+ this.#vNode.update();
9393
9565
  this.#logger.info('Application mounted.');
9394
9566
  }
9567
+ /**
9568
+ * Cleans the element by removing unnecessary whitespace text nodes.
9569
+ * @param element The element to clean.
9570
+ */
9571
+ #cleanElement(element) {
9572
+ let buffer = null;
9573
+ for (const node of Array.from(element.childNodes)) {
9574
+ if (node.nodeType === Node.TEXT_NODE) {
9575
+ const text = node;
9576
+ if (/^[\s\n\r\t]*$/.test(text.nodeValue || '')) {
9577
+ element.removeChild(text);
9578
+ }
9579
+ else {
9580
+ if (buffer) {
9581
+ buffer.nodeValue += text.nodeValue || '';
9582
+ element.removeChild(text);
9583
+ }
9584
+ else {
9585
+ buffer = text;
9586
+ }
9587
+ }
9588
+ }
9589
+ else {
9590
+ buffer = null;
9591
+ if (node.nodeType === Node.ELEMENT_NODE) {
9592
+ this.#cleanElement(node);
9593
+ }
9594
+ }
9595
+ }
9596
+ }
9597
+ /**
9598
+ * Initializes bindings from data, computed properties, and methods.
9599
+ * @returns The initialized bindings object.
9600
+ */
9601
+ #initializeBindings() {
9602
+ // Create bindings with change tracking
9603
+ this.#bindings = new VBindings({
9604
+ onChange: (key) => {
9605
+ this.#scheduleUpdate();
9606
+ }
9607
+ });
9608
+ // Inject utility methods into bindings
9609
+ this.#bindings.set('$nextTick', (callback) => this.#nextTick(callback));
9610
+ // Add methods
9611
+ if (this.#options.methods) {
9612
+ for (const [key, method] of Object.entries(this.#options.methods)) {
9613
+ if (typeof method !== 'function') {
9614
+ this.#logger.warn(`Method '${key}' is not a function and will be ignored.`);
9615
+ continue;
9616
+ }
9617
+ // Bind the method to the raw bindings object to ensure 'this' refers to bindings
9618
+ // This allows methods to access and modify bindings properties via 'this'
9619
+ this.#bindings.set(key, method.bind(this.#bindings.raw));
9620
+ }
9621
+ }
9622
+ // Add data properties
9623
+ if (this.#options.data) {
9624
+ const data = this.#options.data();
9625
+ if (data && typeof data === 'object') {
9626
+ for (const [key, value] of Object.entries(data)) {
9627
+ this.#bindings.set(key, value);
9628
+ }
9629
+ }
9630
+ }
9631
+ // Add computed properties
9632
+ this.#recomputeProperties();
9633
+ }
9395
9634
  /**
9396
9635
  * Schedules a DOM update in the next microtask.
9397
9636
  * Multiple calls within the same event loop will be batched into a single update.
9398
9637
  */
9399
- scheduleUpdate() {
9638
+ #scheduleUpdate() {
9400
9639
  if (this.#updateScheduled) {
9401
9640
  return;
9402
9641
  }
9403
9642
  this.#updateScheduled = true;
9404
9643
  queueMicrotask(() => {
9644
+ this.#update();
9405
9645
  this.#updateScheduled = false;
9406
- this.update();
9407
9646
  });
9408
9647
  }
9409
9648
  /**
9410
9649
  * Executes an immediate DOM update.
9411
9650
  */
9412
- update() {
9413
- if (!this.#vNode) {
9651
+ #update() {
9652
+ // Re-evaluate computed properties that depend on changed values
9653
+ this.#recomputeProperties();
9654
+ // Update the DOM
9655
+ this.#vNode?.update();
9656
+ // Clear the set of changed identifiers after the update
9657
+ this.#bindings?.clearChanges();
9658
+ }
9659
+ /**
9660
+ * Recursively recomputes computed properties based on changed identifiers.
9661
+ */
9662
+ #recomputeProperties() {
9663
+ if (!this.#options.computed) {
9414
9664
  return;
9415
9665
  }
9416
- // Get the changed data properties
9417
- const dataChanges = Array.from(this.#changedKeys);
9418
- this.#changedKeys.clear();
9419
- // Re-evaluate all computed properties
9420
- const computedChanges = [];
9421
- if (this.#options.computed) {
9422
- for (const [key, computedFn] of Object.entries(this.#options.computed)) {
9423
- try {
9424
- const oldValue = this.#bindings[key];
9425
- const newValue = computedFn.call(this.#bindings);
9426
- this.#bindings[key] = newValue;
9427
- // Track if the computed value actually changed
9428
- if (oldValue !== newValue) {
9429
- computedChanges.push(key);
9430
- this.#logger.debug(`Computed property '${key}' changed: ${oldValue} -> ${newValue}`);
9431
- }
9666
+ const computed = new Set();
9667
+ const processing = new Set();
9668
+ // Helper function to recursively compute a property
9669
+ const compute = (key) => {
9670
+ // Skip if already computed in this update cycle
9671
+ if (computed.has(key)) {
9672
+ return;
9673
+ }
9674
+ // Detect circular dependency
9675
+ if (processing.has(key)) {
9676
+ this.#logger.error(`Circular dependency detected for computed property '${key}'`);
9677
+ return;
9678
+ }
9679
+ processing.add(key);
9680
+ // Get the dependencies for this computed property
9681
+ const deps = this.#computedDependencies[key] || [];
9682
+ // If none of the dependencies have changed, skip recomputation
9683
+ if (!deps.some(dep => this.#bindings?.changes.includes(dep))) {
9684
+ computed.add(key);
9685
+ return;
9686
+ }
9687
+ // First, recursively compute any dependent computed properties
9688
+ for (const dep of deps) {
9689
+ if (this.#options.computed[dep]) {
9690
+ compute(dep);
9432
9691
  }
9433
- catch (error) {
9434
- this.#logger.error(`Error evaluating computed property '${key}': ${error}`);
9692
+ }
9693
+ // Now compute this property
9694
+ const computedFn = this.#options.computed[key];
9695
+ try {
9696
+ const oldValue = this.#bindings?.get(key);
9697
+ const newValue = computedFn.call(this.#bindings?.raw);
9698
+ // Track if the computed value actually changed
9699
+ if (oldValue !== newValue) {
9700
+ this.#bindings?.set(key, newValue);
9701
+ this.#logger.debug(`Computed property '${key}' changed: ${oldValue} -> ${newValue}`);
9435
9702
  }
9436
9703
  }
9704
+ catch (error) {
9705
+ this.#logger.error(`Error evaluating computed property '${key}': ${error}`);
9706
+ }
9707
+ computed.add(key);
9708
+ processing.delete(key);
9709
+ };
9710
+ // Find all computed properties that need to be recomputed
9711
+ for (const [key, deps] of Object.entries(this.#computedDependencies)) {
9712
+ // Check if any dependency has changed
9713
+ if (deps.some(dep => this.#bindings?.changes.includes(dep))) {
9714
+ compute(key);
9715
+ }
9437
9716
  }
9438
- // Combine all changes
9439
- const allChanges = [...dataChanges, ...computedChanges];
9440
- // Update the DOM
9441
- this.#vNode.update({
9442
- bindings: this.#bindings,
9443
- changedIdentifiers: allChanges,
9444
- isInitial: false
9445
- });
9446
9717
  }
9447
9718
  /**
9448
9719
  * Executes a callback after the next DOM update.
9449
9720
  * @param callback The callback to execute.
9450
9721
  */
9451
- nextTick(callback) {
9722
+ #nextTick(callback) {
9452
9723
  if (this.#updateScheduled) {
9453
9724
  queueMicrotask(() => {
9454
9725
  queueMicrotask(callback);