@mintjamsinc/ichigojs 0.1.73 → 0.1.75

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
@@ -15,6 +15,7 @@ A simple and intuitive reactive framework. Lightweight, fast, and user-friendly
15
15
  - 🔌 **Lifecycle Hooks** - `@mount`, `@mounted`, `@update`, `@updated`, `@unmount`, `@unmounted` with context (`$ctx`)
16
16
  - 💾 **userData Storage** - Proxy-free storage for third-party library instances with auto-cleanup
17
17
  - 🧩 **Components** - Reusable Web Components via `defineComponent` with `props`, `slot`, and `$emit`
18
+ - 🔗 **Template refs** - Direct access to DOM elements and component hosts via `ref` and `$refs`
18
19
  - 📦 **Lightweight** - Minimal bundle size
19
20
  - 🚀 **High Performance** - Efficient batched updates via microtask queue
20
21
  - 💪 **TypeScript** - Written in TypeScript with full type support
@@ -823,6 +824,102 @@ methods: {
823
824
  }
824
825
  ```
825
826
 
827
+ ### Template refs (`$refs`)
828
+
829
+ Use the `ref` attribute to obtain a direct reference to a DOM element (or a
830
+ component's host element). Referenced elements are collected into the `$refs`
831
+ object, available on `this` in methods and as `$refs` in template expressions —
832
+ the same mechanism as Vue's `$refs`.
833
+
834
+ ```html
835
+ <input ref="search">
836
+ <button @click="focusSearch">Focus</button>
837
+ ```
838
+
839
+ ```javascript
840
+ methods: {
841
+ focusSearch() {
842
+ // this.$refs.search is the <input> element
843
+ this.$refs.search.focus();
844
+ }
845
+ }
846
+ ```
847
+
848
+ **When refs are available:**
849
+
850
+ A ref is registered during the element's mount phase and removed during its
851
+ unmount phase, so it is already populated by the time a `@mounted` hook runs.
852
+ After changing data, wait for `$nextTick` before reading refs for elements that
853
+ were just rendered (e.g. by `v-if` / `v-for`).
854
+
855
+ ```javascript
856
+ methods: {
857
+ async showAndFocus() {
858
+ this.editing = true; // reveals <input v-if="editing" ref="field">
859
+ this.$nextTick(() => this.$refs.field.focus());
860
+ }
861
+ }
862
+ ```
863
+
864
+ **Refs on components:**
865
+
866
+ A `ref` on a component resolves to the component's **host custom element**, so
867
+ you can call DOM methods on it (focus, scroll, `getBoundingClientRect`,
868
+ `dispatchEvent`, etc.).
869
+
870
+ ```html
871
+ <my-card ref="card"></my-card>
872
+ ```
873
+
874
+ ```javascript
875
+ this.$refs.card; // the <my-card> element
876
+ ```
877
+
878
+ **Refs inside `v-for` (arrays):**
879
+
880
+ When `ref` is used on (or inside) a `v-for`, the matching elements are collected
881
+ into an **array** under that name.
882
+
883
+ ```html
884
+ <ul>
885
+ <li v-for="item in items" :key="item.id" ref="rows">{{ item.name }}</li>
886
+ </ul>
887
+ ```
888
+
889
+ ```javascript
890
+ this.$refs.rows; // [<li>, <li>, <li>, ...] — one entry per rendered row
891
+ ```
892
+
893
+ Entries are stored in registration order. Because reused rows are not
894
+ re-registered, the array order is not guaranteed to match DOM order after a
895
+ reorder — the same caveat Vue documents.
896
+
897
+ **Dynamic and function refs (`:ref`):**
898
+
899
+ Bind `ref` with `:ref` to compute it from an expression. If the expression
900
+ evaluates to a **string**, it is used as the ref name; if it evaluates to a
901
+ **function**, the function is called with the element on mount and with `null`
902
+ on unmount (a function ref). Function refs are convenient for `v-for` or dynamic
903
+ scenarios where you want full control over storage.
904
+
905
+ ```html
906
+ <!-- Dynamic name -->
907
+ <input :ref="fieldName">
908
+
909
+ <!-- Function ref -->
910
+ <li v-for="item in items" :key="item.id" :ref="el => registerRow(item.id, el)">
911
+ {{ item.name }}
912
+ </li>
913
+ ```
914
+
915
+ `:ref` is evaluated once at mount (it does not reactively re-run when the bound
916
+ value changes).
917
+
918
+ > **Note:** `$refs` is **not reactive**. Registering or clearing a ref never
919
+ > triggers a re-render, and `$refs` should not be used to drive reactive
920
+ > template output — read it from event handlers and lifecycle hooks instead.
921
+ > This matches Vue's behavior.
922
+
826
923
  ## Components
827
924
 
828
925
  ichigo.js components are real [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)
@@ -996,6 +1093,7 @@ Creates a new application instance.
996
1093
  - `$markRaw(obj)`: Marks an object as non-reactive (see [Marking Objects as Non-Reactive](#marking-objects-as-non-reactive))
997
1094
  - `$nextTick(callback)`: Runs a callback after the next DOM update
998
1095
  - `$emit(name, detail?, options?)`: Dispatches a custom event (see [Events](#events-emit))
1096
+ - `$refs`: Object of template references registered via the `ref` attribute (see [Template refs](#template-refs-refs)). Non-reactive.
999
1097
  - `$ctx`: Lifecycle/handler context with `element`, `vnode`, and `userData`
1000
1098
 
1001
1099
  ### defineComponent(tagName, options)
@@ -1027,4 +1125,4 @@ All trademarks are the property of their respective owners.
1027
1125
 
1028
1126
  ---
1029
1127
 
1030
- Built with ❤️ by [MintJams Inc.](https://github.com/mintjamsinc)
1128
+ Built with ❤️ by [MintJams Inc.](https://www.mintjams.jp/)
package/dist/ichigo.cjs CHANGED
@@ -179,6 +179,8 @@
179
179
  StandardDirectiveName["V_TEXT"] = "v-text";
180
180
  /** Focus management directive. */
181
181
  StandardDirectiveName["V_FOCUS"] = "v-focus";
182
+ /** Template reference directive (Vue's `ref` / `$refs`). */
183
+ StandardDirectiveName["REF"] = "ref";
182
184
  })(StandardDirectiveName || (StandardDirectiveName = {}));
183
185
 
184
186
  // This file was generated. Do not modify manually!
@@ -8440,6 +8442,13 @@
8440
8442
  * Flag to suppress onChange callbacks temporarily.
8441
8443
  */
8442
8444
  #suppressOnChange = false;
8445
+ /**
8446
+ * Flag forcing the next write through the `set` trap to stay in THIS scope's own
8447
+ * local store instead of walking up to an inherited binding of the same name.
8448
+ * Set only by {@link setLocal} for scope-local variables (e.g. v-for loop items),
8449
+ * which must shadow — never overwrite — a parent binding with the same key.
8450
+ */
8451
+ #localWrite = false;
8443
8452
  /**
8444
8453
  * Per-instance path aliases. Maps local variable names to their reactive source paths.
8445
8454
  * Scoped here instead of the global ReactiveProxy map so that v-for items each maintain
@@ -8518,8 +8527,13 @@
8518
8527
  setter(value);
8519
8528
  return true;
8520
8529
  }
8530
+ // For a key that is not local to this scope, an assignment normally
8531
+ // retargets to the nearest parent that owns it, so reassignments
8532
+ // (e.g. `count = count + 1` in a handler) mutate the original binding.
8533
+ // A scope-local write (setLocal — used for v-for loop variables) skips
8534
+ // this walk so the variable shadows, never clobbers, an inherited key.
8521
8535
  let target = obj;
8522
- if (!Reflect.has(target, key)) {
8536
+ if (!this.#localWrite && !Reflect.has(target, key)) {
8523
8537
  for (let parent = this.#parent; parent; parent = parent.#parent) {
8524
8538
  if (Reflect.has(parent.#local, key)) {
8525
8539
  target = parent.#local;
@@ -8694,6 +8708,25 @@
8694
8708
  set(key, value) {
8695
8709
  this.#local[key] = value;
8696
8710
  }
8711
+ /**
8712
+ * Sets a binding value in THIS scope's own local store, without walking up to a
8713
+ * parent scope. Used for scope-local variables (e.g. v-for loop items) that must
8714
+ * shadow — never overwrite — an inherited binding of the same name. Contrast with
8715
+ * {@link set}, which retargets a write of an inherited key to the owning parent so
8716
+ * that reassignments mutate the original; a loop variable named like a root data /
8717
+ * method / computed key (e.g. `t`) would otherwise clobber it.
8718
+ * @param key The binding name.
8719
+ * @param value The binding value.
8720
+ */
8721
+ setLocal(key, value) {
8722
+ this.#localWrite = true;
8723
+ try {
8724
+ this.#local[key] = value;
8725
+ }
8726
+ finally {
8727
+ this.#localWrite = false;
8728
+ }
8729
+ }
8697
8730
  /**
8698
8731
  * Gets a binding value.
8699
8732
  * @param key The binding name.
@@ -10789,26 +10822,30 @@
10789
10822
  * Sets item bindings for a VNode, handling nested destructuring if needed.
10790
10823
  */
10791
10824
  #setItemBindings(bindings, context) {
10825
+ // Loop variables are scope-local: they must shadow, never overwrite, an
10826
+ // inherited binding of the same name (a data / method / computed key on a
10827
+ // parent scope). setLocal writes to this iteration's own store so a loop
10828
+ // variable named like e.g. the i18n helper `t` cannot clobber it.
10792
10829
  if (this.#itemDestructure && Array.isArray(context.item)) {
10793
10830
  // Nested destructuring: spread array items to individual bindings
10794
10831
  // e.g., item = ['alice', 1] with itemDestructure = ['k', 'v']
10795
- // bindings.set('k', 'alice')
10796
- // bindings.set('v', 1)
10832
+ // bindings.setLocal('k', 'alice')
10833
+ // bindings.setLocal('v', 1)
10797
10834
  this.#itemDestructure.forEach((name, i) => {
10798
- bindings.set(name, context.item[i]);
10835
+ bindings.setLocal(name, context.item[i]);
10799
10836
  });
10800
10837
  }
10801
10838
  else if (this.#itemName) {
10802
10839
  // Simple item binding
10803
- bindings.set(this.#itemName, context.item);
10840
+ bindings.setLocal(this.#itemName, context.item);
10804
10841
  }
10805
10842
  if (this.#indexName) {
10806
10843
  // For objects, use objectKey if available; otherwise use numeric index
10807
- bindings.set(this.#indexName, context.objectKey ?? context.index);
10844
+ bindings.setLocal(this.#indexName, context.objectKey ?? context.index);
10808
10845
  }
10809
10846
  if (this.#thirdName) {
10810
10847
  // Third argument is always the numeric index
10811
- bindings.set(this.#thirdName, context.index);
10848
+ bindings.setLocal(this.#thirdName, context.index);
10812
10849
  }
10813
10850
  }
10814
10851
  /**
@@ -13312,6 +13349,240 @@
13312
13349
  }
13313
13350
  }
13314
13351
 
13352
+ // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13353
+ /**
13354
+ * Directive for registering template references (Vue's `ref` / `$refs`).
13355
+ *
13356
+ * Usage:
13357
+ * <input ref="search"> Static name. $refs.search === the element.
13358
+ * <my-card ref="card"></my-card> On a component: $refs.card === the host element.
13359
+ * <li v-for="i in items" ref="rows"> Inside v-for: $refs.rows === array of elements.
13360
+ * <input :ref="dynamicName"> Dynamic name from an expression (evaluated at mount).
13361
+ * <input :ref="el => collect(el)"> Function ref: called with the element on mount,
13362
+ * and with null on unmount (great for v-for / dynamic).
13363
+ *
13364
+ * Behavior notes:
13365
+ * - The reference is registered during the mount phase (before any `@mounted` hook fires) and
13366
+ * removed during the unmount phase, mirroring Vue's lifecycle for `$refs`.
13367
+ * - `$refs` is intentionally NON-reactive: registering or clearing a ref never schedules a render
13368
+ * and must not be used to drive reactive template output (this matches Vue).
13369
+ * - When the same `ref` name appears inside a `v-for` (i.e. one of the element's ancestors is a
13370
+ * `v-for` template), the entries are collected into an array, in registration order. Reused
13371
+ * v-for rows are not re-registered, so the array order is not guaranteed to match DOM order after
13372
+ * reordering — the same caveat Vue documents.
13373
+ * - Cleanup is identity-safe: a ref slot (or array entry) is only cleared when it still points at
13374
+ * THIS element, so a freshly mounted element that reused the name is never clobbered by the
13375
+ * teardown of the element it replaced.
13376
+ *
13377
+ * The directive is purely lifecycle-driven: it has no DOM updater and never templatizes the node.
13378
+ */
13379
+ class VRefDirective {
13380
+ /**
13381
+ * The virtual node to which this directive is applied.
13382
+ */
13383
+ #vNode;
13384
+ /**
13385
+ * The static ref name parsed from `ref="name"`. Undefined for the dynamic `:ref` form.
13386
+ */
13387
+ #staticName;
13388
+ /**
13389
+ * Expression evaluator for the dynamic `:ref` / `v-bind:ref` form. Undefined for static refs.
13390
+ */
13391
+ #evaluator;
13392
+ /**
13393
+ * The name under which this element was actually registered, retained so unmount removes the
13394
+ * exact same slot it created (the dynamic name is evaluated once at mount and may differ from
13395
+ * the source expression). Undefined when nothing was registered (function ref, or empty value).
13396
+ */
13397
+ #registeredName;
13398
+ /**
13399
+ * The function supplied by a function ref (`:ref="el => ..."`), retained so it can be invoked
13400
+ * with null on unmount. Undefined unless a function ref was used.
13401
+ */
13402
+ #functionRef;
13403
+ /**
13404
+ * Whether this ref is registered in array mode (collected because it lives inside a v-for).
13405
+ * Resolved lazily at mount time via {@link #isInsideForLoop}.
13406
+ */
13407
+ #asArray = false;
13408
+ /**
13409
+ * @param context The context for parsing the directive.
13410
+ */
13411
+ constructor(context) {
13412
+ this.#vNode = context.vNode;
13413
+ const attrName = context.attribute.name;
13414
+ const isDynamic = attrName !== StandardDirectiveName.REF; // ":ref" or "v-bind:ref"
13415
+ if (isDynamic) {
13416
+ // Dynamic / function ref: the value is an expression.
13417
+ const expression = context.attribute.value;
13418
+ if (expression && context.vNode.bindings) {
13419
+ this.#evaluator = ExpressionEvaluator.create(expression, context.vNode.bindings, context.vNode.vApplication.functionDependencies);
13420
+ }
13421
+ }
13422
+ else {
13423
+ // Static ref: the value is the literal name.
13424
+ const name = context.attribute.value.trim();
13425
+ this.#staticName = name.length > 0 ? name : undefined;
13426
+ }
13427
+ // Remove the directive attribute from the element so it does not linger in the DOM.
13428
+ this.#vNode.node.removeAttribute(attrName);
13429
+ }
13430
+ /**
13431
+ * @inheritdoc
13432
+ */
13433
+ get name() {
13434
+ return StandardDirectiveName.REF;
13435
+ }
13436
+ /**
13437
+ * @inheritdoc
13438
+ */
13439
+ get vNode() {
13440
+ return this.#vNode;
13441
+ }
13442
+ /**
13443
+ * @inheritdoc
13444
+ */
13445
+ get needsAnchor() {
13446
+ return false;
13447
+ }
13448
+ /**
13449
+ * @inheritdoc
13450
+ */
13451
+ get bindingsPreparer() {
13452
+ return undefined;
13453
+ }
13454
+ /**
13455
+ * @inheritdoc
13456
+ */
13457
+ get domUpdater() {
13458
+ // Refs are resolved once at mount; they are deliberately not reactive (see class docs).
13459
+ return undefined;
13460
+ }
13461
+ /**
13462
+ * @inheritdoc
13463
+ */
13464
+ get templatize() {
13465
+ return false;
13466
+ }
13467
+ /**
13468
+ * @inheritdoc
13469
+ */
13470
+ get dependentIdentifiers() {
13471
+ // This directive never drives reactive updates, so it contributes no dependencies.
13472
+ return [];
13473
+ }
13474
+ /**
13475
+ * @inheritdoc
13476
+ */
13477
+ get onMount() {
13478
+ // Register during the mount phase so the reference is available before any `@mounted`
13479
+ // hook runs (those fire a frame later via requestAnimationFrame).
13480
+ return () => this.#register();
13481
+ }
13482
+ /**
13483
+ * @inheritdoc
13484
+ */
13485
+ get onMounted() {
13486
+ return undefined;
13487
+ }
13488
+ /**
13489
+ * @inheritdoc
13490
+ */
13491
+ get onUpdate() {
13492
+ return undefined;
13493
+ }
13494
+ /**
13495
+ * @inheritdoc
13496
+ */
13497
+ get onUpdated() {
13498
+ return undefined;
13499
+ }
13500
+ /**
13501
+ * @inheritdoc
13502
+ */
13503
+ get onUnmount() {
13504
+ return undefined;
13505
+ }
13506
+ /**
13507
+ * @inheritdoc
13508
+ */
13509
+ get onUnmounted() {
13510
+ // Clear during the unmount phase. The element reference is still valid here.
13511
+ return () => this.#unregister();
13512
+ }
13513
+ /**
13514
+ * @inheritdoc
13515
+ */
13516
+ destroy() {
13517
+ // No additional cleanup beyond onUnmounted; kept for interface completeness.
13518
+ }
13519
+ /**
13520
+ * Registers this element under its resolved ref name (or invokes its function ref).
13521
+ */
13522
+ #register() {
13523
+ const element = this.#vNode.node;
13524
+ if (this.#evaluator) {
13525
+ // Dynamic / function ref.
13526
+ let value;
13527
+ try {
13528
+ value = this.#evaluator.evaluate();
13529
+ }
13530
+ catch {
13531
+ // Evaluation errors are already logged by ExpressionEvaluator; nothing to register.
13532
+ return;
13533
+ }
13534
+ if (typeof value === 'function') {
13535
+ this.#functionRef = value;
13536
+ this.#functionRef(element);
13537
+ return;
13538
+ }
13539
+ if (value === null || value === undefined || value === '') {
13540
+ return; // Nothing to register under an empty dynamic name.
13541
+ }
13542
+ this.#registeredName = String(value);
13543
+ }
13544
+ else if (this.#staticName) {
13545
+ this.#registeredName = this.#staticName;
13546
+ }
13547
+ if (!this.#registeredName) {
13548
+ return;
13549
+ }
13550
+ this.#asArray = this.#isInsideForLoop();
13551
+ this.#vNode.vApplication.registerRef(this.#registeredName, element, this.#asArray);
13552
+ }
13553
+ /**
13554
+ * Removes this element's reference (or invokes its function ref with null).
13555
+ */
13556
+ #unregister() {
13557
+ if (this.#functionRef) {
13558
+ this.#functionRef(null);
13559
+ this.#functionRef = undefined;
13560
+ return;
13561
+ }
13562
+ if (this.#registeredName) {
13563
+ this.#vNode.vApplication.unregisterRef(this.#registeredName, this.#vNode.node);
13564
+ this.#registeredName = undefined;
13565
+ }
13566
+ }
13567
+ /**
13568
+ * Determines whether this element is rendered inside a `v-for` by walking up the VNode tree.
13569
+ * A `v-for` multiplies its template into N rows, so a static ref name on (or within) a row is
13570
+ * shared by N elements and must be collected into an array — matching Vue's documented
13571
+ * "ref inside v-for yields an array" behavior. The walk inspects ancestors only: the directive
13572
+ * lives on a cloned row (or a descendant of one), and the clone's parent chain leads back to the
13573
+ * `v-for` template node, which still carries the `v-for` directive.
13574
+ */
13575
+ #isInsideForLoop() {
13576
+ for (let ancestor = this.#vNode.parentVNode; ancestor; ancestor = ancestor.parentVNode) {
13577
+ const hasFor = ancestor.directiveManager?.directives?.some(d => d.name === StandardDirectiveName.V_FOR);
13578
+ if (hasFor) {
13579
+ return true;
13580
+ }
13581
+ }
13582
+ return false;
13583
+ }
13584
+ }
13585
+
13315
13586
  // Copyright (c) 2025 MintJams Inc. Licensed under MIT License.
13316
13587
  /**
13317
13588
  * The directive parser for standard directives.
@@ -13356,15 +13627,33 @@
13356
13627
  context.attribute.name === StandardDirectiveName.V_TEXT ||
13357
13628
  // v-focus, v-focus.<modifier>
13358
13629
  context.attribute.name === StandardDirectiveName.V_FOCUS ||
13359
- context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".")) {
13630
+ context.attribute.name.startsWith(StandardDirectiveName.V_FOCUS + ".") ||
13631
+ // ref, :ref, v-bind:ref
13632
+ this.#isRefAttribute(context.attribute.name)) {
13360
13633
  return true;
13361
13634
  }
13362
13635
  return false;
13363
13636
  }
13637
+ /**
13638
+ * Determines whether the attribute is a template reference (`ref`, `:ref`, or `v-bind:ref`).
13639
+ * The dynamic forms also match the generic v-bind shorthand, so {@link parse} dispatches them
13640
+ * to VRefDirective explicitly before the v-bind branch is reached.
13641
+ */
13642
+ #isRefAttribute(name) {
13643
+ return name === StandardDirectiveName.REF ||
13644
+ name === ":" + StandardDirectiveName.REF ||
13645
+ name === StandardDirectiveName.V_BIND + ":" + StandardDirectiveName.REF;
13646
+ }
13364
13647
  /**
13365
13648
  * @inheritdoc
13366
13649
  */
13367
13650
  parse(context) {
13651
+ // ref, :ref, v-bind:ref
13652
+ // Checked before the v-bind branch below, since ":ref" / "v-bind:ref" also match the
13653
+ // generic v-bind shorthand but must be handled as template references, not attribute binds.
13654
+ if (this.#isRefAttribute(context.attribute.name)) {
13655
+ return new VRefDirective(context);
13656
+ }
13368
13657
  if (context.attribute.name === StandardDirectiveName.V_IF) {
13369
13658
  return new VIfDirective(context);
13370
13659
  }
@@ -13801,6 +14090,17 @@
13801
14090
  * The watcher manager for this application.
13802
14091
  */
13803
14092
  #watcher;
14093
+ /**
14094
+ * Template references registered by the `ref` directive, exposed to data(), methods, and
14095
+ * expressions as `$refs` (Vue's `$refs`). Each entry is either a single Element or, for a
14096
+ * `ref` used inside `v-for`, an array of Elements.
14097
+ *
14098
+ * This object is intentionally marked raw (non-reactive): mutating it when an element mounts or
14099
+ * unmounts must never schedule a render, and it must not be used to drive reactive template
14100
+ * output — matching Vue's semantics for `$refs`. It is held as a direct field (in addition to
14101
+ * being injected as the `$refs` binding) so registration is a plain property write.
14102
+ */
14103
+ #refs = {};
13804
14104
  /**
13805
14105
  * Flag to indicate if an update is already scheduled.
13806
14106
  */
@@ -13901,6 +14201,13 @@
13901
14201
  get functionDependencies() {
13902
14202
  return this.#functionDependencies;
13903
14203
  }
14204
+ /**
14205
+ * Gets the template references registered via the `ref` directive (Vue's `$refs`). The same
14206
+ * object is exposed to data(), methods, and expressions as `$refs`. Non-reactive by design.
14207
+ */
14208
+ get refs() {
14209
+ return this.#refs;
14210
+ }
13904
14211
  /**
13905
14212
  * Mounts the application.
13906
14213
  * @param target The CSS selector string or HTMLElement to mount the application to.
@@ -14029,6 +14336,56 @@
14029
14336
  }
14030
14337
  return identifiers;
14031
14338
  }
14339
+ /**
14340
+ * Registers a template reference under the given name. Called by the `ref` directive during the
14341
+ * mount phase. When {@param asArray} is true (the element lives inside a `v-for`), references
14342
+ * accumulate into an array in registration order; otherwise the name holds a single element.
14343
+ *
14344
+ * Mutates the raw (non-reactive) `$refs` object directly, so this never schedules a render.
14345
+ *
14346
+ * @param name The ref name.
14347
+ * @param element The element (or component host element) to register.
14348
+ * @param asArray Whether to collect into an array (refs used inside a v-for).
14349
+ */
14350
+ registerRef(name, element, asArray) {
14351
+ if (asArray) {
14352
+ const current = this.#refs[name];
14353
+ if (Array.isArray(current)) {
14354
+ if (!current.includes(element)) {
14355
+ current.push(element);
14356
+ }
14357
+ }
14358
+ else {
14359
+ this.#refs[name] = [element];
14360
+ }
14361
+ return;
14362
+ }
14363
+ this.#refs[name] = element;
14364
+ }
14365
+ /**
14366
+ * Removes a template reference registered under the given name. Called by the `ref` directive
14367
+ * during the unmount phase. The removal is identity-safe: an entry is only cleared when it still
14368
+ * points at {@param element}, so a newly mounted element that reused the name is never clobbered
14369
+ * by the teardown of the element it replaced.
14370
+ *
14371
+ * @param name The ref name.
14372
+ * @param element The element whose registration should be removed.
14373
+ */
14374
+ unregisterRef(name, element) {
14375
+ const current = this.#refs[name];
14376
+ if (Array.isArray(current)) {
14377
+ const index = current.indexOf(element);
14378
+ if (index !== -1) {
14379
+ current.splice(index, 1);
14380
+ }
14381
+ if (current.length === 0) {
14382
+ delete this.#refs[name];
14383
+ }
14384
+ }
14385
+ else if (current === element) {
14386
+ delete this.#refs[name];
14387
+ }
14388
+ }
14032
14389
  /**
14033
14390
  * Initializes bindings from data, computed properties, and methods.
14034
14391
  * @returns The initialized bindings object.
@@ -14049,6 +14406,10 @@
14049
14406
  this.#bindings.set('$nextTick', (callback) => this.#nextTick(callback));
14050
14407
  this.#bindings.set('$markRaw', (obj) => ReactiveProxy.markRaw(obj));
14051
14408
  this.#bindings.set('$emit', (name, detail, options) => this.#emit(name, detail, options));
14409
+ // Inject the (non-reactive) $refs container. Marking it raw prevents VBindings from wrapping
14410
+ // it in a reactive proxy, so populating it from the ref directive never schedules a render.
14411
+ ReactiveProxy.markRaw(this.#refs);
14412
+ this.#bindings.set('$refs', this.#refs);
14052
14413
  // Add methods
14053
14414
  if (this.#options.methods) {
14054
14415
  for (const [key, method] of Object.entries(this.#options.methods)) {