aurelia/aurelia

View on GitHub
packages/router/src/routing-hook.ts

Summary

Maintainability
C
1 day
Test Coverage
/**
 *
 * NOTE: This file is still WIP and will go through at least one more iteration of refactoring, commenting and clean up!
 * In its current state, it is NOT a good source for learning about the inner workings and design of the router.
 *
 */
import { ComponentAppellation, IComponentAndOrViewportOrNothing, RouteableComponentType } from './interfaces';
import { RoutingInstruction } from './instructions/routing-instruction';
import { Navigation } from './navigation';
import { Viewport } from './endpoints/viewport';
import { InstructionComponent } from './instructions/instruction-component';
import { InstructionEndpoint } from './instructions/instruction-endpoint';

/**
 * Public API
 */
export type RoutingHookType = 'beforeNavigation' | 'transformFromUrl' | 'transformToUrl' | 'transformTitle';
/**
 * Public API
 */
export type BeforeNavigationHookFunction = (routingInstructions: RoutingInstruction[], navigationInstruction: Navigation) => Promise<boolean | RoutingInstruction[]>;
/**
 * Public API
 */
export type TransformFromUrlHookFunction = (url: string, navigationInstruction: Navigation) => Promise<string | RoutingInstruction[]>;
/**
 * Public API
 */
export type TransformToUrlHookFunction = (state: string | RoutingInstruction[], navigationInstruction: Navigation) => Promise<string | RoutingInstruction[]>;
/**
 * Public API
 */
export type TransformTitleHookFunction = (title: string | RoutingInstruction[], navigationInstruction: Navigation) => Promise<string | RoutingInstruction[]>;

/**
 * @internal
 */
export type RoutingHookFunction = BeforeNavigationHookFunction | TransformFromUrlHookFunction | TransformToUrlHookFunction | TransformTitleHookFunction;
/**
 * @internal
 */
export type RoutingHookParameter = string | RoutingInstruction[];
/**
 * @internal
 */
export type RoutingHookResult = boolean | string | RoutingInstruction[];

export type RoutingHookTarget = ComponentAppellation | IComponentAndOrViewportOrNothing;

/**
 * Public API
 */
export type RoutingHookIdentity = number;

/**
 * Public API
 */
export interface IRoutingHookOptions {
  /**
   * What event/when to hook. Defaults to BeforeNavigation
   */
  type?: RoutingHookType;
  /**
   * What to hook. If omitted, everything is included
   */
  include?: RoutingHookTarget[];
  /**
   * What not to hook. If omitted, nothing is excluded
   */
  exclude?: RoutingHookTarget[];
}

/**
 * Public API
 */
export interface IRoutingHookDefinition {
  hook: RoutingHookFunction;
  options: IRoutingHookOptions;
}

/**
 * @internal - Shouldn't be used directly
 */
export class RoutingHook {
  public static hooks: Record<RoutingHookType, RoutingHook[]> = {
    beforeNavigation: [],
    transformFromUrl: [],
    transformToUrl: [],
    transformTitle: [],
  };

  private static lastIdentity: number = 0;

  public type: RoutingHookType = 'beforeNavigation';
  public includeTargets: Target[] = [];
  public excludeTargets: Target[] = [];

  public constructor(
    public hook: RoutingHookFunction,
    options: IRoutingHookOptions,
    public id: RoutingHookIdentity
  ) {
    if (options.type !== void 0) {
      this.type = options.type;
    }

    for (const target of options.include ?? []) {
      this.includeTargets.push(new Target(target));
    }
    for (const target of options.exclude ?? []) {
      this.excludeTargets.push(new Target(target));
    }
  }

  public static add(beforeNavigationHookFunction: BeforeNavigationHookFunction, options?: IRoutingHookOptions): RoutingHookIdentity;
  public static add(transformFromUrlHookFunction: TransformFromUrlHookFunction, options?: IRoutingHookOptions): RoutingHookIdentity;
  public static add(transformToUrlHookFunction: TransformToUrlHookFunction, options?: IRoutingHookOptions): RoutingHookIdentity;
  public static add(transformTitleHookFunction: TransformTitleHookFunction, options?: IRoutingHookOptions): RoutingHookIdentity;
  public static add(hookFunction: RoutingHookFunction, options?: IRoutingHookOptions): RoutingHookIdentity;
  public static add(hookFunction: RoutingHookFunction, options?: IRoutingHookOptions): RoutingHookIdentity {
    const hook = new RoutingHook(hookFunction, options ?? {}, ++this.lastIdentity);

    this.hooks[hook.type].push(hook);

    return this.lastIdentity;
  }

  public static remove(id: RoutingHookIdentity): void {
    for (const type in this.hooks) {
      if (Object.prototype.hasOwnProperty.call(this.hooks, type)) {
        const index = this.hooks[type as RoutingHookType].findIndex(hook => hook.id === id);
        if (index >= 0) {
          this.hooks[type as RoutingHookType].splice(index, 1);
        }
      }
    }
  }

  public static removeAll(): void {
    for (const type in this.hooks) {
      this.hooks[type as RoutingHookType] = [];
    }
  }

  public static async invokeBeforeNavigation(routingInstructions: RoutingInstruction[], navigationInstruction: Navigation): Promise<boolean | RoutingInstruction[]> {
    return this.invoke('beforeNavigation', navigationInstruction, routingInstructions) as Promise<boolean | RoutingInstruction[]>;
  }
  public static async invokeTransformFromUrl(url: string, navigationInstruction: Navigation): Promise<string | RoutingInstruction[]> {
    return this.invoke('transformFromUrl', navigationInstruction, url) as Promise<string | RoutingInstruction[]>;
  }
  public static async invokeTransformToUrl(state: string | RoutingInstruction[], navigationInstruction: Navigation): Promise<string | RoutingInstruction[]> {
    return this.invoke('transformToUrl', navigationInstruction, state) as Promise<string | RoutingInstruction[]>;
  }
  public static async invokeTransformTitle(title: string | RoutingInstruction[], navigationInstruction: Navigation): Promise<string | RoutingInstruction[]> {
    return this.invoke('transformTitle', navigationInstruction, title) as Promise<string | RoutingInstruction[]>;
  }

  public static async invoke(type: RoutingHookType, navigationInstruction: Navigation, arg: RoutingHookParameter): Promise<RoutingHookResult> {
    let outcome: RoutingHookResult = arg;
    for (const hook of this.hooks[type]) {
      if (!hook.wantsMatch || hook.matches(arg)) {
        outcome = await hook.invoke(navigationInstruction, arg);
        if (typeof outcome === 'boolean') {
          if (!outcome) {
            return false;
          }
        } else {
          arg = outcome;
        }
      }
    }
    return outcome;
  }

  public get wantsMatch(): boolean {
    return this.includeTargets.length > 0 || this.excludeTargets.length > 0;
  }

  public matches(routingInstructions: RoutingHookParameter): boolean {
    if (this.includeTargets.length && !this.includeTargets.some(target => target.matches(routingInstructions as RoutingInstruction[]))) {
      return false;
    }
    if (this.excludeTargets.length && this.excludeTargets.some(target => target.matches(routingInstructions as RoutingInstruction[]))) {
      return false;
    }
    return true;
  }

  public invoke(navigationInstruction: Navigation, arg: RoutingHookParameter): Promise<RoutingHookResult> {
    // TODO: Fix the type here
    return this.hook(arg as any, navigationInstruction);
  }
}

class Target {
  public componentType: RouteableComponentType | null = null;
  public componentName: string | null = null;
  public viewport: Viewport | null = null;
  public viewportName: string | null = null;

  public constructor(target: RoutingHookTarget) {
    if (typeof target === 'string') {
      this.componentName = target;
    } else if (InstructionComponent.isType(target as RouteableComponentType)) {
      this.componentType = target as RouteableComponentType;
      this.componentName = InstructionComponent.getName(target as RouteableComponentType);
    } else {
      const cvTarget = target as IComponentAndOrViewportOrNothing;
      if (cvTarget.component != null) {
        this.componentType = InstructionComponent.isType(cvTarget.component)
          ? InstructionComponent.getType(cvTarget.component)
          : null;
        this.componentName = InstructionComponent.getName(cvTarget.component);
      }
      if (cvTarget.viewport != null) {
        this.viewport = InstructionEndpoint.isInstance(cvTarget.viewport) ? cvTarget.viewport : null;
        this.viewportName = InstructionEndpoint.getName(cvTarget.viewport);
      }
    }
  }

  public matches(routingInstructions: RoutingInstruction[]): boolean {
    const instructions = routingInstructions.slice();
    if (!instructions.length) {
      // instructions.push(new RoutingInstruction(''));
      instructions.push(RoutingInstruction.create('') as RoutingInstruction);
    }
    for (const instruction of instructions) {
      if ((this.componentName !== null && this.componentName === instruction.component.name) ||
        (this.componentType !== null && this.componentType === instruction.component.type) ||
        (this.viewportName !== null && this.viewportName === instruction.endpoint.name) ||
        (this.viewport !== null && this.viewport === instruction.endpoint.instance)) {
        return true;
      }
    }
    return false;
  }
}