All files / shell/src/component fluctuate.ts

100% Statements 87/87
100% Branches 11/11
100% Functions 3/3
100% Lines 87/87

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 881x 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 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x 7x 6x 6x 6x 6x 6x 6x 7x 7x 7x 7x 7x 7x 7x 7x 7x 4x 7x 7x 7x 7x 6x 6x 6x 7x 1x 1x  
import { Mutable, assign } from '@sgrud/core';
import { ObservableInput, from } from 'rxjs';
import { Component } from './component';
 
/**
 * {@link Component} prototype property decorator factory. Applying this
 * **Fluctuate** decorator to a property of a custom {@link Component} while
 * supplying a `streamFactory` that returns an {@link ObservableInput} upon
 * invocation will subscribe the {@link Component.fluctuationChangedCallback}
 * method to each emission from this {@link ObservableInput} and replace the
 * decorated property with a getter returning its last emitted value. Further,
 * the resulting subscription, referenced by the decorated property, is assigned
 * to the {@link Component.observedFluctuations} property and may be terminated
 * by unsubscribing manually. Finally, the {@link Component} will seize to
 * **Fluctuate** automatically when it's disconnected from the {@link Document}.
 *
 * @param streamFactory - A forward reference to an {@link ObservableInput}.
 * @returns A {@link Component} prototype property decorator.
 *
 * @example
 * A {@link Component} that **Fluctuate**s:
 * ```tsx
 * import { Component, Fluctuate } from '@sgrud/shell';
 * import { fromEvent } from 'rxjs';
 *
 * declare global {
 *   interface HTMLElementTagNameMap {
 *     'example-component': ExampleComponent;
 *   }
 * }
 *
 * ⁠@Component('example-component')
 * export class ExampleComponent extends HTMLElement implements Component {
 *
 *   ⁠@Fluctuate(() => fromEvent(document, 'click'))
 *   private readonly pointer?: MouseEvent;
 *
 *   public get template(): JSX.Element {
 *     return <span>Clicked at ({this.pointer?.x}, {this.pointer?.y})</span>;
 *   }
 *
 * }
 * ```
 *
 * @see {@link Component}
 */
export function Fluctuate(streamFactory: () => ObservableInput<unknown>) {
 
  /**
   * @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 {
    // eslint-disable-next-line @typescript-eslint/unbound-method
    const { connectedCallback, disconnectedCallback } = prototype;
 
    prototype.connectedCallback = function(this: Component): void {
      let fluctuation: unknown;
 
      assign((this as Mutable<Component>).observedFluctuations ||= {}, {
        [propertyKey]: from(streamFactory()).subscribe((next) => {
          this.fluctuationChangedCallback?.(
            propertyKey,
            fluctuation,
            fluctuation = next
          );
        })
      });
 
      Object.defineProperty(this, propertyKey, {
        enumerable: true,
        get: (): unknown => fluctuation,
        set: Function.prototype as (...args: any[]) => any
      });
 
      return connectedCallback
        ? connectedCallback.call(this)
        : this.renderComponent?.();
    };
 
    prototype.disconnectedCallback = function(this: Component): void {
      this.observedFluctuations![propertyKey].unsubscribe();
      disconnectedCallback?.call(this);
    };
  };
 
}