All files / src/components Show.ts

100% Statements 33/33
100% Branches 10/10
100% Functions 1/1
100% Lines 33/33

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 831x 1x                                                                           1x 5x 5x   5x 5x 5x   5x 8x     8x 3x   3x   8x 8x   8x   7x 7x   7x 3x 3x 3x 7x 7x 8x 1x 1x     8x 7x 7x 5x   5x 5x  
import { createEffect } from '../primitives/effect';
import { createRoot, onCleanup } from '../lifecycle/lifecycle';
import type { Disposer, SignalGetter } from '../types';
 
/**
 * Props for the Show component that conditionally renders content.
 */
type ShowProps<T> = {
  /** Signal that determines whether to show children or fallback */
  when: SignalGetter<T>;
  /** Optional function that returns content to show when condition is falsy */
  fallback?: () => Node;
  /** Function that returns content to show when condition is truthy */
  children: () => Node;
};
 
/**
 * Conditionally renders content based on a reactive condition.
 *
 * The Show component efficiently switches between showing children or fallback content
 * based on the truthiness of the `when` signal. Each conditional branch gets its own
 * reactive scope for proper cleanup when switching.
 *
 * @param props Configuration object with when, children, and optional fallback
 * @returns A DocumentFragment that conditionally contains the rendered content
 *
 * @example
 * ```typescript
 * const [isLoggedIn, setLoggedIn] = createSignal(false);
 *
 * const content = Show({
 *   when: isLoggedIn,
 *   fallback: () => h.p('Please log in'),
 *   children: () => h.p('Welcome back!')
 * });
 *
 * // Shows "Please log in" initially, switches to "Welcome back!" when logged in
 * ```
 */
export function Show<T>(props: ShowProps<T>): Node {
  const { when, fallback, children } = props;
  const container = document.createDocumentFragment();
  // End marker provides stable insertion point for conditional content
  const endMarker = document.createTextNode('');
  container.appendChild(endMarker);
  let currentDisposer: Disposer | null = null;
 
  createEffect(() => {
    const condition = !!when();
 
    // Always clean up the previous state before rendering the new one
    if (currentDisposer) {
      currentDisposer();
      // Removed: container.textContent = ''; (invalid on DocumentFragment)
    }
 
    const renderer = condition ? children : fallback;
    let branchElement: Node | null = null;  // Local to this effect run
 
    if (renderer) {
      // Create a new root for the branch, so it gets its own lifecycle
      currentDisposer = createRoot(() => {
        branchElement = renderer();
        // Added: Explicit cleanup to remove the node from DOM on dispose
        onCleanup(() => {
          if (branchElement && branchElement.parentNode) {
            branchElement.parentNode.removeChild(branchElement);
          }
        });
      });
    } else {
      currentDisposer = null;
    }
 
    // Added: Insert the new branch before the endMarker (handles fragment initial / DOM updates)
    if (branchElement) {
      endMarker.parentNode!.insertBefore(branchElement, endMarker);
    }
  });
 
  return container;
}