aurelia/aurelia

View on GitHub
packages/state/src/state-getter-binding.ts

Summary

Maintainability
A
0 mins
Test Coverage

import { IDisposable, IIndexable, IServiceLocator, type Writable } from '@aurelia/kernel';
import {
  connectable,
  IObserverLocatorBasedConnectable,
} from '@aurelia/runtime';
import {
  IStore,
  type Unsubscribable,
  type IStoreSubscriber
} from './interfaces';
import { createStateBindingScope, isSubscribable } from './state-utilities';
import { IBinding, Scope, type IOverrideContext, } from '@aurelia/runtime-html';

/**
 * A binding that handles the connection of the global state to a property of a target object
 */
export interface StateGetterBinding extends IObserverLocatorBasedConnectable, IServiceLocator { }
export class StateGetterBinding implements IBinding, IStoreSubscriber<object> {
  public isBound: boolean = false;

  /** @internal */
  private _scope?: Scope | undefined;

  private readonly $get: (s: unknown) => unknown;
  private readonly target: IIndexable;
  private readonly key: PropertyKey;

  /** @internal */ private readonly _store: IStore<object>;
  /** @internal */ private _value: unknown = void 0;
  /** @internal */ private _sub?: IDisposable | Unsubscribable | (() => void) = void 0;
  /** @internal */ private _updateCount = 0;

  public constructor(
    target: object,
    prop: PropertyKey,
    store: IStore<object>,
    getValue: (s: unknown) => unknown,
  ) {
    this._store = store;
    this.$get = getValue;
    this.target = target as IIndexable;
    this.key = prop;
  }

  private updateTarget(value: unknown) {
    const target = this.target;
    const prop = this.key;
    const updateCount = this._updateCount++;
    const isCurrentValue = () => updateCount === this._updateCount - 1;
    this._unsub();

    if (isSubscribable(value)) {
      this._sub = value.subscribe($value => {
        if (isCurrentValue()) {
          target[prop] = $value;
        }
      });
      return;
    }

    if (value instanceof Promise) {
      void value.then($value => {
        if (isCurrentValue()) {
            target[prop] = $value;
        }
      }, () => {/* todo: don't ignore */});
      return;
    }

    target[prop] = value;
  }

  public bind(_scope: Scope): void {
    if (this.isBound) {
      return;
    }
    const state = this._store.getState();
    this._scope = createStateBindingScope(state, _scope);
    this._store.subscribe(this);
    this.updateTarget(this._value = this.$get(state));
    this.isBound = true;
  }

  public unbind(): void {
    if (!this.isBound) {
      return;
    }
    this.isBound = false;
    this._unsub();
    // also disregard incoming future value of promise resolution if any
    this._updateCount++;
    this._scope = void 0;
    this._store.unsubscribe(this);
  }

  public handleStateChange(state: object): void {
    const _scope = this._scope!;
    const overrideContext = _scope.overrideContext as Writable<IOverrideContext>;
    _scope.bindingContext = overrideContext.bindingContext = overrideContext.$state = state;
    const value = this.$get(this._store.getState());

    if (value === this._value) {
      return;
    }
    this._value = value;
    this.updateTarget(value);
  }

  /** @internal */
  private _unsub() {
    if (typeof this._sub === 'function') {
      this._sub();
    } else if (this._sub !== void 0) {
      (this._sub as IDisposable).dispose?.();
      (this._sub as Unsubscribable).unsubscribe?.();
    }
    this._sub = void 0;
  }
}
connectable(StateGetterBinding, null!);