-
Notifications
You must be signed in to change notification settings - Fork 159
/
transition.ts
408 lines (355 loc) · 12.5 KB
/
transition.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
import { Promise } from 'rsvp';
import { Dict, Maybe, Option } from './core';
import InternalRouteInfo, { Route, RouteInfo } from './route-info';
import Router from './router';
import TransitionAborted, { ITransitionAbortedError } from './transition-aborted-error';
import { OpaqueIntent } from './transition-intent';
import TransitionState, { TransitionError } from './transition-state';
import { log, promiseLabel } from './utils';
export type OnFulfilled<T, TResult1> =
| ((value: T) => TResult1 | PromiseLike<TResult1>)
| undefined
| null;
export type OnRejected<T, TResult2> =
| ((reason: T) => TResult2 | PromiseLike<TResult2>)
| undefined
| null;
export type PublicTransition = Transition<any>;
export type OpaqueTransition = PublicTransition;
export const STATE_SYMBOL = `__STATE__-2619860001345920-3322w3`;
export const PARAMS_SYMBOL = `__PARAMS__-261986232992830203-23323`;
export const QUERY_PARAMS_SYMBOL = `__QPS__-2619863929824844-32323`;
/**
A Transition is a thennable (a promise-like object) that represents
an attempt to transition to another route. It can be aborted, either
explicitly via `abort` or by attempting another transition while a
previous one is still underway. An aborted transition can also
be `retry()`d later.
@class Transition
@constructor
@param {Object} router
@param {Object} intent
@param {Object} state
@param {Object} error
@private
*/
export default class Transition<T extends Route> implements Partial<Promise<T>> {
[STATE_SYMBOL]: TransitionState<T>;
from?: RouteInfo = undefined;
to?: RouteInfo = undefined;
router: Router<T>;
data: Dict<unknown>;
intent: Maybe<OpaqueIntent>;
resolvedModels: Dict<Dict<unknown>>;
[QUERY_PARAMS_SYMBOL]: Dict<unknown>;
promise?: Promise<any>; // Todo: Fix this shit its actually TransitionState | IHandler | undefined | Error
error: Maybe<Error>;
[PARAMS_SYMBOL]: Dict<unknown>;
routeInfos: InternalRouteInfo<Route>[];
targetName: Maybe<string>;
pivotHandler: Maybe<Route>;
sequence: number;
isAborted = false;
isActive = true;
urlMethod: Option<string> = 'update';
resolveIndex = 0;
queryParamsOnly = false;
isTransition = true;
isCausedByAbortingTransition = false;
isCausedByInitialTransition = false;
isCausedByAbortingReplaceTransition = false;
_visibleQueryParams: Dict<unknown> = {};
constructor(
router: Router<T>,
intent: Maybe<OpaqueIntent>,
state: TransitionState<T> | undefined,
error: Maybe<Error> = undefined,
previousTransition: Maybe<Transition<T>> = undefined
) {
this[STATE_SYMBOL] = state! || router.state!;
this.intent = intent;
this.router = router;
this.data = (intent && intent.data) || {};
this.resolvedModels = {};
this[QUERY_PARAMS_SYMBOL] = {};
this.promise = undefined;
this.error = undefined;
this[PARAMS_SYMBOL] = {};
this.routeInfos = [];
this.targetName = undefined;
this.pivotHandler = undefined;
this.sequence = -1;
if (error) {
this.promise = Promise.reject(error);
this.error = error;
return;
}
// if you're doing multiple redirects, need the new transition to know if it
// is actually part of the first transition or not. Any further redirects
// in the initial transition also need to know if they are part of the
// initial transition
this.isCausedByAbortingTransition = !!previousTransition;
this.isCausedByInitialTransition =
!!previousTransition &&
(previousTransition.isCausedByInitialTransition || previousTransition.sequence === 0);
// Every transition in the chain is a replace
this.isCausedByAbortingReplaceTransition =
!!previousTransition &&
(previousTransition.urlMethod === 'replace' &&
(!previousTransition.isCausedByAbortingTransition ||
previousTransition.isCausedByAbortingReplaceTransition));
if (state) {
this[PARAMS_SYMBOL] = state.params;
this[QUERY_PARAMS_SYMBOL] = state.queryParams;
this.routeInfos = state.routeInfos;
let len = state.routeInfos.length;
if (len) {
this.targetName = state.routeInfos[len - 1].name;
}
for (let i = 0; i < len; ++i) {
let handlerInfo = state.routeInfos[i];
// TODO: this all seems hacky
if (!handlerInfo.isResolved) {
break;
}
this.pivotHandler = handlerInfo.route;
}
this.sequence = router.currentSequence++;
this.promise = state
.resolve(() => {
if (this.isAborted) {
return Promise.reject(false, promiseLabel('Transition aborted - reject'));
}
return Promise.resolve(true);
}, this)
.catch((result: TransitionError) => {
return Promise.reject(this.router.transitionDidError(result, this));
}, promiseLabel('Handle Abort'));
} else {
this.promise = Promise.resolve(this[STATE_SYMBOL]!);
this[PARAMS_SYMBOL] = {};
}
}
/**
The Transition's internal promise. Calling `.then` on this property
is that same as calling `.then` on the Transition object itself, but
this property is exposed for when you want to pass around a
Transition's promise, but not the Transition object itself, since
Transition object can be externally `abort`ed, while the promise
cannot.
@property promise
@type {Object}
@public
*/
/**
Custom state can be stored on a Transition's `data` object.
This can be useful for decorating a Transition within an earlier
hook and shared with a later hook. Properties set on `data` will
be copied to new transitions generated by calling `retry` on this
transition.
@property data
@type {Object}
@public
*/
/**
A standard promise hook that resolves if the transition
succeeds and rejects if it fails/redirects/aborts.
Forwards to the internal `promise` property which you can
use in situations where you want to pass around a thennable,
but not the Transition itself.
@method then
@param {Function} onFulfilled
@param {Function} onRejected
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise}
@public
*/
then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
label?: string
): Promise<TResult1 | TResult2> {
return this.promise!.then(onFulfilled, onRejected, label);
}
/**
Forwards to the internal `promise` property which you can
use in situations where you want to pass around a thennable,
but not the Transition itself.
@method catch
@param {Function} onRejection
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise}
@public
*/
catch<T>(onRejection?: OnRejected<TransitionState<any>, T>, label?: string) {
return this.promise!.catch(onRejection, label);
}
/**
Forwards to the internal `promise` property which you can
use in situations where you want to pass around a thennable,
but not the Transition itself.
@method finally
@param {Function} callback
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise}
@public
*/
finally<T>(callback?: T | undefined, label?: string) {
return this.promise!.finally(callback, label);
}
/**
Aborts the Transition. Note you can also implicitly abort a transition
by initiating another transition while a previous one is underway.
@method abort
@return {Transition} this transition
@public
*/
abort() {
this.rollback();
let transition = new Transition(this.router, undefined, undefined, undefined);
transition.to = this.from;
transition.from = this.from;
transition.isAborted = true;
this.router.routeWillChange(transition);
this.router.routeDidChange(transition);
return this;
}
rollback() {
if (!this.isAborted) {
log(this.router, this.sequence, this.targetName + ': transition was aborted');
if (this.intent !== undefined && this.intent !== null) {
this.intent.preTransitionState = this.router.state;
}
this.isAborted = true;
this.isActive = false;
this.router.activeTransition = undefined;
}
}
redirect(newTransition: Transition<T>) {
this.rollback();
this.router.routeWillChange(newTransition);
}
/**
Retries a previously-aborted transition (making sure to abort the
transition if it's still active). Returns a new transition that
represents the new attempt to transition.
@method retry
@return {Transition} new transition
@public
*/
retry() {
// TODO: add tests for merged state retry()s
this.abort();
let newTransition = this.router.transitionByIntent(this.intent as OpaqueIntent, false);
// inheriting a `null` urlMethod is not valid
// the urlMethod is only set to `null` when
// the transition is initiated *after* the url
// has been updated (i.e. `router.handleURL`)
//
// in that scenario, the url method cannot be
// inherited for a new transition because then
// the url would not update even though it should
if (this.urlMethod !== null) {
newTransition.method(this.urlMethod);
}
return newTransition;
}
/**
Sets the URL-changing method to be employed at the end of a
successful transition. By default, a new Transition will just
use `updateURL`, but passing 'replace' to this method will
cause the URL to update using 'replaceWith' instead. Omitting
a parameter will disable the URL change, allowing for transitions
that don't update the URL at completion (this is also used for
handleURL, since the URL has already changed before the
transition took place).
@method method
@param {String} method the type of URL-changing method to use
at the end of a transition. Accepted values are 'replace',
falsy values, or any other non-falsy value (which is
interpreted as an updateURL transition).
@return {Transition} this transition
@public
*/
method(method: Option<string>) {
this.urlMethod = method;
return this;
}
// Alias 'trigger' as 'send'
send(
ignoreFailure: boolean,
_name: string,
err?: Error,
transition?: Transition<T>,
handler?: Route
) {
this.trigger(ignoreFailure, _name, err, transition, handler);
}
/**
Fires an event on the current list of resolved/resolving
handlers within this transition. Useful for firing events
on route hierarchies that haven't fully been entered yet.
Note: This method is also aliased as `send`
@method trigger
@param {Boolean} [ignoreFailure=false] a boolean specifying whether unhandled events throw an error
@param {String} name the name of the event to fire
@public
*/
trigger(ignoreFailure: boolean, name: string, ...args: any[]) {
this.router.triggerEvent(
this[STATE_SYMBOL]!.routeInfos.slice(0, this.resolveIndex + 1),
ignoreFailure,
name,
args
);
}
/**
Transitions are aborted and their promises rejected
when redirects occur; this method returns a promise
that will follow any redirects that occur and fulfill
with the value fulfilled by any redirecting transitions
that occur.
@method followRedirects
@return {Promise} a promise that fulfills with the same
value that the final redirecting transition fulfills with
@public
*/
followRedirects(): Promise<T> {
let router = this.router;
return this.promise!.catch(function(reason) {
if (router.activeTransition) {
return router.activeTransition.followRedirects();
}
return Promise.reject(reason);
});
}
toString() {
return 'Transition (sequence ' + this.sequence + ')';
}
/**
@private
*/
log(message: string) {
log(this.router, this.sequence, message);
}
}
/**
@private
Logs and returns an instance of TransitionAborted.
*/
export function logAbort(transition: Transition<any>): ITransitionAbortedError {
log(transition.router, transition.sequence, 'detected abort.');
return new TransitionAborted();
}
export function isTransition(obj: Dict<unknown> | undefined): obj is typeof Transition {
return typeof obj === 'object' && obj instanceof Transition && obj.isTransition;
}
export function prepareResult(obj: Dict<unknown> | undefined) {
if (isTransition(obj)) {
return null;
}
return obj;
}