@mintjamsinc/ichigojs 0.1.68 → 0.1.70

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
@@ -9,14 +9,17 @@ A simple and intuitive reactive framework. Lightweight, fast, and user-friendly
9
9
 
10
10
  - ✨ **Vue-like API** - Familiar syntax for Vue developers
11
11
  - ⚡ **Reactive Proxy System** - Automatic change detection without manual triggers
12
- - 🎯 **Computed Properties** - Automatic dependency tracking and re-evaluation
12
+ - 🎯 **Computed Properties** - Automatic dependency tracking and re-evaluation, including writable computed (`{ get, set }`)
13
+ - 👀 **Watchers** - React to data changes with the `watch` option (`deep`, `immediate`)
13
14
  - 🔄 **Two-way Binding** - `v-model` with modifiers (`.lazy`, `.number`, `.trim`)
14
15
  - 🔌 **Lifecycle Hooks** - `@mount`, `@mounted`, `@update`, `@updated`, `@unmount`, `@unmounted` with context (`$ctx`)
15
16
  - 💾 **userData Storage** - Proxy-free storage for third-party library instances with auto-cleanup
17
+ - 🧩 **Components** - Reusable Web Components via `defineComponent` with `props`, `slot`, and `$emit`
16
18
  - 📦 **Lightweight** - Minimal bundle size
17
19
  - 🚀 **High Performance** - Efficient batched updates via microtask queue
18
20
  - 💪 **TypeScript** - Written in TypeScript with full type support
19
- - 🎨 **Directives** - `v-if`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`, `v-resize`, `v-intersection`, `v-performance`
21
+ - 🎨 **Directives** - `v-if`, `v-else-if`, `v-else`, `v-for`, `v-show`, `v-bind`, `v-on`, `v-model`, `v-text`, `v-html`, `v-focus`, `v-resize`, `v-intersection`, `v-performance`
22
+ - 🎯 **Focus Management** - Declarative focus control with the `v-focus` directive (`.select`, `.cursor-end`)
20
23
  - 📐 **Resize Observer** - Monitor element size changes with `v-resize` directive
21
24
  - 👁️ **Intersection Observer** - Detect element visibility with `v-intersection` directive
22
25
  - ⚡ **Performance Observer** - Monitor performance metrics with `v-performance` directive
@@ -195,6 +198,102 @@ VDOM.createApp({
195
198
  }).mount('#app');
196
199
  ```
197
200
 
201
+ **Lazy (pull-based) evaluation:**
202
+
203
+ Computed properties are evaluated lazily and cached. When a dependency changes,
204
+ the dependent computed is marked stale (rather than eagerly recomputed) and is
205
+ recomputed on the next read. Because of this, reading a computed property
206
+ **synchronously after mutating a dependency returns an up-to-date value** — you
207
+ do not have to wait for the next tick:
208
+
209
+ ```javascript
210
+ this.cartItems.push(item);
211
+ console.log(this.subtotal); // already reflects the new item
212
+ ```
213
+
214
+ DOM updates remain batched in a microtask, so multiple synchronous mutations
215
+ still result in a single render. Each computed is recomputed at most once per
216
+ update cycle (on first read, or during the pre-render flush, whichever comes
217
+ first), and a computed whose recomputed value is unchanged does not trigger DOM
218
+ updates or watchers that depend on it. Computed→computed chains resolve
219
+ automatically and independently of declaration order.
220
+
221
+ A computed property can also be defined as an object with both a `get` and a
222
+ `set` function. This makes it writable, so it can be used as a `v-model` target
223
+ or assigned to directly. Reads go through `get`, while assignments are routed
224
+ through `set`.
225
+
226
+ ```javascript
227
+ VDOM.createApp({
228
+ data() {
229
+ return {
230
+ firstName: 'John',
231
+ lastName: 'Doe'
232
+ };
233
+ },
234
+ computed: {
235
+ fullName: {
236
+ get() {
237
+ return `${this.firstName} ${this.lastName}`;
238
+ },
239
+ set(value) {
240
+ const [first, last] = value.split(' ');
241
+ this.firstName = first;
242
+ this.lastName = last;
243
+ }
244
+ }
245
+ }
246
+ }).mount('#app');
247
+ ```
248
+
249
+ ```html
250
+ <!-- Assigning through v-model invokes the computed setter -->
251
+ <input v-model="fullName">
252
+ ```
253
+
254
+ ### Watchers
255
+
256
+ Use the `watch` option to run a callback whenever a watched property changes.
257
+ Keys are property paths (e.g. `"count"`, `"user.name"`), and the callback
258
+ receives the new and previous values.
259
+
260
+ ```javascript
261
+ VDOM.createApp({
262
+ data() {
263
+ return {
264
+ count: 0,
265
+ user: { name: 'Alice' }
266
+ };
267
+ },
268
+ watch: {
269
+ // Shorthand: a callback function
270
+ count(newValue, oldValue) {
271
+ console.log(`count changed from ${oldValue} to ${newValue}`);
272
+ },
273
+
274
+ // Watch a nested property by path
275
+ 'user.name'(newValue, oldValue) {
276
+ console.log(`name changed from ${oldValue} to ${newValue}`);
277
+ },
278
+
279
+ // Full form: an options object with deep / immediate
280
+ user: {
281
+ handler(newValue, oldValue) {
282
+ console.log('user object changed', newValue);
283
+ },
284
+ deep: true, // Observe nested changes inside the object
285
+ immediate: true // Invoke once immediately with the current value
286
+ }
287
+ }
288
+ }).mount('#app');
289
+ ```
290
+
291
+ **Watcher options:**
292
+
293
+ - `handler` - The callback invoked when the watched value changes
294
+ - `deep` - When `true`, deeply observes nested object/array changes (default: `false`)
295
+ - `immediate` - When `true`, invokes the handler once immediately on registration with the current value (default: `false`)
296
+
198
297
  ### Directives
199
298
 
200
299
  #### v-if / v-else-if / v-else
@@ -314,6 +413,73 @@ methods: {
314
413
  }
315
414
  ```
316
415
 
416
+ #### v-text
417
+
418
+ Set the text content of an element. The expression result replaces the
419
+ element's `textContent`. Unlike `v-html`, the content is rendered as plain
420
+ text, so HTML is escaped and XSS is not a concern.
421
+
422
+ ```html
423
+ <span v-text="message"></span>
424
+
425
+ <!-- Equivalent to -->
426
+ <span>{{ message }}</span>
427
+ ```
428
+
429
+ Use `v-text` when you want to set the entire text content of an element from a
430
+ single expression (it overwrites any existing content), rather than
431
+ interpolating with `{{ }}`.
432
+
433
+ #### v-html
434
+
435
+ Set the raw HTML content of an element. The expression result is assigned to
436
+ the element's `innerHTML`.
437
+
438
+ ```html
439
+ <div v-html="htmlContent"></div>
440
+ ```
441
+
442
+ > ⚠️ **Security warning:** Dynamically rendering arbitrary HTML can easily lead
443
+ > to XSS attacks. Only use `v-html` on **trusted** content, and **never** on
444
+ > user-provided content. For plain text, use `v-text` or `{{ }}` interpolation
445
+ > instead.
446
+
447
+ #### v-focus
448
+
449
+ Declaratively manage focus on an element. Focus is deferred via
450
+ `requestAnimationFrame`, so elements that become visible just before the
451
+ directive runs (for example inside a `v-if` or a `display: none` container)
452
+ still receive focus reliably.
453
+
454
+ ```html
455
+ <!-- Focus once after mount -->
456
+ <input v-focus>
457
+
458
+ <!-- Focus + select all text after mount -->
459
+ <input v-focus.select>
460
+
461
+ <!-- Focus + place the caret at the end of the value -->
462
+ <input v-focus.cursor-end value="prefilled">
463
+
464
+ <!-- Conditional focus: fires when the expression goes from falsy to truthy -->
465
+ <input v-focus="isEditing">
466
+
467
+ <!-- Conditional focus + select all -->
468
+ <input v-focus.select="isEditing">
469
+ ```
470
+
471
+ **Behavior:**
472
+
473
+ - **Without an expression**, the element is focused exactly once after mount.
474
+ - **With an expression**, focus fires only on the falsy → truthy edge, so the
475
+ user is not repeatedly re-focused on every reactive update. If the value is
476
+ already truthy on mount, the element is focused immediately.
477
+
478
+ **Modifiers:**
479
+
480
+ - `.select` - After focusing, selects all text in the input/textarea
481
+ - `.cursor-end` - After focusing, places the caret at the end of the value
482
+
317
483
  #### v-resize
318
484
 
319
485
  Monitor element size changes using ResizeObserver:
@@ -591,9 +757,20 @@ Two-way data binding:
591
757
  <input v-model.number="age"> <!-- Convert to number -->
592
758
  <input v-model.trim="username"> <!-- Trim whitespace -->
593
759
 
594
- <!-- Checkbox -->
760
+ <!-- Checkbox (boolean) -->
595
761
  <input type="checkbox" v-model="isChecked">
596
762
 
763
+ <!-- Checkbox with custom true/false values -->
764
+ <input type="checkbox" v-model="status" :true-value="'yes'" :false-value="'no'">
765
+
766
+ <!-- Checkbox group bound to an array -->
767
+ <input type="checkbox" value="a" v-model="selectedItems">
768
+ <input type="checkbox" value="b" v-model="selectedItems">
769
+
770
+ <!-- Radio -->
771
+ <input type="radio" value="a" v-model="picked">
772
+ <input type="radio" value="b" v-model="picked">
773
+
597
774
  <!-- Select -->
598
775
  <select v-model="selected">
599
776
  <option value="a">Option A</option>
@@ -601,6 +778,13 @@ Two-way data binding:
601
778
  </select>
602
779
  ```
603
780
 
781
+ **Supported elements:**
782
+
783
+ - **Text inputs / `<textarea>`** - Binds to the element's value
784
+ - **Checkbox** - Binds to a boolean, to a custom value pair via `:true-value` / `:false-value`, or to an array (when the bound value is an array, the checkbox's `value` is added/removed)
785
+ - **Radio** - Binds to the `value` (or `:value`) of the selected radio button
786
+ - **Select** - Binds to the selected option's value (re-applied automatically when options are generated dynamically via `v-for`)
787
+
604
788
  ### Methods
605
789
 
606
790
  Methods have access to data and computed properties via `this`:
@@ -639,13 +823,115 @@ methods: {
639
823
  }
640
824
  ```
641
825
 
826
+ ## Components
827
+
828
+ ichigo.js components are real [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)
829
+ backed by the same reactivity system. Define a component with `defineComponent`,
830
+ pointing it at a `<template>` for its markup.
831
+
832
+ ```html
833
+ <!-- Component markup -->
834
+ <template id="my-list">
835
+ <ul v-if="items.length > 0">
836
+ <li v-for="item of items" :key="item.id">{{ item.name }}</li>
837
+ </ul>
838
+ <!-- Fallback content projected from the parent -->
839
+ <slot></slot>
840
+ </template>
841
+ ```
842
+
843
+ ```javascript
844
+ import { defineComponent } from '@mintjamsinc/ichigojs';
845
+
846
+ defineComponent('my-list', {
847
+ template: '#my-list', // CSS selector for the <template>
848
+ props: ['items'], // Props received from the parent
849
+ data() {
850
+ // Props are accessible via `this` and can be defaulted/transformed here
851
+ return { items: this.items ?? [] };
852
+ }
853
+ });
854
+ ```
855
+
856
+ ```html
857
+ <!-- Usage -->
858
+ <my-list :items="searchResults">
859
+ <span slot="empty">No results.</span>
860
+ </my-list>
861
+ ```
862
+
863
+ **Props:**
864
+
865
+ - Declared via the `props` array. Each declared prop becomes a property on the
866
+ custom element, so the parent can bind to it with `v-bind` / `:`
867
+ (e.g. `:items="searchResults"`).
868
+ - Props are reactive from the start and are included in the component's data
869
+ automatically. Values returned from `data()` take precedence, allowing you to
870
+ default or transform a prop (e.g. `this.items ?? []`).
871
+
872
+ **Slots:**
873
+
874
+ Use the native `<slot>` element in the component template to project content
875
+ from the parent. ichigo.js components use Light DOM.
876
+
877
+ ### Events (`$emit`)
878
+
879
+ Components (and applications) can dispatch custom events with `$emit`, which is
880
+ available in both templates and methods. By default the event bubbles from the
881
+ component's root element, so a parent can listen for it with `v-on` / `@` on the
882
+ component tag.
883
+
884
+ ```javascript
885
+ defineComponent('my-button', {
886
+ template: '#my-button',
887
+ // Optional: declare the events this component emits.
888
+ // Emitting an undeclared event logs a development warning (validation only;
889
+ // it never blocks dispatch). Omit `emits` to allow any event name.
890
+ emits: ['selected'],
891
+ methods: {
892
+ onClick() {
893
+ // $emit(name, detail?, options?)
894
+ this.$emit('selected', { id: 42 });
895
+ }
896
+ }
897
+ });
898
+ ```
899
+
900
+ ```html
901
+ <!-- Parent listens for the custom event; payload is in event.detail -->
902
+ <my-button @selected="onSelected"></my-button>
903
+ ```
904
+
905
+ **`$emit(name, detail?, options?)`:**
906
+
907
+ - `name` - The event name (listened to as `@name` on the parent)
908
+ - `detail` - The payload exposed as `event.detail`
909
+ - `options` - Dispatch options (`VEmitOptions`):
910
+ - `bubbles` - Whether the event bubbles (default: `true`)
911
+ - `cancelable` - Whether `preventDefault()` has an effect (default: `true`); `$emit` returns `false` when a listener calls `preventDefault()`
912
+ - `composed` - Whether the event crosses shadow DOM boundaries (default: `false`)
913
+ - `target` - The dispatch target (default: the application root element). Set to `document` / `window` for a global event bus.
914
+
915
+ ### Legacy component directive (`v-component`)
916
+
917
+ > ⚠️ **Deprecated.** The `v-component` directive and the `VComponentRegistry`
918
+ > are deprecated and will be removed in a future release. Use
919
+ > [`defineComponent`](#components) (Custom Elements) for new code.
920
+
921
+ For reference, the legacy mechanism renders a component registered in the
922
+ application's `VComponentRegistry` by id, passing props through `:options`:
923
+
924
+ ```html
925
+ <div v-component="my-component" :options="{ message: 'Hello' }"></div>
926
+ ```
927
+
642
928
  ## Performance
643
929
 
644
930
  ichigo.js uses several optimization techniques:
645
931
 
646
932
  - **Microtask batching**: Multiple synchronous changes result in a single DOM update
647
933
  - **Efficient change tracking**: Only changed properties trigger re-evaluation
648
- - **Smart computed caching**: Computed properties only re-evaluate when dependencies change
934
+ - **Lazy computed caching**: Computed properties are pull-based — they re-evaluate only when a dependency changes and the value is actually read, at most once per update cycle
649
935
 
650
936
  Benchmark (1000 item list update): **~6.8ms** ⚡
651
937
 
@@ -696,13 +982,33 @@ Creates a new application instance.
696
982
 
697
983
  **Options:**
698
984
 
699
- - `data()`: Function that returns the initial data object
700
- - `computed`: Object containing computed property definitions
985
+ - `data()`: Function that returns the initial data object. Called with a `$ctx` (`{ $markRaw }`) as `this`.
986
+ - `computed`: Object containing computed property definitions. Each value is either a getter function (read-only) or a `{ get, set }` object (writable).
701
987
  - `methods`: Object containing method definitions
988
+ - `watch`: Object mapping property paths to watcher definitions (a callback, or `{ handler, deep, immediate }`)
989
+ - `emits`: Optional array of event names the app/component is expected to emit via `$emit`. Emitting an undeclared event logs a development warning (validation only).
702
990
  - `logLevel`: Logging level (`'debug'` | `'info'` | `'warn'` | `'error'`)
703
991
 
704
992
  **Returns:** Application instance with `mount(selector)` method
705
993
 
994
+ **Instance helpers** (available in `data()`, methods, expressions, and lifecycle hooks as appropriate):
995
+
996
+ - `$markRaw(obj)`: Marks an object as non-reactive (see [Marking Objects as Non-Reactive](#marking-objects-as-non-reactive))
997
+ - `$nextTick(callback)`: Runs a callback after the next DOM update
998
+ - `$emit(name, detail?, options?)`: Dispatches a custom event (see [Events](#events-emit))
999
+ - `$ctx`: Lifecycle/handler context with `element`, `vnode`, and `userData`
1000
+
1001
+ ### defineComponent(tagName, options)
1002
+
1003
+ Defines and registers a custom element backed by ichigo.js reactivity. See [Components](#components).
1004
+
1005
+ **Options** (extends the `createApp` options above):
1006
+
1007
+ - `template`: CSS selector for the `<template>` element that defines the component's markup (required)
1008
+ - `props`: Array of property names received from the parent via attribute/property binding
1009
+
1010
+ **Returns:** `void` (the custom element is registered via `customElements.define`)
1011
+
706
1012
  ## Contributing
707
1013
 
708
1014
  Contributions are welcome! Please feel free to submit a Pull Request.