All files / core/src binding.ts

100% Statements 50/50
100% Branches 21/21
100% Functions 1/1
100% Lines 50/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 851x 1x 1x                                                   1x 7x 7x 7x 7x 7x 7x   7x 1x 1x 1x 1x   7x 1x 1x 1x 1x   5x 5x   5x 5x   5x 5x 5x 14x 11x   9x 9x 9x 14x 5x 5x   5x 5x 5x 14x   3x 3x 3x 14x 5x 5x   5x 5x 5x 5x 5x  
import { anchor } from './anchor.js';
import { captureStack } from './exception.js';
import { subscribe } from './subscription.js';
import type { ObjLike, StateBinding, StateUnsubscribe } from './types.js';
 
/**
 * Creates a two-way binding between a source and target reactive object properties.
 *
 * This function establishes a bidirectional data flow where changes to either the source
 * or target property will automatically update the other. Both objects must be reactive
 * (created with the anchor system) for the binding to work properly.
 *
 * Note: This binding is only for a single property and is non-recursive. For object
 * properties, only the reference is bound, not the object's internal properties.
 *
 * @template T - The type of the source value
 * @template B - The type of the source binding key (defaults to 'value')
 * @template S extends ObjLike - The type of the target object
 *
 * @param source - The source binding which can be either:
 *   - A reactive object (defaulting to bind to its 'value' property)
 *   - A tuple of [reactiveObject, propertyName] to specify a custom property
 * @param target - The target reactive object
 * @param targetKey - The property name in the target object to bind to
 *
 * @returns A function that can be called to unsubscribe and remove the binding
 * @throws Will throw an error if either source or target objects are not reactive
 */
export function binding<T, B, S extends ObjLike = ObjLike>(
  source: StateBinding<T, B>,
  target: S,
  targetKey: keyof S
): StateUnsubscribe {
  const _source = (Array.isArray(source) ? source[0] : source) as ObjLike;
  const sourceKey = (Array.isArray(source) ? source[1] : 'value') as string;
 
  if (!anchor.has(_source)) {
    const error = new Error('State is not reactive.');
    captureStack.violation.derivation('Attempted to bind state from a non-reactive state.', error);
    return () => {};
  }
 
  if (!anchor.has(target)) {
    const error = new Error('State is not reactive.');
    captureStack.violation.derivation('Attempted to bind state to a non-reactive state.', error);
    return () => {};
  }
 
  const rawSource = anchor.get(_source);
  const rawTarget = anchor.get(target);
 
  let updatingSource = false;
  let updatingTarget = false;
 
  const leaveSource = subscribe(
    _source,
    () => {
      if (updatingSource) return;
      if (rawTarget[targetKey] === rawSource[sourceKey]) return;
 
      updatingTarget = true;
      target[targetKey] = rawSource[sourceKey] as never;
      updatingTarget = false;
    },
    false
  );
 
  const leaveTarget = subscribe(
    target,
    (_, event) => {
      if (updatingTarget || event.type === 'init') return;
 
      updatingSource = true;
      _source[sourceKey] = rawTarget[targetKey] as never;
      updatingSource = false;
    },
    false
  );
 
  return () => {
    leaveSource();
    leaveTarget();
  };
}