From 32e64f037d6d537829692e9af9f5abd2a294d6f7 Mon Sep 17 00:00:00 2001 From: Chris Thielen Date: Wed, 4 Jan 2017 00:31:26 -0600 Subject: [PATCH] feat(LocationServices): Add a `parts()` method which returns the URL parts as an object refactor(UrlSync): Consolidate UrlSync UrlListen UrlDeferIntercept feat(UrlServices): Add `match()`: given a URL, return the best matching Url Rule --- src/common/coreservices.ts | 10 +++++--- src/url/interface.ts | 30 ++++++++++++++--------- src/url/urlRouter.ts | 49 +++++++++++++++++++++++-------------- src/url/urlService.ts | 20 ++++++++++++--- src/vanilla/hashLocation.ts | 2 +- test/urlRouterSpec.ts | 30 ++++++++++++++++++++++- 6 files changed, 103 insertions(+), 38 deletions(-) diff --git a/src/common/coreservices.ts b/src/common/coreservices.ts index 83a963b8..76f9aec2 100644 --- a/src/common/coreservices.ts +++ b/src/common/coreservices.ts @@ -7,6 +7,7 @@ /** for typedoc */ import {IInjectable, Obj} from "./common"; import { Disposable } from "../interface"; +import { UrlParts } from "../url/interface"; export let notImplemented = (fnname: string) => () => { throw new Error(`${fnname}(): No coreservices implementation for UI-Router is loaded.`); @@ -77,29 +78,32 @@ export interface LocationServices extends Disposable { url(newurl: string, replace?: boolean, state?: any): string; /** - * Gets the path portion of the current url + * Gets the path part of the current url * * If the current URL is `/some/path?query=value#anchor`, this returns `/some/path` * * @return the path portion of the url */ path(): string; + /** - * Gets the search portion of the current url as an object + * Gets the search part of the current url as an object * * If the current URL is `/some/path?query=value#anchor`, this returns `{ query: 'value' }` * * @return the search (querystring) portion of the url, as an object */ search(): { [key: string]: any }; + /** - * Gets the hash portion of the current url + * Gets the hash part of the current url * * If the current URL is `/some/path?query=value#anchor`, this returns `anchor` * * @return the hash (anchor) portion of the url */ hash(): string; + /** * Registers a url change handler * diff --git a/src/url/interface.ts b/src/url/interface.ts index f3204437..039db138 100644 --- a/src/url/interface.ts +++ b/src/url/interface.ts @@ -91,7 +91,6 @@ export interface UrlMatcherConfig { */ defaultSquashPolicy(value?: (boolean|string)): (boolean|string); - /** * Creates and registers a custom [[ParamTypeDefinition]] object * @@ -120,7 +119,7 @@ export interface UrlMatcherConfig { } /** @internalapi */ -export interface UrlSync { +export interface UrlSyncApi { /** * Checks the URL for a matching [[UrlRule]] * @@ -142,10 +141,7 @@ export interface UrlSync { * ``` */ sync(evt?): void; -} -/** @internalapi */ -export interface UrlListen { /** * Starts or stops listening for URL changes * @@ -166,11 +162,8 @@ export interface UrlListen { * }); * ``` */ - listen(enabled?: boolean): Function; -} + listen(enabled?: boolean): Function -/** @internalapi */ -export interface UrlDeferIntercept { /** * Disables monitoring of the URL. * @@ -195,7 +188,7 @@ export interface UrlDeferIntercept { * @param defer Indicates whether to defer location change interception. * Passing no parameter is equivalent to `true`. */ - deferIntercept(defer?: boolean); + deferIntercept(defer?: boolean) } /** @@ -376,10 +369,23 @@ export interface UrlRules { */ export interface UrlParts { path: string; - search: { [key: string]: any }; - hash: string; + search?: { [key: string]: any }; + hash?: string; } +/** + * A UrlRule match result + * + * The result of UrlRouter.match() + */ +export interface MatchResult { + /** The matched value from a [[UrlRule]] */ + match: any; + /** The rule that matched */ + rule: UrlRule; + /** The match result weight */ + weight: number; +} /** * A function that matches the URL for a [[UrlRule]] * diff --git a/src/url/urlRouter.ts b/src/url/urlRouter.ts index e68de426..736b5e9e 100644 --- a/src/url/urlRouter.ts +++ b/src/url/urlRouter.ts @@ -1,8 +1,9 @@ /** * @internalapi * @module url - */ /** for typedoc */ -import { removeFrom, createProxyFunctions, inArray, composeSort, sortBy } from "../common/common"; + */ +/** for typedoc */ +import { removeFrom, createProxyFunctions, inArray, composeSort, sortBy, extend } from "../common/common"; import { isFunction, isString, isDefined } from "../common/predicates"; import { UrlMatcher } from "./urlMatcher"; import { RawParams } from "../params/interface"; @@ -11,7 +12,7 @@ import { UIRouter } from "../router"; import { val, is, pattern, prop, pipe } from "../common/hof"; import { UrlRuleFactory } from "./urlRule"; import { TargetState } from "../state/targetState"; -import { UrlRule, UrlRuleHandlerFn, UrlParts, UrlRules, UrlSync, UrlListen, UrlDeferIntercept } from "./interface"; +import { UrlRule, UrlRuleHandlerFn, UrlParts, UrlRules, UrlSyncApi, MatchResult } from "./interface"; import { TargetStateDef } from "../state/interface"; /** @hidden */ @@ -53,7 +54,7 @@ defaultRuleSortFn = composeSort( * This class updates the URL when the state changes. * It also responds to changes in the URL. */ -export class UrlRouter implements UrlRules, UrlSync, UrlListen, UrlDeferIntercept, Disposable { +export class UrlRouter implements UrlRules, UrlSyncApi, Disposable { /** used to create [[UrlRule]] objects for common cases */ public urlRuleFactory: UrlRuleFactory; @@ -85,25 +86,20 @@ export class UrlRouter implements UrlRules, UrlSync, UrlListen, UrlDeferIntercep this._rules.sort(this._sortFn = compareFn || this._sortFn); } - /** @inheritdoc */ - sync(evt?) { - if (evt && evt.defaultPrevented) return; - - let router = this._router, - $url = router.urlService, - $state = router.stateService; - + /** + * Given a URL, check all rules and return the best [[MatchResult]] + * @param url + * @returns {MatchResult} + */ + match(url: UrlParts): MatchResult { + url = extend({path: '', search: {}, hash: '' }, url); let rules = this.rules(); if (this._otherwiseFn) rules.push(this._otherwiseFn); - let url: UrlParts = { - path: $url.path(), search: $url.search(), hash: $url.hash() - }; - // Checks a single rule. Returns { rule: rule, match: match, weight: weight } if it matched, or undefined - interface MatchResult { match: any, rule: UrlRule, weight: number } + let checkRule = (rule: UrlRule): MatchResult => { - let match = rule.match(url, router); + let match = rule.match(url, this._router); return match && { match, rule, weight: rule.matchPriority(match) }; }; @@ -121,6 +117,23 @@ export class UrlRouter implements UrlRules, UrlSync, UrlListen, UrlDeferIntercep best = (!best || current && current.weight > best.weight) ? current : best; } + return best; + } + + /** @inheritdoc */ + sync(evt?) { + if (evt && evt.defaultPrevented) return; + + let router = this._router, + $url = router.urlService, + $state = router.stateService; + + let url: UrlParts = { + path: $url.path(), search: $url.search(), hash: $url.hash() + }; + + let best = this.match(url); + let applyResult = pattern([ [isString, (newurl: string) => $url.url(newurl)], [TargetState.isDef, (def: TargetStateDef) => $state.go(def.state, def.params, def.options)], diff --git a/src/url/urlService.ts b/src/url/urlService.ts index e2195dbf..54927cc9 100644 --- a/src/url/urlService.ts +++ b/src/url/urlService.ts @@ -6,7 +6,7 @@ import { UIRouter } from "../router"; import { LocationServices, notImplemented, LocationConfig } from "../common/coreservices"; import { noop, createProxyFunctions } from "../common/common"; -import { UrlConfig, UrlSync, UrlListen, UrlRules, UrlDeferIntercept } from "./interface"; +import { UrlConfig, UrlSyncApi, UrlRules, UrlParts, MatchResult } from "./interface"; /** @hidden */ const makeStub = (keys: string[]): any => @@ -16,12 +16,12 @@ const makeStub = (keys: string[]): any => /** @hidden */ const locationConfigFns = ["port", "protocol", "host", "baseHref", "html5Mode", "hashPrefix"]; /** @hidden */ const umfFns = ["type", "caseInsensitive", "strictMode", "defaultSquashPolicy"]; /** @hidden */ const rulesFns = ["sort", "when", "otherwise", "rules", "rule", "removeRule"]; -/** @hidden */ const syncFns = ["deferIntercept", "listen", "sync"]; +/** @hidden */ const syncFns = ["deferIntercept", "listen", "sync", "match"]; /** * API for URL management */ -export class UrlService implements LocationServices, UrlSync, UrlListen, UrlDeferIntercept { +export class UrlService implements LocationServices, UrlSyncApi { /** @hidden */ static locationServiceStub: LocationServices = makeStub(locationServicesFns); /** @hidden */ @@ -41,6 +41,18 @@ export class UrlService implements LocationServices, UrlSync, UrlListen, UrlDefe /** @inheritdoc */ onChange(callback: Function): Function { return }; + + /** + * Returns the current URL parts + * + * This method returns the current URL components as a [[UrlParts]] object. + * + * @returns the current url parts + */ + parts(): UrlParts { + return { path: this.path(), search: this.search(), hash: this.hash() } + } + dispose() { } /** @inheritdoc */ @@ -49,6 +61,8 @@ export class UrlService implements LocationServices, UrlSync, UrlListen, UrlDefe listen(enabled?: boolean): Function { return }; /** @inheritdoc */ deferIntercept(defer?: boolean) { return } + /** @inheritdoc */ + match(urlParts: UrlParts): MatchResult { return } /** * A nested API for managing URL rules and rewrites diff --git a/src/vanilla/hashLocation.ts b/src/vanilla/hashLocation.ts index e1939ece..675105f2 100644 --- a/src/vanilla/hashLocation.ts +++ b/src/vanilla/hashLocation.ts @@ -22,7 +22,7 @@ export class HashLocationService implements LocationServices, Disposable { url(url?: string, replace: boolean = true): string { if (isDefined(url)) location.hash = url; return buildUrl(this); - }; + } onChange(cb: EventListener) { window.addEventListener('hashchange', cb, false); diff --git a/test/urlRouterSpec.ts b/test/urlRouterSpec.ts index 93f69cdd..269520de 100644 --- a/test/urlRouterSpec.ts +++ b/test/urlRouterSpec.ts @@ -4,7 +4,7 @@ import { LocationServices } from "../src/common/coreservices"; import { UrlService } from "../src/url/urlService"; import { StateRegistry } from "../src/state/stateRegistry"; import { noop } from "../src/common/common"; -import { UrlRule } from "../src/url/interface"; +import { UrlRule, MatchResult } from "../src/url/interface"; declare var jasmine; var _anything = jasmine.anything(); @@ -272,6 +272,34 @@ describe("UrlRouter", function () { }) }); }); + + describe('match', () => { + let A, B, CCC; + beforeEach(() => { + A = stateRegistry.register({ name: 'A', url: '/:pA' }); + B = stateRegistry.register({ name: 'B', url: '/BBB' }); + CCC = urlService.rules.when('/CCC', '/DDD'); + }); + + it("should return the best match for a URL 1", () => { + let match: MatchResult = urlRouter.match({ path: '/BBB' }); + expect(match.rule.type).toBe("STATE"); + expect(match.rule['state']).toBe(B) + }); + + it("should return the best match for a URL 2", () => { + let match: MatchResult = urlRouter.match({ path: '/EEE' }); + expect(match.rule.type).toBe("STATE"); + expect(match.rule['state']).toBe(A); + expect(match.match).toEqual({ pA: 'EEE' }); + }); + + it("should return the best match for a URL 3", () => { + let match: MatchResult = urlRouter.match({ path: '/CCC' }); + expect(match.rule.type).toBe("URLMATCHER"); + expect(match.rule).toBe(CCC); + }); + }); }); describe('UrlRouter.deferIntercept', () => {