@mintjamsinc/ichigojs 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,11 +11,13 @@ A simple and intuitive reactive framework. Lightweight, fast, and user-friendly
11
11
  - ⚡ **Reactive Proxy System** - Automatic change detection without manual triggers
12
12
  - 🎯 **Computed Properties** - Automatic dependency tracking and re-evaluation
13
13
  - 🔄 **Two-way Binding** - `v-model` with modifiers (`.lazy`, `.number`, `.trim`)
14
- - 🔌 **Lifecycle Hooks** - `@mount`, `@mounted`, `@update`, `@updated`, `@unmount`, `@unmounted`
14
+ - 🔌 **Lifecycle Hooks** - `@mount`, `@mounted`, `@update`, `@updated`, `@unmount`, `@unmounted` with context (`$ctx`)
15
+ - 💾 **userData Storage** - Proxy-free storage for third-party library instances with auto-cleanup
15
16
  - 📦 **Lightweight** - Minimal bundle size
16
17
  - 🚀 **High Performance** - Efficient batched updates via microtask queue
17
18
  - 💪 **TypeScript** - Written in TypeScript with full type support
18
- - 🎨 **Directives** - `v-if`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`
19
+ - 🎨 **Directives** - `v-if`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`, `v-resize`
20
+ - 📐 **Resize Observer** - Monitor element size changes with `v-resize` directive
19
21
 
20
22
  ## Installation
21
23
 
@@ -159,9 +161,52 @@ Event handling with modifiers:
159
161
 
160
162
  Supported modifiers: `.stop`, `.prevent`, `.capture`, `.self`, `.once`
161
163
 
164
+ **Event Handlers with Context:**
165
+
166
+ All event handlers receive the event as the first parameter and `$ctx` as the second parameter:
167
+
168
+ ```javascript
169
+ methods: {
170
+ handleClick(event, $ctx) {
171
+ // event - the DOM event object
172
+ // $ctx.element - the DOM element
173
+ // $ctx.vnode - the VNode instance
174
+ // $ctx.userData - Proxy-free storage
175
+ }
176
+ }
177
+ ```
178
+
179
+ #### v-resize
180
+
181
+ Monitor element size changes using ResizeObserver:
182
+
183
+ ```html
184
+ <div v-resize="onResize" class="resizable-box">
185
+ {{ width }}px × {{ height }}px
186
+ </div>
187
+ ```
188
+
189
+ ```javascript
190
+ methods: {
191
+ onResize(entries, $ctx) {
192
+ const entry = entries[0];
193
+ this.width = Math.round(entry.contentRect.width);
194
+ this.height = Math.round(entry.contentRect.height);
195
+
196
+ // Access element through $ctx
197
+ console.log('Element:', $ctx.element);
198
+ }
199
+ }
200
+ ```
201
+
202
+ **Features:**
203
+ - Native ResizeObserver API for efficient resize detection
204
+ - Automatic cleanup when element is removed
205
+ - Access to element, VNode, and userData via `$ctx`
206
+
162
207
  #### Lifecycle Hooks
163
208
 
164
- Lifecycle hooks allow you to run code at specific stages of an element's lifecycle:
209
+ Lifecycle hooks allow you to run code at specific stages of an element's lifecycle. Each hook receives a **lifecycle context** (`$ctx`) with access to the element, VNode, and userData storage.
165
210
 
166
211
  ```html
167
212
  <div v-if="show"
@@ -182,31 +227,85 @@ Lifecycle hooks allow you to run code at specific stages of an element's lifecyc
182
227
  - `@update` - Called before the element is updated
183
228
  - `@updated` - Called after the element is updated
184
229
  - `@unmount` - Called before the element is removed from the DOM
185
- - `@unmounted` - Called after the element is removed from the DOM
230
+ - `@unmounted` - Called after VNode cleanup is complete (element reference still available)
231
+
232
+ **Lifecycle Context (`$ctx`):**
186
233
 
187
- **Use cases:**
234
+ Every lifecycle hook receives a context object with:
235
+
236
+ - `$ctx.element` - The DOM element
237
+ - `$ctx.vnode` - The VNode instance
238
+ - `$ctx.userData` - Proxy-free storage Map for user data
239
+
240
+ **userData Storage:**
241
+
242
+ `$ctx.userData` is a safe space to store data associated with the element's lifecycle. It's not affected by the reactive proxy system, making it perfect for storing third-party library instances.
188
243
 
189
244
  ```javascript
190
245
  methods: {
191
- onMounted(el) {
192
- // Initialize third-party library (el is the DOM element)
193
- const canvas = el.querySelector('canvas');
194
- canvas._chartInstance = new Chart(canvas.getContext('2d'), { /* ... */ });
246
+ onMounted($ctx) {
247
+ // Initialize third-party library
248
+ const canvas = $ctx.element.querySelector('canvas');
249
+ const chart = new Chart(canvas.getContext('2d'), {
250
+ type: 'line',
251
+ data: { /* ... */ }
252
+ });
253
+
254
+ // Store in userData (Proxy-free storage)
255
+ $ctx.userData.set('chart', chart);
195
256
  },
196
- onUpdated(el) {
197
- // Update chart with new data
198
- const canvas = el.querySelector('canvas');
199
- canvas._chartInstance?.update();
257
+
258
+ onUpdated($ctx) {
259
+ // Retrieve from userData
260
+ const chart = $ctx.userData.get('chart');
261
+ if (chart) {
262
+ chart.data.labels = [...this.labels];
263
+ chart.update();
264
+ }
200
265
  },
201
- onUnmounted(el) {
202
- // Clean up resources
203
- const canvas = el.querySelector('canvas');
204
- canvas._chartInstance?.destroy();
205
- delete canvas._chartInstance;
266
+
267
+ onUnmount($ctx) {
268
+ // Clean up before removal
269
+ const chart = $ctx.userData.get('chart');
270
+ if (chart) {
271
+ chart.destroy();
272
+ $ctx.userData.delete('chart');
273
+ }
274
+ }
275
+ }
276
+ ```
277
+
278
+ **Automatic Cleanup:**
279
+
280
+ Objects with a `close()` method stored in `userData` are automatically cleaned up during the destroy phase:
281
+
282
+ ```javascript
283
+ methods: {
284
+ onMounted($ctx) {
285
+ // Object with close() method
286
+ const resource = {
287
+ data: someData,
288
+ close() {
289
+ // Custom cleanup logic
290
+ console.log('Resource cleaned up');
291
+ }
292
+ };
293
+
294
+ $ctx.userData.set('myResource', resource);
295
+ // resource.close() will be called automatically on unmount
206
296
  }
207
297
  }
208
298
  ```
209
299
 
300
+ **Cleanup Order:**
301
+
302
+ 1. `@unmount` hook fires (element still in DOM)
303
+ 2. `userData` auto-cleanup (close() methods called)
304
+ 3. Child nodes destroyed recursively
305
+ 4. Dependencies unregistered
306
+ 5. Directive manager cleanup
307
+ 6. `@unmounted` hook fires (element removed from DOM, but reference still available in `$ctx.element`)
308
+
210
309
  **Works with v-if and v-for:**
211
310
 
212
311
  ```html
@@ -53,6 +53,7 @@ var StandardDirectiveName;
53
53
  StandardDirectiveName["V_ON"] = "v-on";
54
54
  StandardDirectiveName["V_BIND"] = "v-bind";
55
55
  StandardDirectiveName["V_MODEL"] = "v-model";
56
+ StandardDirectiveName["V_RESIZE"] = "v-resize";
56
57
  })(StandardDirectiveName || (StandardDirectiveName = {}));
57
58
 
58
59
  // This file was generated. Do not modify manually!
@@ -7672,6 +7673,13 @@ class VNode {
7672
7673
  * This is optional and may be undefined if the node has not been templatized.
7673
7674
  */
7674
7675
  #templatized;
7676
+ /**
7677
+ * User data storage for lifecycle directives.
7678
+ * This provides a Proxy-free space where developers can store arbitrary data
7679
+ * associated with this VNode. The data is automatically cleaned up when the
7680
+ * VNode is destroyed.
7681
+ */
7682
+ #userData;
7675
7683
  /**
7676
7684
  * Creates an instance of the virtual node.
7677
7685
  * @param args The initialization arguments for the virtual node.
@@ -7883,6 +7891,18 @@ class VNode {
7883
7891
  this.#preparableIdentifiers = preparableIdentifiers.length === 0 ? [] : [...new Set(preparableIdentifiers)];
7884
7892
  return this.#preparableIdentifiers;
7885
7893
  }
7894
+ /**
7895
+ * Gets the user data storage for this virtual node.
7896
+ * This is lazily initialized and provides a Proxy-free space for storing
7897
+ * arbitrary data associated with lifecycle directives.
7898
+ * @returns A Map for storing user data.
7899
+ */
7900
+ get userData() {
7901
+ if (!this.#userData) {
7902
+ this.#userData = new Map();
7903
+ }
7904
+ return this.#userData;
7905
+ }
7886
7906
  /**
7887
7907
  * The DOM path of this virtual node.
7888
7908
  * This is a string representation of the path from the root to this node,
@@ -8089,6 +8109,14 @@ class VNode {
8089
8109
  /**
8090
8110
  * Cleans up any resources used by this virtual node.
8091
8111
  * This method is called when the virtual node is no longer needed.
8112
+ *
8113
+ * Cleanup order:
8114
+ * 1. Call onUnmount lifecycle hooks
8115
+ * 2. Auto-cleanup userData (close() on Closeable objects)
8116
+ * 3. Recursively destroy child nodes
8117
+ * 4. Unregister dependencies
8118
+ * 5. Clean up directive manager
8119
+ * 6. Call onUnmounted lifecycle hooks
8092
8120
  */
8093
8121
  destroy() {
8094
8122
  // If no directive requires template preservation, call onUnmount for directives that do not templatize
@@ -8097,6 +8125,23 @@ class VNode {
8097
8125
  d.onUnmount?.();
8098
8126
  });
8099
8127
  }
8128
+ // Clean up user data, calling close() on any Closeable objects
8129
+ // This happens after onUnmount but before other cleanup, allowing users to
8130
+ // perform custom cleanup in onUnmount while having automatic cleanup of userData
8131
+ if (this.#userData) {
8132
+ for (const [key, value] of this.#userData.entries()) {
8133
+ try {
8134
+ // If the value has a close() method (Closeable pattern), call it
8135
+ if (value && typeof value === 'object' && typeof value.close === 'function') {
8136
+ value.close();
8137
+ }
8138
+ }
8139
+ catch (error) {
8140
+ this.#vApplication.logManager.getLogger(this.constructor.name).error(`Error closing user data '${key}': ${error}`);
8141
+ }
8142
+ }
8143
+ this.#userData.clear();
8144
+ }
8100
8145
  // Recursively destroy child nodes
8101
8146
  if (this.#childVNodes) {
8102
8147
  for (const childVNode of this.#childVNodes) {
@@ -9259,7 +9304,7 @@ class VModelDirective {
9259
9304
  * @update="onUpdate" - Called before the element is updated
9260
9305
  * @updated="onUpdated" - Called after the element is updated
9261
9306
  * @unmount="onUnmount" - Called before the element is removed from the DOM
9262
- * @unmounted="onUnmounted" - Called after the element is removed from the DOM
9307
+ * @unmounted="onUnmounted" - Called after VNode cleanup is complete (element reference still available)
9263
9308
  *
9264
9309
  * This directive is essential for handling user interactions and lifecycle events in your application.
9265
9310
  * Note that the methods referenced in the directive should be defined in the component's methods object.
@@ -9501,7 +9546,7 @@ class VOnDirective {
9501
9546
  return ['mount', 'mounted', 'update', 'updated', 'unmount', 'unmounted'].includes(eventName);
9502
9547
  }
9503
9548
  /**
9504
- * Creates a wrapper function for lifecycle hooks (with element parameter).
9549
+ * Creates a wrapper function for lifecycle hooks (with context parameter).
9505
9550
  * @param expression The expression string to evaluate.
9506
9551
  * @returns A function that handles the lifecycle hook.
9507
9552
  */
@@ -9511,26 +9556,31 @@ class VOnDirective {
9511
9556
  // Return a function that handles the lifecycle hook with proper scope
9512
9557
  return () => {
9513
9558
  const bindings = vNode.bindings;
9514
- const el = vNode.node;
9559
+ const $ctx = {
9560
+ element: vNode.node,
9561
+ vnode: vNode,
9562
+ userData: vNode.userData
9563
+ };
9515
9564
  // If the expression is just a method name, call it with bindings as 'this'
9516
9565
  const trimmedExpr = expression.trim();
9517
9566
  if (identifiers.includes(trimmedExpr) && typeof bindings?.get(trimmedExpr) === 'function') {
9518
9567
  const methodName = trimmedExpr;
9519
9568
  const originalMethod = bindings?.get(methodName);
9520
- // Call the method with bindings as 'this' context and element as parameter
9521
- // This allows the method to access the DOM element and bindings
9522
- return originalMethod(el);
9569
+ // Call the method with bindings as 'this' context and context as parameter
9570
+ // This allows the method to access the DOM element, VNode, and userData
9571
+ return originalMethod($ctx);
9523
9572
  }
9524
- // For inline expressions, evaluate normally with element parameter
9573
+ // For inline expressions, evaluate normally with $ctx parameter
9574
+ // Note: $ctx is a reserved variable name for lifecycle context
9525
9575
  const values = identifiers.map(id => vNode.bindings?.get(id));
9526
- const args = [...identifiers, 'el'].join(", ");
9576
+ const args = [...identifiers, '$ctx'].join(", ");
9527
9577
  const funcBody = `return (${expression});`;
9528
9578
  const func = new Function(args, funcBody);
9529
- return func.call(bindings?.raw, ...values, el);
9579
+ return func.call(bindings?.raw, ...values, $ctx);
9530
9580
  };
9531
9581
  }
9532
9582
  /**
9533
- * Creates a wrapper function for DOM event handlers (with event parameter).
9583
+ * Creates a wrapper function for DOM event handlers (with event and $ctx parameters).
9534
9584
  * @param expression The expression string to evaluate.
9535
9585
  * @returns A function that handles the event.
9536
9586
  */
@@ -9540,21 +9590,210 @@ class VOnDirective {
9540
9590
  // Return a function that handles the event with proper scope
9541
9591
  return (event) => {
9542
9592
  const bindings = vNode.bindings;
9593
+ const $ctx = {
9594
+ element: vNode.node,
9595
+ vnode: vNode,
9596
+ userData: vNode.userData
9597
+ };
9543
9598
  // If the expression is just a method name, call it with bindings as 'this'
9544
9599
  const trimmedExpr = expression.trim();
9545
9600
  if (identifiers.includes(trimmedExpr) && typeof bindings?.get(trimmedExpr) === 'function') {
9546
9601
  const methodName = trimmedExpr;
9547
9602
  const originalMethod = bindings?.get(methodName);
9548
9603
  // Call the method with bindings as 'this' context
9549
- // This allows the method to access and modify bindings properties via 'this'
9550
- return originalMethod(event);
9604
+ // Pass event as first argument and $ctx as second argument
9605
+ return originalMethod(event, $ctx);
9551
9606
  }
9552
9607
  // For inline expressions, evaluate normally
9608
+ // Note: inline expressions receive event and $ctx as parameters
9553
9609
  const values = identifiers.map(id => vNode.bindings?.get(id));
9554
- const args = identifiers.join(", ");
9610
+ const args = [...identifiers, 'event', '$ctx'].join(", ");
9555
9611
  const funcBody = `return (${expression});`;
9556
9612
  const func = new Function(args, funcBody);
9557
- return func.call(bindings?.raw, ...values, event);
9613
+ return func.call(bindings?.raw, ...values, event, $ctx);
9614
+ };
9615
+ }
9616
+ }
9617
+
9618
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
9619
+ /**
9620
+ * Directive for observing element resize events using ResizeObserver.
9621
+ * The `v-resize` directive allows you to respond to changes in an element's size.
9622
+ *
9623
+ * Example usage:
9624
+ * <div v-resize="handleResize">Resizable content</div>
9625
+ *
9626
+ * The handler receives ResizeObserverEntry array as the first argument and $ctx as the second:
9627
+ * handleResize(entries, $ctx) {
9628
+ * const { width, height } = entries[0].contentRect;
9629
+ * console.log(`Size: ${width}x${height}`);
9630
+ * }
9631
+ *
9632
+ * This directive is useful for responsive layouts, charts, and other components
9633
+ * that need to adapt to size changes.
9634
+ */
9635
+ class VResizeDirective {
9636
+ /**
9637
+ * The virtual node to which this directive is applied.
9638
+ */
9639
+ #vNode;
9640
+ /**
9641
+ * A list of variable and function names used in the directive's expression.
9642
+ */
9643
+ #dependentIdentifiers;
9644
+ /**
9645
+ * The resize handler wrapper function.
9646
+ */
9647
+ #handlerWrapper;
9648
+ /**
9649
+ * The ResizeObserver instance.
9650
+ */
9651
+ #resizeObserver;
9652
+ /**
9653
+ * @param context The context for parsing the directive.
9654
+ */
9655
+ constructor(context) {
9656
+ this.#vNode = context.vNode;
9657
+ // Parse the expression to extract identifiers and create the handler wrapper
9658
+ const expression = context.attribute.value;
9659
+ if (expression) {
9660
+ this.#dependentIdentifiers = ExpressionUtils.extractIdentifiers(expression, context.vNode.vApplication.functionDependencies);
9661
+ this.#handlerWrapper = this.#createResizeHandlerWrapper(expression);
9662
+ }
9663
+ // Remove the directive attribute from the element
9664
+ this.#vNode.node.removeAttribute(context.attribute.name);
9665
+ }
9666
+ /**
9667
+ * @inheritdoc
9668
+ */
9669
+ get name() {
9670
+ return StandardDirectiveName.V_RESIZE;
9671
+ }
9672
+ /**
9673
+ * @inheritdoc
9674
+ */
9675
+ get vNode() {
9676
+ return this.#vNode;
9677
+ }
9678
+ /**
9679
+ * @inheritdoc
9680
+ */
9681
+ get needsAnchor() {
9682
+ return false;
9683
+ }
9684
+ /**
9685
+ * @inheritdoc
9686
+ */
9687
+ get bindingsPreparer() {
9688
+ return undefined;
9689
+ }
9690
+ /**
9691
+ * @inheritdoc
9692
+ */
9693
+ get domUpdater() {
9694
+ return undefined;
9695
+ }
9696
+ /**
9697
+ * @inheritdoc
9698
+ */
9699
+ get templatize() {
9700
+ return false;
9701
+ }
9702
+ /**
9703
+ * @inheritdoc
9704
+ */
9705
+ get dependentIdentifiers() {
9706
+ return this.#dependentIdentifiers ?? [];
9707
+ }
9708
+ /**
9709
+ * @inheritdoc
9710
+ */
9711
+ get onMount() {
9712
+ return undefined;
9713
+ }
9714
+ /**
9715
+ * @inheritdoc
9716
+ */
9717
+ get onMounted() {
9718
+ if (!this.#handlerWrapper) {
9719
+ return undefined;
9720
+ }
9721
+ const element = this.#vNode.node;
9722
+ const handler = this.#handlerWrapper;
9723
+ return () => {
9724
+ // Create ResizeObserver and start observing
9725
+ this.#resizeObserver = new ResizeObserver((entries) => {
9726
+ handler(entries);
9727
+ });
9728
+ this.#resizeObserver.observe(element);
9729
+ };
9730
+ }
9731
+ /**
9732
+ * @inheritdoc
9733
+ */
9734
+ get onUpdate() {
9735
+ return undefined;
9736
+ }
9737
+ /**
9738
+ * @inheritdoc
9739
+ */
9740
+ get onUpdated() {
9741
+ return undefined;
9742
+ }
9743
+ /**
9744
+ * @inheritdoc
9745
+ */
9746
+ get onUnmount() {
9747
+ return undefined;
9748
+ }
9749
+ /**
9750
+ * @inheritdoc
9751
+ */
9752
+ get onUnmounted() {
9753
+ return undefined;
9754
+ }
9755
+ /**
9756
+ * @inheritdoc
9757
+ */
9758
+ destroy() {
9759
+ // Disconnect the ResizeObserver when the directive is destroyed
9760
+ if (this.#resizeObserver) {
9761
+ this.#resizeObserver.disconnect();
9762
+ this.#resizeObserver = undefined;
9763
+ }
9764
+ }
9765
+ /**
9766
+ * Creates a wrapper function for resize handlers.
9767
+ * @param expression The expression string to evaluate.
9768
+ * @returns A function that handles the resize event.
9769
+ */
9770
+ #createResizeHandlerWrapper(expression) {
9771
+ const identifiers = this.#dependentIdentifiers ?? [];
9772
+ const vNode = this.#vNode;
9773
+ // Return a function that handles the resize event with proper scope
9774
+ return (entries) => {
9775
+ const bindings = vNode.bindings;
9776
+ const $ctx = {
9777
+ element: vNode.node,
9778
+ vnode: vNode,
9779
+ userData: vNode.userData
9780
+ };
9781
+ // If the expression is just a method name, call it with bindings as 'this'
9782
+ const trimmedExpr = expression.trim();
9783
+ if (identifiers.includes(trimmedExpr) && typeof bindings?.get(trimmedExpr) === 'function') {
9784
+ const methodName = trimmedExpr;
9785
+ const originalMethod = bindings?.get(methodName);
9786
+ // Call the method with bindings as 'this' context
9787
+ // Pass entries as first argument and $ctx as second argument
9788
+ return originalMethod(entries, $ctx);
9789
+ }
9790
+ // For inline expressions, evaluate normally
9791
+ // Note: inline expressions receive entries and $ctx as parameters
9792
+ const values = identifiers.map(id => vNode.bindings?.get(id));
9793
+ const args = [...identifiers, 'entries', '$ctx'].join(", ");
9794
+ const funcBody = `return (${expression});`;
9795
+ const func = new Function(args, funcBody);
9796
+ return func.call(bindings?.raw, ...values, entries, $ctx);
9558
9797
  };
9559
9798
  }
9560
9799
  }
@@ -9784,7 +10023,9 @@ class VStandardDirectiveParser {
9784
10023
  context.attribute.name.startsWith(":") ||
9785
10024
  // v-model, v-model.<modifier>
9786
10025
  context.attribute.name === StandardDirectiveName.V_MODEL ||
9787
- context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ".")) {
10026
+ context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ".") ||
10027
+ // v-resize
10028
+ context.attribute.name === StandardDirectiveName.V_RESIZE) {
9788
10029
  return true;
9789
10030
  }
9790
10031
  return false;
@@ -9823,6 +10064,10 @@ class VStandardDirectiveParser {
9823
10064
  context.attribute.name.startsWith(StandardDirectiveName.V_MODEL + ".")) {
9824
10065
  return new VModelDirective(context);
9825
10066
  }
10067
+ // v-resize
10068
+ if (context.attribute.name === StandardDirectiveName.V_RESIZE) {
10069
+ return new VResizeDirective(context);
10070
+ }
9826
10071
  throw new Error(`The attribute "${context.attribute.name}" cannot be parsed by ${this.name}.`);
9827
10072
  }
9828
10073
  }