Skip to content

Commit

Permalink
feat(State): Switch Internal State Object to prototypally inherit fro…
Browse files Browse the repository at this point in the history
…m the State Declaration

In 0.x, we used prototypal inheritance, i.e. `internalImpl = inherit(stateDeclaration)`.
In 1.0 alphas, we switched to `extend(new State(), stateDeclaration)` to enable us to use `instanceof State`.
However, this convenience for ui-router code was inconvenient for end users so we're switching back to using the state declaration as the prototype of the internal state object.

- This enables users to create classes for their states, providing (e.g.) standardized onEnter functions, such as:
```js
var myState = new LoggingState('mystate', '/url');
```

Closes angular-ui/ui-router#3293
Closes #34
  • Loading branch information
christopherthielen committed Jan 29, 2017
1 parent ef6c87b commit 027c995
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 87 deletions.
9 changes: 6 additions & 3 deletions src/common/predicates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
* Although these functions are exported, they are subject to change without notice.
*
* @module common_predicates
*/ /** */
import { and, not, pipe, prop, compose, or } from "./hof";
import {Predicate} from "./common"; // has or is using
*/
/** */
import { and, not, pipe, prop, or } from "./hof";
import { Predicate } from "./common"; // has or is using
import { State } from "../state/stateObject";

const toStr = Object.prototype.toString;
const tis = (t: string) => (x: any) => typeof(x) === t;
Expand All @@ -21,6 +23,7 @@ export const isObject = (x: any) => x !== null && typeof x === 'object';
export const isArray = Array.isArray;
export const isDate: (x: any) => x is Date = <any> ((x: any) => toStr.call(x) === '[object Date]');
export const isRegExp: (x: any) => x is RegExp = <any> ((x: any) => toStr.call(x) === '[object RegExp]');
export const isState: (x: any) => x is State = State.isState;

/**
* Predicate which checks if a value is injectable
Expand Down
4 changes: 2 additions & 2 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ export class UIRouter {

/**
* Deprecated for public use. Use [[urlService]] instead.
* @deprecated
* @deprecated Use [[urlService]] instead
*/
urlMatcherFactory: UrlMatcherFactory = new UrlMatcherFactory();

/**
* Deprecated for public use. Use [[urlService]] instead.
* @deprecated
* @deprecated Use [[urlService]] instead
*/
urlRouter: UrlRouter = new UrlRouter(this);

Expand Down
46 changes: 33 additions & 13 deletions src/state/stateObject.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
/**
* @coreapi
* @module state
*/ /** for typedoc */

import {StateDeclaration, _ViewDeclaration} from "./interface";
import {extend, defaults, values, find} from "../common/common";
import {propEq} from "../common/hof";
import {Param} from "../params/param";
import {UrlMatcher} from "../url/urlMatcher";
import {Resolvable} from "../resolve/resolvable";
import {TransitionStateHookFn} from "../transition/interface";
import {TargetState} from "./targetState";
import {Transition} from "../transition/transition";
*/
/** for typedoc */
import { StateDeclaration, _ViewDeclaration } from "./interface";
import { defaults, values, find, inherit } from "../common/common";
import { propEq } from "../common/hof";
import { Param } from "../params/param";
import { UrlMatcher } from "../url/urlMatcher";
import { Resolvable } from "../resolve/resolvable";
import { TransitionStateHookFn } from "../transition/interface";
import { TargetState } from "./targetState";
import { Transition } from "../transition/transition";

/**
* Internal representation of a UI-Router state.
Expand Down Expand Up @@ -96,11 +96,31 @@ export class State {
);


/** @deprecated use State.create() */
constructor(config?: StateDeclaration) {
extend(this, config);
// Object.freeze(this);
return State.create(config || {});
}

/**
* Create a state object to put the private/internal implementation details onto.
* The object's prototype chain looks like:
* (Internal State Object) -> (Copy of State.prototype) -> (State Declaration object) -> (State Declaration's prototype...)
*
* @param stateDecl the user-supplied State Declaration
* @returns {State} an internal State object
*/
static create(stateDecl: StateDeclaration): State {
let state = inherit(inherit(stateDecl, State.prototype)) as State;
stateDecl.$$state = () => state;
state['__stateObject'] = true;
state.self = stateDecl;
return state;
}

/** Predicate which returns true if the object is an internal State object */
static isState = (obj: any): obj is State =>
obj['__stateObject'] === true;

/**
* Returns true if the provided parameter is the same state.
*
Expand Down
26 changes: 11 additions & 15 deletions src/state/stateQueueManager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/** @module state */ /** for typedoc */
import { extend, inherit, pluck } from "../common/common";
import { isString } from "../common/predicates";
import { extend, inherit, pluck, inArray } from "../common/common";
import { isString, isDefined } from "../common/predicates";
import { StateDeclaration } from "./interface";
import { State } from "./stateObject";
import { StateBuilder } from "./stateBuilder";
import { StateRegistryListener, StateRegistry } from "./stateRegistry";
import { Disposable } from "../interface";
import { UrlRouter } from "../url/urlRouter";
import { prop } from "../common/hof";

/** @internalapi */
export class StateQueueManager implements Disposable {
Expand All @@ -26,19 +27,14 @@ export class StateQueueManager implements Disposable {
this.queue = [];
}

register(config: StateDeclaration) {
let {states, queue} = this;
// Wrap a new object around the state so we can store our private details easily.
// @TODO: state = new State(extend({}, config, { ... }))
let state = inherit(new State(), extend({}, config, {
self: config,
resolve: config.resolve || [],
toString: () => config.name
}));

if (!isString(state.name)) throw new Error("State must have a valid name");
if (states.hasOwnProperty(state.name) || pluck(queue, 'name').indexOf(state.name) !== -1)
throw new Error(`State '${state.name}' is already defined`);
register(stateDecl: StateDeclaration) {
let queue = this.queue;
let state = State.create(stateDecl);
let name = state.name;

if (!isString(name)) throw new Error("State must have a valid name");
if (this.states.hasOwnProperty(name) || inArray(queue.map(prop('name')), name))
throw new Error(`State '${name}' is already defined`);

queue.push(state);
this.flush();
Expand Down
4 changes: 2 additions & 2 deletions src/url/urlRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @module url
*/ /** */
import { UrlMatcher } from "./urlMatcher";
import { isString, isDefined, isFunction } from "../common/predicates";
import { isString, isDefined, isFunction, isState } from "../common/predicates";
import { UIRouter } from "../router";
import { identity, extend } from "../common/common";
import { is, pattern } from "../common/hof";
Expand Down Expand Up @@ -38,7 +38,7 @@ export class UrlRuleFactory {
const makeRule = pattern([
[isString, (_what: string) => makeRule(this.compile(_what))],
[is(UrlMatcher), (_what: UrlMatcher) => this.fromUrlMatcher(_what, handler)],
[is(State), (_what: State) => this.fromState(_what, this.router)],
[isState, (_what: State) => this.fromState(_what, this.router)],
[is(RegExp), (_what: RegExp) => this.fromRegExp(_what, handler)],
[isFunction, (_what: UrlRuleMatchFn) => new BaseUrlRule(_what, handler as UrlRuleHandlerFn)],
]);
Expand Down
Loading

0 comments on commit 027c995

Please sign in to comment.