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