@uirouter/core
Version:
UI-Router Core: Framework agnostic, State-based routing for JavaScript Single Page Apps
569 lines • 25.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateService = void 0;
var common_1 = require("../common/common");
var predicates_1 = require("../common/predicates");
var queue_1 = require("../common/queue");
var coreservices_1 = require("../common/coreservices");
var pathUtils_1 = require("../path/pathUtils");
var pathNode_1 = require("../path/pathNode");
var transitionService_1 = require("../transition/transitionService");
var rejectFactory_1 = require("../transition/rejectFactory");
var targetState_1 = require("./targetState");
var param_1 = require("../params/param");
var glob_1 = require("../common/glob");
var resolveContext_1 = require("../resolve/resolveContext");
var lazyLoad_1 = require("../hooks/lazyLoad");
var hof_1 = require("../common/hof");
/**
* Provides services related to ui-router states.
*
* This API is located at `router.stateService` ([[UIRouter.stateService]])
*/
var StateService = /** @class */ (function () {
/** @internal */
function StateService(/** @internal */ router) {
this.router = router;
/** @internal */
this.invalidCallbacks = [];
/** @internal */
this._defaultErrorHandler = function $defaultErrorHandler($error$) {
if ($error$ instanceof Error && $error$.stack) {
console.error($error$);
console.error($error$.stack);
}
else if ($error$ instanceof rejectFactory_1.Rejection) {
console.error($error$.toString());
if ($error$.detail && $error$.detail.stack)
console.error($error$.detail.stack);
}
else {
console.error($error$);
}
};
var getters = ['current', '$current', 'params', 'transition'];
var boundFns = Object.keys(StateService.prototype).filter(hof_1.not(common_1.inArray(getters)));
common_1.createProxyFunctions(hof_1.val(StateService.prototype), this, hof_1.val(this), boundFns);
}
Object.defineProperty(StateService.prototype, "transition", {
/**
* The [[Transition]] currently in progress (or null)
*
* @deprecated This is a passthrough through to [[UIRouterGlobals.transition]]
*/
get: function () {
return this.router.globals.transition;
},
enumerable: false,
configurable: true
});
Object.defineProperty(StateService.prototype, "params", {
/**
* The latest successful state parameters
*
* @deprecated This is a passthrough through to [[UIRouterGlobals.params]]
*/
get: function () {
return this.router.globals.params;
},
enumerable: false,
configurable: true
});
Object.defineProperty(StateService.prototype, "current", {
/**
* The current [[StateDeclaration]]
*
* @deprecated This is a passthrough through to [[UIRouterGlobals.current]]
*/
get: function () {
return this.router.globals.current;
},
enumerable: false,
configurable: true
});
Object.defineProperty(StateService.prototype, "$current", {
/**
* The current [[StateObject]] (an internal API)
*
* @deprecated This is a passthrough through to [[UIRouterGlobals.$current]]
*/
get: function () {
return this.router.globals.$current;
},
enumerable: false,
configurable: true
});
/** @internal */
StateService.prototype.dispose = function () {
this.defaultErrorHandler(common_1.noop);
this.invalidCallbacks = [];
};
/**
* Handler for when [[transitionTo]] is called with an invalid state.
*
* Invokes the [[onInvalid]] callbacks, in natural order.
* Each callback's return value is checked in sequence until one of them returns an instance of TargetState.
* The results of the callbacks are wrapped in $q.when(), so the callbacks may return promises.
*
* If a callback returns an TargetState, then it is used as arguments to $state.transitionTo() and the result returned.
*
* @internal
*/
StateService.prototype._handleInvalidTargetState = function (fromPath, toState) {
var _this = this;
var fromState = pathUtils_1.PathUtils.makeTargetState(this.router.stateRegistry, fromPath);
var globals = this.router.globals;
var latestThing = function () { return globals.transitionHistory.peekTail(); };
var latest = latestThing();
var callbackQueue = new queue_1.Queue(this.invalidCallbacks.slice());
var injector = new resolveContext_1.ResolveContext(fromPath).injector();
var checkForRedirect = function (result) {
if (!(result instanceof targetState_1.TargetState)) {
return;
}
var target = result;
// Recreate the TargetState, in case the state is now defined.
target = _this.target(target.identifier(), target.params(), target.options());
if (!target.valid()) {
return rejectFactory_1.Rejection.invalid(target.error()).toPromise();
}
if (latestThing() !== latest) {
return rejectFactory_1.Rejection.superseded().toPromise();
}
return _this.transitionTo(target.identifier(), target.params(), target.options());
};
function invokeNextCallback() {
var nextCallback = callbackQueue.dequeue();
if (nextCallback === undefined)
return rejectFactory_1.Rejection.invalid(toState.error()).toPromise();
var callbackResult = coreservices_1.services.$q.when(nextCallback(toState, fromState, injector));
return callbackResult.then(checkForRedirect).then(function (result) { return result || invokeNextCallback(); });
}
return invokeNextCallback();
};
/**
* Registers an Invalid State handler
*
* Registers a [[OnInvalidCallback]] function to be invoked when [[StateService.transitionTo]]
* has been called with an invalid state reference parameter
*
* Example:
* ```js
* stateService.onInvalid(function(to, from, injector) {
* if (to.name() === 'foo') {
* let lazyLoader = injector.get('LazyLoadService');
* return lazyLoader.load('foo')
* .then(() => stateService.target('foo'));
* }
* });
* ```
*
* @param {function} callback invoked when the toState is invalid
* This function receives the (invalid) toState, the fromState, and an injector.
* The function may optionally return a [[TargetState]] or a Promise for a TargetState.
* If one is returned, it is treated as a redirect.
*
* @returns a function which deregisters the callback
*/
StateService.prototype.onInvalid = function (callback) {
this.invalidCallbacks.push(callback);
return function deregisterListener() {
common_1.removeFrom(this.invalidCallbacks)(callback);
}.bind(this);
};
/**
* Reloads the current state
*
* A method that force reloads the current state, or a partial state hierarchy.
* All resolves are re-resolved, and components reinstantiated.
*
* #### Example:
* ```js
* let app angular.module('app', ['ui.router']);
*
* app.controller('ctrl', function ($scope, $state) {
* $scope.reload = function(){
* $state.reload();
* }
* });
* ```
*
* Note: `reload()` is just an alias for:
*
* ```js
* $state.transitionTo($state.current, $state.params, {
* reload: true, inherit: false
* });
* ```
*
* @param reloadState A state name or a state object.
* If present, this state and all its children will be reloaded, but ancestors will not reload.
*
* #### Example:
* ```js
* //assuming app application consists of 3 states: 'contacts', 'contacts.detail', 'contacts.detail.item'
* //and current state is 'contacts.detail.item'
* let app angular.module('app', ['ui.router']);
*
* app.controller('ctrl', function ($scope, $state) {
* $scope.reload = function(){
* //will reload 'contact.detail' and nested 'contact.detail.item' states
* $state.reload('contact.detail');
* }
* });
* ```
*
* @returns A promise representing the state of the new transition. See [[StateService.go]]
*/
StateService.prototype.reload = function (reloadState) {
return this.transitionTo(this.current, this.params, {
reload: predicates_1.isDefined(reloadState) ? reloadState : true,
inherit: false,
notify: false,
});
};
/**
* Transition to a different state and/or parameters
*
* Convenience method for transitioning to a new state.
*
* `$state.go` calls `$state.transitionTo` internally but automatically sets options to
* `{ location: true, inherit: true, relative: router.globals.$current, notify: true }`.
* This allows you to use either an absolute or relative `to` argument (because of `relative: router.globals.$current`).
* It also allows you to specify * only the parameters you'd like to update, while letting unspecified parameters
* inherit from the current parameter values (because of `inherit: true`).
*
* #### Example:
* ```js
* let app = angular.module('app', ['ui.router']);
*
* app.controller('ctrl', function ($scope, $state) {
* $scope.changeState = function () {
* $state.go('contact.detail');
* };
* });
* ```
*
* @param to Absolute state name, state object, or relative state path (relative to current state).
*
* Some examples:
*
* - `$state.go('contact.detail')` - will go to the `contact.detail` state
* - `$state.go('^')` - will go to the parent state
* - `$state.go('^.sibling')` - if current state is `home.child`, will go to the `home.sibling` state
* - `$state.go('.child.grandchild')` - if current state is home, will go to the `home.child.grandchild` state
*
* @param params A map of the parameters that will be sent to the state, will populate $stateParams.
*
* Any parameters that are not specified will be inherited from current parameter values (because of `inherit: true`).
* This allows, for example, going to a sibling state that shares parameters defined by a parent state.
*
* @param options Transition options
*
* @returns {promise} A promise representing the state of the new transition.
*/
StateService.prototype.go = function (to, params, options) {
var defautGoOpts = { relative: this.$current, inherit: true };
var transOpts = common_1.defaults(options, defautGoOpts, transitionService_1.defaultTransOpts);
return this.transitionTo(to, params, transOpts);
};
/**
* Creates a [[TargetState]]
*
* This is a factory method for creating a TargetState
*
* This may be returned from a Transition Hook to redirect a transition, for example.
*/
StateService.prototype.target = function (identifier, params, options) {
if (options === void 0) { options = {}; }
// If we're reloading, find the state object to reload from
if (predicates_1.isObject(options.reload) && !options.reload.name)
throw new Error('Invalid reload state object');
var reg = this.router.stateRegistry;
options.reloadState =
options.reload === true ? reg.root() : reg.matcher.find(options.reload, options.relative);
if (options.reload && !options.reloadState)
throw new Error("No such reload state '" + (predicates_1.isString(options.reload) ? options.reload : options.reload.name) + "'");
return new targetState_1.TargetState(this.router.stateRegistry, identifier, params, options);
};
/** @internal */
StateService.prototype.getCurrentPath = function () {
var _this = this;
var globals = this.router.globals;
var latestSuccess = globals.successfulTransitions.peekTail();
var rootPath = function () { return [new pathNode_1.PathNode(_this.router.stateRegistry.root())]; };
return latestSuccess ? latestSuccess.treeChanges().to : rootPath();
};
/**
* Low-level method for transitioning to a new state.
*
* The [[go]] method (which uses `transitionTo` internally) is recommended in most situations.
*
* #### Example:
* ```js
* let app = angular.module('app', ['ui.router']);
*
* app.controller('ctrl', function ($scope, $state) {
* $scope.changeState = function () {
* $state.transitionTo('contact.detail');
* };
* });
* ```
*
* @param to State name or state object.
* @param toParams A map of the parameters that will be sent to the state,
* will populate $stateParams.
* @param options Transition options
*
* @returns A promise representing the state of the new transition. See [[go]]
*/
StateService.prototype.transitionTo = function (to, toParams, options) {
var _this = this;
if (toParams === void 0) { toParams = {}; }
if (options === void 0) { options = {}; }
var router = this.router;
var globals = router.globals;
options = common_1.defaults(options, transitionService_1.defaultTransOpts);
var getCurrent = function () { return globals.transition; };
options = common_1.extend(options, { current: getCurrent });
var ref = this.target(to, toParams, options);
var currentPath = this.getCurrentPath();
if (!ref.exists())
return this._handleInvalidTargetState(currentPath, ref);
if (!ref.valid())
return common_1.silentRejection(ref.error());
if (options.supercede === false && getCurrent()) {
return (rejectFactory_1.Rejection.ignored('Another transition is in progress and supercede has been set to false in TransitionOptions for the transition. So the transition was ignored in favour of the existing one in progress.').toPromise());
}
/**
* Special handling for Ignored, Aborted, and Redirected transitions
*
* The semantics for the transition.run() promise and the StateService.transitionTo()
* promise differ. For instance, the run() promise may be rejected because it was
* IGNORED, but the transitionTo() promise is resolved because from the user perspective
* no error occurred. Likewise, the transition.run() promise may be rejected because of
* a Redirect, but the transitionTo() promise is chained to the new Transition's promise.
*/
var rejectedTransitionHandler = function (trans) { return function (error) {
if (error instanceof rejectFactory_1.Rejection) {
var isLatest = router.globals.lastStartedTransitionId <= trans.$id;
if (error.type === rejectFactory_1.RejectType.IGNORED) {
isLatest && router.urlRouter.update();
// Consider ignored `Transition.run()` as a successful `transitionTo`
return coreservices_1.services.$q.when(globals.current);
}
var detail = error.detail;
if (error.type === rejectFactory_1.RejectType.SUPERSEDED && error.redirected && detail instanceof targetState_1.TargetState) {
// If `Transition.run()` was redirected, allow the `transitionTo()` promise to resolve successfully
// by returning the promise for the new (redirect) `Transition.run()`.
var redirect = trans.redirect(detail);
return redirect.run().catch(rejectedTransitionHandler(redirect));
}
if (error.type === rejectFactory_1.RejectType.ABORTED) {
isLatest && router.urlRouter.update();
return coreservices_1.services.$q.reject(error);
}
}
var errorHandler = _this.defaultErrorHandler();
errorHandler(error);
return coreservices_1.services.$q.reject(error);
}; };
var transition = this.router.transitionService.create(currentPath, ref);
var transitionToPromise = transition.run().catch(rejectedTransitionHandler(transition));
common_1.silenceUncaughtInPromise(transitionToPromise); // issue #2676
// Return a promise for the transition, which also has the transition object on it.
return common_1.extend(transitionToPromise, { transition: transition });
};
/**
* Checks if the current state *is* the provided state
*
* Similar to [[includes]] but only checks for the full state name.
* If params is supplied then it will be tested for strict equality against the current
* active params object, so all params must match with none missing and no extras.
*
* #### Example:
* ```js
* $state.$current.name = 'contacts.details.item';
*
* // absolute name
* $state.is('contact.details.item'); // returns true
* $state.is(contactDetailItemStateObject); // returns true
* ```
*
* // relative name (. and ^), typically from a template
* // E.g. from the 'contacts.details' template
* ```html
* <div ng-class="{highlighted: $state.is('.item')}">Item</div>
* ```
*
* @param stateOrName The state name (absolute or relative) or state object you'd like to check.
* @param params A param object, e.g. `{sectionId: section.id}`, that you'd like
* to test against the current active state.
* @param options An options object. The options are:
* - `relative`: If `stateOrName` is a relative state name and `options.relative` is set, .is will
* test relative to `options.relative` state (or name).
*
* @returns Returns true if it is the state.
*/
StateService.prototype.is = function (stateOrName, params, options) {
options = common_1.defaults(options, { relative: this.$current });
var state = this.router.stateRegistry.matcher.find(stateOrName, options.relative);
if (!predicates_1.isDefined(state))
return undefined;
if (this.$current !== state)
return false;
if (!params)
return true;
var schema = state.parameters({ inherit: true, matchingKeys: params });
return param_1.Param.equals(schema, param_1.Param.values(schema, params), this.params);
};
/**
* Checks if the current state *includes* the provided state
*
* A method to determine if the current active state is equal to or is the child of the
* state stateName. If any params are passed then they will be tested for a match as well.
* Not all the parameters need to be passed, just the ones you'd like to test for equality.
*
* #### Example when `$state.$current.name === 'contacts.details.item'`
* ```js
* // Using partial names
* $state.includes("contacts"); // returns true
* $state.includes("contacts.details"); // returns true
* $state.includes("contacts.details.item"); // returns true
* $state.includes("contacts.list"); // returns false
* $state.includes("about"); // returns false
* ```
*
* #### Glob Examples when `* $state.$current.name === 'contacts.details.item.url'`:
* ```js
* $state.includes("*.details.*.*"); // returns true
* $state.includes("*.details.**"); // returns true
* $state.includes("**.item.**"); // returns true
* $state.includes("*.details.item.url"); // returns true
* $state.includes("*.details.*.url"); // returns true
* $state.includes("*.details.*"); // returns false
* $state.includes("item.**"); // returns false
* ```
*
* @param stateOrName A partial name, relative name, glob pattern,
* or state object to be searched for within the current state name.
* @param params A param object, e.g. `{sectionId: section.id}`,
* that you'd like to test against the current active state.
* @param options An options object. The options are:
* - `relative`: If `stateOrName` is a relative state name and `options.relative` is set, .is will
* test relative to `options.relative` state (or name).
*
* @returns {boolean} Returns true if it does include the state
*/
StateService.prototype.includes = function (stateOrName, params, options) {
options = common_1.defaults(options, { relative: this.$current });
var glob = predicates_1.isString(stateOrName) && glob_1.Glob.fromString(stateOrName);
if (glob) {
if (!glob.matches(this.$current.name))
return false;
stateOrName = this.$current.name;
}
var state = this.router.stateRegistry.matcher.find(stateOrName, options.relative), include = this.$current.includes;
if (!predicates_1.isDefined(state))
return undefined;
if (!predicates_1.isDefined(include[state.name]))
return false;
if (!params)
return true;
var schema = state.parameters({ inherit: true, matchingKeys: params });
return param_1.Param.equals(schema, param_1.Param.values(schema, params), this.params);
};
/**
* Generates a URL for a state and parameters
*
* Returns the url for the given state populated with the given params.
*
* #### Example:
* ```js
* expect($state.href("about.person", { person: "bob" })).toEqual("/about/bob");
* ```
*
* @param stateOrName The state name or state object you'd like to generate a url from.
* @param params An object of parameter values to fill the state's required parameters.
* @param options Options object. The options are:
*
* @returns {string} compiled state url
*/
StateService.prototype.href = function (stateOrName, params, options) {
var defaultHrefOpts = {
lossy: true,
inherit: true,
absolute: false,
relative: this.$current,
};
options = common_1.defaults(options, defaultHrefOpts);
params = params || {};
var state = this.router.stateRegistry.matcher.find(stateOrName, options.relative);
if (!predicates_1.isDefined(state))
return null;
if (options.inherit)
params = this.params.$inherit(params, this.$current, state);
var nav = state && options.lossy ? state.navigable : state;
if (!nav || nav.url === undefined || nav.url === null) {
return null;
}
return this.router.urlRouter.href(nav.url, params, { absolute: options.absolute });
};
/**
* Sets or gets the default [[transitionTo]] error handler.
*
* The error handler is called when a [[Transition]] is rejected or when any error occurred during the Transition.
* This includes errors caused by resolves and transition hooks.
*
* Note:
* This handler does not receive certain Transition rejections.
* Redirected and Ignored Transitions are not considered to be errors by [[StateService.transitionTo]].
*
* The built-in default error handler logs the error to the console.
*
* You can provide your own custom handler.
*
* #### Example:
* ```js
* stateService.defaultErrorHandler(function() {
* // Do not log transitionTo errors
* });
* ```
*
* @param handler a global error handler function
* @returns the current global error handler
*/
StateService.prototype.defaultErrorHandler = function (handler) {
return (this._defaultErrorHandler = handler || this._defaultErrorHandler);
};
StateService.prototype.get = function (stateOrName, base) {
var reg = this.router.stateRegistry;
if (arguments.length === 0)
return reg.get();
return reg.get(stateOrName, base || this.$current);
};
/**
* Lazy loads a state
*
* Explicitly runs a state's [[StateDeclaration.lazyLoad]] function.
*
* @param stateOrName the state that should be lazy loaded
* @param transition the optional Transition context to use (if the lazyLoad function requires an injector, etc)
* Note: If no transition is provided, a noop transition is created using the from the current state to the current state.
* This noop transition is not actually run.
*
* @returns a promise to lazy load
*/
StateService.prototype.lazyLoad = function (stateOrName, transition) {
var state = this.get(stateOrName);
if (!state || !state.lazyLoad)
throw new Error('Can not lazy load ' + stateOrName);
var currentPath = this.getCurrentPath();
var target = pathUtils_1.PathUtils.makeTargetState(this.router.stateRegistry, currentPath);
transition = transition || this.router.transitionService.create(currentPath, target);
return lazyLoad_1.lazyLoadState(transition, state);
};
return StateService;
}());
exports.StateService = StateService;
//# sourceMappingURL=stateService.js.map