Skip to content

Commit

Permalink
feat(TargetState): Add builder methods .withState, .withParams, and .…
Browse files Browse the repository at this point in the history
…withOptions

Remove ParamsOrArray type which was not really used.
  • Loading branch information
christopherthielen committed Sep 30, 2017
1 parent 3904487 commit 6b93142
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 50 deletions.
9 changes: 3 additions & 6 deletions src/params/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ export interface RawParams {
[key: string]: any;
}

/** @internalapi */
export type ParamsOrArray = (RawParams|RawParams[]);

/**
* Configuration for a single Parameter
*
Expand Down Expand Up @@ -465,15 +462,15 @@ export interface Replace {
* // Changes URL to '/list/3', logs "Ringo" to the console
* $state.go('list', { item: "Ringo" });
* ```
*
*
* See: [[UrlConfigApi.type]]
* @coreapi
*/
export interface ParamTypeDefinition {
/**
* Tests if some object type is compatible with this parameter type
*
* Detects whether some value is of this particular type.
*
* Detects whether some value is of this particular type.
* Accepts a decoded value and determines whether it matches this `ParamType` object.
*
* If your custom type encodes the parameter to a specific type, check for that type here.
Expand Down
5 changes: 3 additions & 2 deletions src/path/pathFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {TargetState} from "../state/targetState";
import {GetParamsFn, PathNode} from "./pathNode";
import {ViewService} from "../view/view";
import { Param } from '../params/param';
import { StateRegistry } from '../state';

/**
* This class contains functions which convert TargetStates, Nodes and paths from one type to another.
Expand All @@ -24,9 +25,9 @@ export class PathUtils {
constructor() { }

/** Given a PathNode[], create an TargetState */
static makeTargetState(path: PathNode[]): TargetState {
static makeTargetState(registry: StateRegistry, path: PathNode[]): TargetState {
let state = tail(path).state;
return new TargetState(state, state, path.map(prop("paramValues")).reduce(mergeR, {}));
return new TargetState(registry, state, path.map(prop("paramValues")).reduce(mergeR, {}), {});
}

static buildPath(targetState: TargetState) {
Expand Down
4 changes: 2 additions & 2 deletions src/state/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @coreapi
* @module state
*/ /** for typedoc */
import { ParamDeclaration, RawParams, ParamsOrArray } from "../params/interface";
import { ParamDeclaration, RawParams } from "../params/interface";
import { StateObject } from "./stateObject";
import { ViewContext } from "../view/interface";
import { IInjectable } from "../common/common";
Expand All @@ -21,7 +21,7 @@ export interface TransitionPromise extends Promise<StateObject> {

export interface TargetStateDef {
state: StateOrName;
params?: ParamsOrArray;
params?: RawParams;
options?: TransitionOptions;
}

Expand Down
5 changes: 3 additions & 2 deletions src/state/stateRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,12 @@ export class StateRegistry {
* Note: this does not return states that are *queued* but not yet registered.
*
* @param stateOrName either the name of a state, or a state object.
* @param base the base state to use when stateOrName is relative.
* @return a registered [[StateDeclaration]] that matched the `stateOrName`, or null if the state isn't registered.
*/
get(stateOrName: StateOrName, base?: StateOrName): StateDeclaration;
get(stateOrName?: StateOrName, base?: StateOrName): any {
if (arguments.length === 0)
if (arguments.length === 0)
return <StateDeclaration[]> Object.keys(this.states).map(name => this.states[name].self);
let found = this.matcher.find(stateOrName, base);
return found && found.self || null;
Expand All @@ -201,4 +202,4 @@ export class StateRegistry {
decorator(name: string, func: BuilderFunction) {
return this.builder.builder(name, func);
}
}
}
11 changes: 5 additions & 6 deletions src/state/stateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { HrefOptions, LazyLoadResult, StateDeclaration, StateOrName, TransitionP
import { StateObject } from './stateObject';
import { TargetState } from './targetState';

import { ParamsOrArray, RawParams } from '../params/interface';
import { RawParams } from '../params/interface';
import { Param } from '../params/param';
import { Glob } from '../common/glob';
import { UIRouter } from '../router';
Expand Down Expand Up @@ -93,7 +93,7 @@ export class StateService {
* @internalapi
*/
private _handleInvalidTargetState(fromPath: PathNode[], toState: TargetState) {
let fromState = PathUtils.makeTargetState(fromPath);
let fromState = PathUtils.makeTargetState(this.router.stateRegistry, fromPath);
let globals = this.router.globals;
const latestThing = () => globals.transitionHistory.peekTail();
let latest = latestThing();
Expand Down Expand Up @@ -268,7 +268,7 @@ export class StateService {
*
* This may be returned from a Transition Hook to redirect a transition, for example.
*/
target(identifier: StateOrName, params?: ParamsOrArray, options: TransitionOptions = {}): TargetState {
target(identifier: StateOrName, params?: RawParams, options: TransitionOptions = {}): TargetState {
// If we're reloading, find the state object to reload from
if (isObject(options.reload) && !(<any>options.reload).name)
throw new Error('Invalid reload state object');
Expand All @@ -278,8 +278,7 @@ export class StateService {
if (options.reload && !options.reloadState)
throw new Error(`No such reload state '${(isString(options.reload) ? options.reload : (<any>options.reload).name)}'`);

let stateDefinition = reg.matcher.find(identifier, options.relative);
return new TargetState(identifier, stateDefinition, params, options);
return new TargetState(this.router.stateRegistry, identifier, params, options);
};

private getCurrentPath(): PathNode[] {
Expand Down Expand Up @@ -595,7 +594,7 @@ export class StateService {
if (!state || !state.lazyLoad) throw new Error("Can not lazy load " + stateOrName);

let currentPath = this.getCurrentPath();
let target = PathUtils.makeTargetState(currentPath);
let target = PathUtils.makeTargetState(this.router.stateRegistry, currentPath);
transition = transition || this.router.transitionService.create(currentPath, target);

return lazyLoadState(transition, state);
Expand Down
68 changes: 48 additions & 20 deletions src/state/targetState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
*/ /** for typedoc */

import { StateDeclaration, StateOrName, TargetStateDef } from "./interface";
import { ParamsOrArray } from "../params/interface";
import { TransitionOptions } from "../transition/interface";
import { StateObject } from "./stateObject";
import { isString } from "../common/predicates";
import { stringify } from '../common/strings';
import { extend } from '../common';
import { StateRegistry } from './stateRegistry';
import { RawParams } from '../params';

/**
* Encapsulate the target (destination) state/params/options of a [[Transition]].
Expand Down Expand Up @@ -40,29 +42,34 @@ import { stringify } from '../common/strings';
* or invalid (the state being targeted is not registered).
*/
export class TargetState {
private _params: ParamsOrArray;
private _definition: StateObject;
private _params: RawParams;
private _options: TransitionOptions;

/**
* The TargetState constructor
*
* Note: Do not construct a `TargetState` manually.
* To create a `TargetState`, use the [[StateService.target]] factory method.
*
* @param _stateRegistry The StateRegistry to use to look up the _definition
* @param _identifier An identifier for a state.
* Either a fully-qualified state name, or the object used to define the state.
* @param _definition The internal state representation, if exists.
* @param _params Parameters for the target state
* @param _options Transition options.
*
* @internalapi
*/
constructor(
private _stateRegistry: StateRegistry,
private _identifier: StateOrName,
private _definition?: StateObject,
_params?: ParamsOrArray,
private _options: TransitionOptions = {}
_params?: RawParams,
_options?: TransitionOptions,
) {
this._params = _params || {};
this._identifier = _identifier;
this._params = extend({}, _params || {});
this._options = extend({}, _options || {});
this._definition = _stateRegistry.matcher.find(_identifier, this._options.relative);
}

/** The name of the state this object targets */
Expand All @@ -76,7 +83,7 @@ export class TargetState {
}

/** The target parameter values */
params(): ParamsOrArray {
params(): RawParams {
return this._params;
}

Expand Down Expand Up @@ -126,16 +133,37 @@ export class TargetState {
static isDef = (obj): obj is TargetStateDef =>
obj && obj.state && (isString(obj.state) || isString(obj.state.name));

// /** Returns a new TargetState based on this one, but using the specified options */
// withOptions(_options: TransitionOptions): TargetState {
// return extend(this._clone(), { _options });
// }
//
// /** Returns a new TargetState based on this one, but using the specified params */
// withParams(_params: ParamsOrArray): TargetState {
// return extend(this._clone(), { _params });
// }

// private _clone = () =>
// new TargetState(this._identifier, this._definition, this._params, this._options);
/**
* Returns a copy of this TargetState which targets a different state.
* The new TargetState has the same parameter values and transition options.
*
* @param state The new state that should be targeted
*/
withState(state: StateOrName): TargetState {
return new TargetState(this._stateRegistry, state, this._params, this._options);
}

/**
* Returns a copy of this TargetState, using the specified parameter values.
*
* @param params the new parameter values to use
* @param replace When false (default) the new parameter values will be merged with the current values.
* When true the parameter values will be used instead of the current values.
*/
withParams(params: RawParams, replace = false): TargetState {
const newParams: RawParams = replace ? params : extend({}, this._params, params);
return new TargetState(this._stateRegistry, this._identifier, newParams, this._options);
}

/**
* Returns a copy of this TargetState, using the specified Transition Options.
*
* @param options the new options to use
* @param replace When false (default) the new options will be merged with the current options.
* When true the options will be used instead of the current options.
*/
withOptions(options: TransitionOptions, replace = false): TargetState {
const newOpts = replace ? options : extend({}, this._options, options);
return new TargetState(this._stateRegistry, this._identifier, this._params, newOpts);
}
}
3 changes: 1 addition & 2 deletions src/transition/transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,8 +528,7 @@ export class Transition implements IHookRegistry {
}

let newOptions = extend({}, this.options(), targetState.options(), redirectOpts);

targetState = new TargetState(targetState.identifier(), targetState.$state(), targetState.params(), newOptions);
targetState = targetState.withOptions(newOptions, true);

let newTransition = this.router.transitionService.create(this._treeChanges.from, targetState);
let originalEnteringNodes = this._treeChanges.entering;
Expand Down
71 changes: 61 additions & 10 deletions test/targetStateSpec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,75 @@
import { TargetState } from "../src/index";
import { TargetState, UIRouter } from '../src/index';
import { StateObject, StateRegistry } from '../src/state';

describe('TargetState object', function() {
let registry: StateRegistry;
beforeEach(() => {
registry = new UIRouter().stateRegistry;
registry.register({ name: 'foo' });
registry.register({ name: 'foo.bar' });
registry.register({ name: 'baz' });
});

it('should be callable and return the correct values', function() {
let state: any = { name: "foo.bar" }, ref = new TargetState(state.name, state, {});
expect(ref.identifier()).toBe("foo.bar");
let state: StateObject = registry.get('foo.bar').$$state();
let ref = new TargetState(registry, state.name, null);
expect(ref.identifier()).toBe('foo.bar');
expect(ref.$state()).toBe(state);
expect(ref.params()).toEqual({});
});

it('should validate state definition', function() {
let ref = new TargetState("foo", null, {}, { relative: {} });
let ref = new TargetState(registry, 'notfound', {}, { relative: {} });
expect(ref.valid()).toBe(false);
expect(ref.error()).toBe("Could not resolve 'foo' from state '[object Object]'");
expect(ref.error()).toBe("Could not resolve 'notfound' from state '[object Object]'");

ref = new TargetState("foo");
ref = new TargetState(registry, 'notfound', null);
expect(ref.valid()).toBe(false);
expect(ref.error()).toBe("No such state 'foo'");
expect(ref.error()).toBe("No such state 'notfound'");
});

ref = new TargetState("foo", <any> { name: "foo" });
expect(ref.valid()).toBe(false);
expect(ref.error()).toBe("State 'foo' has an invalid definition");
describe('.withState', function() {
it('should replace the target state', () => {
let ref = new TargetState(registry, 'foo');
let newRef = ref.withState('baz');
expect(newRef.identifier()).toBe('baz');
expect(newRef.$state()).toBe(registry.get('baz').$$state());
});

it('should find a relative target state using the existing options.relative', () => {
let ref = new TargetState(registry, 'baz', null, { relative: 'foo' });
let newRef = ref.withState('.bar');
expect(newRef.identifier()).toBe('.bar');
expect(newRef.state()).toBe(registry.get('foo.bar'));
expect(newRef.$state()).toBe(registry.get('foo.bar').$$state());
});
});

describe('.withOptions', function() {
it('should merge options with current options when replace is false or unspecified', () => {
let ref = new TargetState(registry, 'foo', {}, { location: false });
let newRef = ref.withOptions({ inherit: false });
expect(newRef.options()).toEqual({ location: false, inherit: false });
});

it('should replace all options when replace is true', () => {
let ref = new TargetState(registry, 'foo', {}, { location: false });
let newRef = ref.withOptions({ inherit: false }, true);
expect(newRef.options()).toEqual({ inherit: false });
});
});

describe('.withParams', function() {
it('should merge params with current params when replace is false or unspecified', () => {
let ref = new TargetState(registry, 'foo', { param1: 1 }, { });
let newRef = ref.withParams({ param2: 2 });
expect(newRef.params()).toEqual({ param1: 1, param2: 2 });
});

it('should replace all params when replace is true', () => {
let ref = new TargetState(registry, 'foo', { param1: 1 }, { });
let newRef = ref.withParams({ param2: 2 }, true);
expect(newRef.params()).toEqual({ param2: 2 });
});
});
});

0 comments on commit 6b93142

Please sign in to comment.