UNPKG

@uirouter/core

Version:

UI-Router Core: Framework agnostic, State-based routing for JavaScript Single Page Apps

569 lines 25.7 kB
"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