Skip to content

Commit 6b93142

Browse files
feat(TargetState): Add builder methods .withState, .withParams, and .withOptions
Remove ParamsOrArray type which was not really used.
1 parent 3904487 commit 6b93142

File tree

8 files changed

+126
-50
lines changed

8 files changed

+126
-50
lines changed

src/params/interface.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ export interface RawParams {
2424
[key: string]: any;
2525
}
2626

27-
/** @internalapi */
28-
export type ParamsOrArray = (RawParams|RawParams[]);
29-
3027
/**
3128
* Configuration for a single Parameter
3229
*
@@ -465,15 +462,15 @@ export interface Replace {
465462
* // Changes URL to '/list/3', logs "Ringo" to the console
466463
* $state.go('list', { item: "Ringo" });
467464
* ```
468-
*
465+
*
469466
* See: [[UrlConfigApi.type]]
470467
* @coreapi
471468
*/
472469
export interface ParamTypeDefinition {
473470
/**
474471
* Tests if some object type is compatible with this parameter type
475-
*
476-
* Detects whether some value is of this particular type.
472+
*
473+
* Detects whether some value is of this particular type.
477474
* Accepts a decoded value and determines whether it matches this `ParamType` object.
478475
*
479476
* If your custom type encodes the parameter to a specific type, check for that type here.

src/path/pathFactory.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {TargetState} from "../state/targetState";
1515
import {GetParamsFn, PathNode} from "./pathNode";
1616
import {ViewService} from "../view/view";
1717
import { Param } from '../params/param';
18+
import { StateRegistry } from '../state';
1819

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

2627
/** Given a PathNode[], create an TargetState */
27-
static makeTargetState(path: PathNode[]): TargetState {
28+
static makeTargetState(registry: StateRegistry, path: PathNode[]): TargetState {
2829
let state = tail(path).state;
29-
return new TargetState(state, state, path.map(prop("paramValues")).reduce(mergeR, {}));
30+
return new TargetState(registry, state, path.map(prop("paramValues")).reduce(mergeR, {}), {});
3031
}
3132

3233
static buildPath(targetState: TargetState) {

src/state/interface.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* @coreapi
33
* @module state
44
*/ /** for typedoc */
5-
import { ParamDeclaration, RawParams, ParamsOrArray } from "../params/interface";
5+
import { ParamDeclaration, RawParams } from "../params/interface";
66
import { StateObject } from "./stateObject";
77
import { ViewContext } from "../view/interface";
88
import { IInjectable } from "../common/common";
@@ -21,7 +21,7 @@ export interface TransitionPromise extends Promise<StateObject> {
2121

2222
export interface TargetStateDef {
2323
state: StateOrName;
24-
params?: ParamsOrArray;
24+
params?: RawParams;
2525
options?: TransitionOptions;
2626
}
2727

src/state/stateRegistry.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,12 @@ export class StateRegistry {
188188
* Note: this does not return states that are *queued* but not yet registered.
189189
*
190190
* @param stateOrName either the name of a state, or a state object.
191+
* @param base the base state to use when stateOrName is relative.
191192
* @return a registered [[StateDeclaration]] that matched the `stateOrName`, or null if the state isn't registered.
192193
*/
193194
get(stateOrName: StateOrName, base?: StateOrName): StateDeclaration;
194195
get(stateOrName?: StateOrName, base?: StateOrName): any {
195-
if (arguments.length === 0)
196+
if (arguments.length === 0)
196197
return <StateDeclaration[]> Object.keys(this.states).map(name => this.states[name].self);
197198
let found = this.matcher.find(stateOrName, base);
198199
return found && found.self || null;
@@ -201,4 +202,4 @@ export class StateRegistry {
201202
decorator(name: string, func: BuilderFunction) {
202203
return this.builder.builder(name, func);
203204
}
204-
}
205+
}

src/state/stateService.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { HrefOptions, LazyLoadResult, StateDeclaration, StateOrName, TransitionP
2020
import { StateObject } from './stateObject';
2121
import { TargetState } from './targetState';
2222

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

281-
let stateDefinition = reg.matcher.find(identifier, options.relative);
282-
return new TargetState(identifier, stateDefinition, params, options);
281+
return new TargetState(this.router.stateRegistry, identifier, params, options);
283282
};
284283

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

597596
let currentPath = this.getCurrentPath();
598-
let target = PathUtils.makeTargetState(currentPath);
597+
let target = PathUtils.makeTargetState(this.router.stateRegistry, currentPath);
599598
transition = transition || this.router.transitionService.create(currentPath, target);
600599

601600
return lazyLoadState(transition, state);

src/state/targetState.ts

+48-20
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
*/ /** for typedoc */
55

66
import { StateDeclaration, StateOrName, TargetStateDef } from "./interface";
7-
import { ParamsOrArray } from "../params/interface";
87
import { TransitionOptions } from "../transition/interface";
98
import { StateObject } from "./stateObject";
109
import { isString } from "../common/predicates";
1110
import { stringify } from '../common/strings';
11+
import { extend } from '../common';
12+
import { StateRegistry } from './stateRegistry';
13+
import { RawParams } from '../params';
1214

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

4549
/**
4650
* The TargetState constructor
4751
*
4852
* Note: Do not construct a `TargetState` manually.
4953
* To create a `TargetState`, use the [[StateService.target]] factory method.
5054
*
55+
* @param _stateRegistry The StateRegistry to use to look up the _definition
5156
* @param _identifier An identifier for a state.
5257
* Either a fully-qualified state name, or the object used to define the state.
53-
* @param _definition The internal state representation, if exists.
5458
* @param _params Parameters for the target state
5559
* @param _options Transition options.
5660
*
5761
* @internalapi
5862
*/
5963
constructor(
64+
private _stateRegistry: StateRegistry,
6065
private _identifier: StateOrName,
61-
private _definition?: StateObject,
62-
_params?: ParamsOrArray,
63-
private _options: TransitionOptions = {}
66+
_params?: RawParams,
67+
_options?: TransitionOptions,
6468
) {
65-
this._params = _params || {};
69+
this._identifier = _identifier;
70+
this._params = extend({}, _params || {});
71+
this._options = extend({}, _options || {});
72+
this._definition = _stateRegistry.matcher.find(_identifier, this._options.relative);
6673
}
6774

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

7885
/** The target parameter values */
79-
params(): ParamsOrArray {
86+
params(): RawParams {
8087
return this._params;
8188
}
8289

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

129-
// /** Returns a new TargetState based on this one, but using the specified options */
130-
// withOptions(_options: TransitionOptions): TargetState {
131-
// return extend(this._clone(), { _options });
132-
// }
133-
//
134-
// /** Returns a new TargetState based on this one, but using the specified params */
135-
// withParams(_params: ParamsOrArray): TargetState {
136-
// return extend(this._clone(), { _params });
137-
// }
138-
139-
// private _clone = () =>
140-
// new TargetState(this._identifier, this._definition, this._params, this._options);
136+
/**
137+
* Returns a copy of this TargetState which targets a different state.
138+
* The new TargetState has the same parameter values and transition options.
139+
*
140+
* @param state The new state that should be targeted
141+
*/
142+
withState(state: StateOrName): TargetState {
143+
return new TargetState(this._stateRegistry, state, this._params, this._options);
144+
}
145+
146+
/**
147+
* Returns a copy of this TargetState, using the specified parameter values.
148+
*
149+
* @param params the new parameter values to use
150+
* @param replace When false (default) the new parameter values will be merged with the current values.
151+
* When true the parameter values will be used instead of the current values.
152+
*/
153+
withParams(params: RawParams, replace = false): TargetState {
154+
const newParams: RawParams = replace ? params : extend({}, this._params, params);
155+
return new TargetState(this._stateRegistry, this._identifier, newParams, this._options);
156+
}
157+
158+
/**
159+
* Returns a copy of this TargetState, using the specified Transition Options.
160+
*
161+
* @param options the new options to use
162+
* @param replace When false (default) the new options will be merged with the current options.
163+
* When true the options will be used instead of the current options.
164+
*/
165+
withOptions(options: TransitionOptions, replace = false): TargetState {
166+
const newOpts = replace ? options : extend({}, this._options, options);
167+
return new TargetState(this._stateRegistry, this._identifier, this._params, newOpts);
168+
}
141169
}

src/transition/transition.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,7 @@ export class Transition implements IHookRegistry {
528528
}
529529

530530
let newOptions = extend({}, this.options(), targetState.options(), redirectOpts);
531-
532-
targetState = new TargetState(targetState.identifier(), targetState.$state(), targetState.params(), newOptions);
531+
targetState = targetState.withOptions(newOptions, true);
533532

534533
let newTransition = this.router.transitionService.create(this._treeChanges.from, targetState);
535534
let originalEnteringNodes = this._treeChanges.entering;

test/targetStateSpec.ts

+61-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,75 @@
1-
import { TargetState } from "../src/index";
1+
import { TargetState, UIRouter } from '../src/index';
2+
import { StateObject, StateRegistry } from '../src/state';
23

34
describe('TargetState object', function() {
5+
let registry: StateRegistry;
6+
beforeEach(() => {
7+
registry = new UIRouter().stateRegistry;
8+
registry.register({ name: 'foo' });
9+
registry.register({ name: 'foo.bar' });
10+
registry.register({ name: 'baz' });
11+
});
12+
413
it('should be callable and return the correct values', function() {
5-
let state: any = { name: "foo.bar" }, ref = new TargetState(state.name, state, {});
6-
expect(ref.identifier()).toBe("foo.bar");
14+
let state: StateObject = registry.get('foo.bar').$$state();
15+
let ref = new TargetState(registry, state.name, null);
16+
expect(ref.identifier()).toBe('foo.bar');
717
expect(ref.$state()).toBe(state);
818
expect(ref.params()).toEqual({});
919
});
1020

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

16-
ref = new TargetState("foo");
26+
ref = new TargetState(registry, 'notfound', null);
1727
expect(ref.valid()).toBe(false);
18-
expect(ref.error()).toBe("No such state 'foo'");
28+
expect(ref.error()).toBe("No such state 'notfound'");
29+
});
1930

20-
ref = new TargetState("foo", <any> { name: "foo" });
21-
expect(ref.valid()).toBe(false);
22-
expect(ref.error()).toBe("State 'foo' has an invalid definition");
31+
describe('.withState', function() {
32+
it('should replace the target state', () => {
33+
let ref = new TargetState(registry, 'foo');
34+
let newRef = ref.withState('baz');
35+
expect(newRef.identifier()).toBe('baz');
36+
expect(newRef.$state()).toBe(registry.get('baz').$$state());
37+
});
38+
39+
it('should find a relative target state using the existing options.relative', () => {
40+
let ref = new TargetState(registry, 'baz', null, { relative: 'foo' });
41+
let newRef = ref.withState('.bar');
42+
expect(newRef.identifier()).toBe('.bar');
43+
expect(newRef.state()).toBe(registry.get('foo.bar'));
44+
expect(newRef.$state()).toBe(registry.get('foo.bar').$$state());
45+
});
46+
});
47+
48+
describe('.withOptions', function() {
49+
it('should merge options with current options when replace is false or unspecified', () => {
50+
let ref = new TargetState(registry, 'foo', {}, { location: false });
51+
let newRef = ref.withOptions({ inherit: false });
52+
expect(newRef.options()).toEqual({ location: false, inherit: false });
53+
});
54+
55+
it('should replace all options when replace is true', () => {
56+
let ref = new TargetState(registry, 'foo', {}, { location: false });
57+
let newRef = ref.withOptions({ inherit: false }, true);
58+
expect(newRef.options()).toEqual({ inherit: false });
59+
});
60+
});
61+
62+
describe('.withParams', function() {
63+
it('should merge params with current params when replace is false or unspecified', () => {
64+
let ref = new TargetState(registry, 'foo', { param1: 1 }, { });
65+
let newRef = ref.withParams({ param2: 2 });
66+
expect(newRef.params()).toEqual({ param1: 1, param2: 2 });
67+
});
68+
69+
it('should replace all params when replace is true', () => {
70+
let ref = new TargetState(registry, 'foo', { param1: 1 }, { });
71+
let newRef = ref.withParams({ param2: 2 }, true);
72+
expect(newRef.params()).toEqual({ param2: 2 });
73+
});
2374
});
2475
});

0 commit comments

Comments
 (0)