All files / shell/src/component reference.ts

100% Statements 106/106
100% Branches 20/20
100% Functions 6/6
100% Lines 106/106

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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 1071x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 7x 7x 5x 5x 5x 5x 5x 5x 5x 13x 13x 13x 13x 15x 15x 15x 15x 15x 12x 3x 11x 11x 11x 15x 15x 15x 15x 12x 13x 5x 5x 2x 3x 5x 5x 7x 7x 7x 7x 7x 7x 7x 7x 9x 9x 7x 7x 7x 7x 7x  
/* eslint-disable @typescript-eslint/unbound-method */
 
import { assign, Mutable } from '@sgrud/core';
import { Component } from './component';
import { references } from './runtime';
 
/**
 * {@link Component} prototype property decorator factory. Applying this
 * **Reference** decorator to a property of a registered {@link Component} while
 * supplying the `reference`ing {@link JSX.Key}] and, optionally, an array of
 * event names to `observe`, will replace the decorated property with a getter
 * returning the `reference`d node, once rendered. If an array of event names is
 * supplied, whenever one of those `observe`d events is emitted by the
 * `reference`d node, the {@link Component.referenceChangedCallback} of the
 * {@link Component} is called with the `reference` key, the `reference`d node
 * and the emitted event.
 *
 * @param reference - The `reference`ing {@link JSX.Key}.
 * @param observe - An array of event names to `observe`.
 * @returns A {@link Component} prototype property decorator.
 *
 * @example
 * **Reference** a node:
 * ```tsx
 * import { Component, Reference } from '@sgrud/shell';
 *
 * declare global {
 *   interface HTMLElementTagNameMap {
 *     'example-component': ExampleComponent;
 *   }
 * }
 *
 * ⁠@Component('example-component')
 * export class ExampleComponent extends HTMLElement implements Component {
 *
 *   ⁠@Reference('example-key')
 *   private readonly span?: HTMLSpanElement;
 *
 *   public get template(): JSX.Element {
 *     return <span key="example-key"></span>;
 *   }
 *
 * }
 * ```
 *
 * @see {@link Component}
 */
export function Reference(
  reference: JSX.Key,
  observe?: (keyof HTMLElementEventMap)[]
) {
 
  /**
   * @param prototype - The {@link Component} `prototype` to be decorated.
   * @param propertyKey - The {@link Component} property to be decorated.
   */
  return function(prototype: Component, propertyKey: PropertyKey): void {
    if (!prototype.observedReferences) {
      const connectedCallback = prototype.connectedCallback;
 
      prototype.connectedCallback = function(this: Component): void {
        const listeners = {} as Record<JSX.Key, (event: Event) => void>;
        const renderComponent = this.renderComponent;
 
        this.renderComponent = function(this: Component): void {
          renderComponent?.call(this);
          const refs = references(this.shadowRoot!);
 
          if (refs) {
            for (const key in prototype.observedReferences) {
              const node = refs.get(key);
 
              if (node) {
                for (const type of prototype.observedReferences[key]) {
                  node.addEventListener(type, listeners[key] ||= (event) => {
                    this.referenceChangedCallback?.(key, node, event);
                  }, {
                    once: true,
                    passive: true
                  });
                }
              }
            }
          }
        };
 
        return connectedCallback
          ? connectedCallback.call(this)
          : this.renderComponent();
      };
    }
 
    assign((prototype as Mutable<Component>).observedReferences ||= {}, {
      [reference]: observe || []
    });
 
    Object.defineProperty(prototype, propertyKey, {
      enumerable: true,
      get(this: Component): Node | undefined {
        return references(this.shadowRoot!)?.get(reference);
      },
      set: Function.prototype as (...args: any[]) => any
    });
  };
 
}