import {computed, makeObservable} from 'mobx'; import type {Dict, EmptyObjectPatch} from 'tslang'; import {isQueryIdsMatched} from '../@utils'; import type {IHistory} from '../history'; import {RouteBuilder} from '../route-builder'; import type {Router, RouterNavigateOptions} from '../router'; import type {RouteMatchEntry, RouteSource} from './route-match'; export type GeneralSegmentDict = Dict; export type GeneralQueryDict = Dict; export type GeneralParamDict = Dict; export type RouteMatchSharedToParamDict = TRouteMatchShared extends RouteMatchShared ? TParamDict : never; export interface RouteMatchBuildOptions { /** * Whether to leave this match's group. */ leave?: boolean; /** * Parallel route groups to leave. */ leaves?: TGroupName | TGroupName[]; } export interface RouteMatchNavigateOptions extends RouteMatchBuildOptions, RouterNavigateOptions {} export interface RouteMatchSharedOptions { match: string | RegExp; query: Map; group: string | undefined; metadata: object | undefined; } export abstract class RouteMatchShared< TParamDict extends GeneralParamDict = GeneralParamDict, TSpecificGroupName extends string | undefined = string | undefined, TGroupName extends string = string, TMetadata extends object = object, > { /** * Name of this `RouteMatch`, correspondent to the field name of route * schema. */ readonly $name: string; /** * Group of this `RouteMatch`, specified in the root route. */ readonly $group: TSpecificGroupName | undefined; readonly $metadata: TMetadata; /** * Parent of this route match. */ readonly $parent: RouteMatchShared | undefined; readonly $router: Router; /** @internal */ readonly _source: RouteSource; /** @internal */ readonly _queryKeyToIdMap: Map; /** @internal */ _children: this[] | undefined; /** @internal */ protected _history: IHistory; /** @internal */ protected _matchPattern: string | RegExp; constructor( name: string, router: Router, source: RouteSource, parent: RouteMatchShared | undefined, history: IHistory, {match, query, group, metadata}: RouteMatchSharedOptions, ) { makeObservable(this); this.$name = name; this.$group = group as TSpecificGroupName; this.$parent = parent; this.$router = router; this._source = source; this._history = history; if (match instanceof RegExp && match.global) { throw new Error( 'Expecting a non-global regular expression as match pattern', ); } this._matchPattern = match; this._queryKeyToIdMap = new Map([ ...(parent?._queryKeyToIdMap ?? []), ...query, ]); // eslint-disable-next-line @mufan/no-object-literal-type-assertion this.$metadata = { ...parent?.$metadata, ...metadata, } as TMetadata; } /** * A dictionary of the combination of query string and segments. */ @computed get $params(): TParamDict { // eslint-disable-next-line @mufan/no-object-literal-type-assertion return { ...this._paramSegments, ...this._query, } as TParamDict; } /** * A reactive value indicates whether this route is exactly matched. */ get $exact(): boolean { const entry = this._matchEntry; return !!entry && entry.exact; } /** * A reactive value indicates whether this route is matched. */ get $matched(): boolean { return !!this._matchEntry; } /** * Get the deepest matching descendant. */ @computed get $rest(): this { if (!this.$matched) { return this; } const children = this._children; const matchingChild = children && children.find(match => match.$matched); return matchingChild ? matchingChild.$rest : this; } /** @internal */ @computed protected get _matchEntry(): RouteMatchEntry | undefined { return this._getMatchEntry(this._source); } /** @internal */ @computed protected get _segment(): string | undefined { const entry = this._matchEntry; return entry && entry.segment; } /** @internal */ @computed protected get _paramSegments(): GeneralSegmentDict { const parent = this.$parent; const upperSegmentDict = parent && parent._paramSegments; const matchPattern = this._matchPattern; const segment = this._segment; if (!(matchPattern instanceof RegExp)) { return { ...upperSegmentDict, }; } return { ...upperSegmentDict, [this.$name]: segment, }; } /** @internal */ @computed get _pathSegments(): GeneralSegmentDict { const parent = this.$parent; const upperSegmentDict = parent && parent._pathSegments; const name = this.$name; const matchPattern = this._matchPattern; const segment = this._segment; return { ...upperSegmentDict, ...(name ? {[name]: typeof matchPattern === 'string' ? matchPattern : segment} : undefined), }; } /** @internal */ @computed get _rest(): string { const entry = this._matchEntry; return entry ? entry.rest : ''; } /** @internal */ @computed protected get _query(): GeneralQueryDict | undefined { const sourceQueryMap = this._source.queryMap; return Array.from(this._queryKeyToIdMap).reduce((dict, [key, id]) => { const sourceQuery = sourceQueryMap.get(key); if (!sourceQuery || !isQueryIdsMatched(sourceQuery.id, id)) { return dict; } dict[key] = sourceQuery.value; return dict; // eslint-disable-next-line @mufan/no-object-literal-type-assertion }, {} as GeneralQueryDict); } $(params?: Partial & EmptyObjectPatch): RouteBuilder { return new RouteBuilder(this.$router, 'none', [ {route: this, params}, ]); } $ref(params?: Partial & EmptyObjectPatch): string { return this.$(params).$ref(); } $href(params?: Partial & EmptyObjectPatch): string { return this.$(params).$href(); } $push( params?: Partial & EmptyObjectPatch, {onComplete, ...options}: RouteMatchNavigateOptions = {}, ): void { this._build(params, options).$push({onComplete}); } $replace( params?: Partial & EmptyObjectPatch, {onComplete, ...options}: RouteMatchNavigateOptions = {}, ): void { this._build(params, options).$replace({onComplete}); } /** @internal */ abstract _getMatchEntry(source: RouteSource): RouteMatchEntry | undefined; /** @internal */ protected abstract _getBuilder(): RouteBuilder; /** @internal */ private _build( params: Partial & EmptyObjectPatch = {}, {leave = false, leaves = []}: RouteMatchBuildOptions = {}, ): RouteBuilder { if (typeof leaves === 'string') { leaves = [leaves]; } if (leave) { const group = this.$group; if (group === undefined) { throw new Error('Cannot leave primary route'); } leaves.push(group as string as TGroupName); } return this._getBuilder() .$(this, params as object) .$leave(leaves); } }