UNPKG

ui-router-core

Version:

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

274 lines 13.8 kB
"use strict"; /** @module state */ /** for typedoc */ var common_1 = require("../common/common"); var predicates_1 = require("../common/predicates"); var strings_1 = require("../common/strings"); var hof_1 = require("../common/hof"); var resolvable_1 = require("../resolve/resolvable"); var coreservices_1 = require("../common/coreservices"); var parseUrl = function (url) { if (!predicates_1.isString(url)) return false; var root = url.charAt(0) === '^'; return { val: root ? url.substring(1) : url, root: root }; }; function nameBuilder(state) { return state.name; } function selfBuilder(state) { state.self.$$state = function () { return state; }; return state.self; } function dataBuilder(state) { if (state.parent && state.parent.data) { state.data = state.self.data = common_1.inherit(state.parent.data, state.data); } return state.data; } var getUrlBuilder = function ($urlMatcherFactoryProvider, root) { return function urlBuilder(state) { var stateDec = state; // For future states, i.e., states whose name ends with `.**`, // match anything that starts with the url prefix if (stateDec && stateDec.url && stateDec.name && stateDec.name.match(/\.\*\*$/)) { stateDec.url += "{remainder:any}"; // match any path (.*) } var parsed = parseUrl(stateDec.url), parent = state.parent; var url = !parsed ? stateDec.url : $urlMatcherFactoryProvider.compile(parsed.val, { params: state.params || {}, paramMap: function (paramConfig, isSearch) { if (stateDec.reloadOnSearch === false && isSearch) paramConfig = common_1.extend(paramConfig || {}, { dynamic: true }); return paramConfig; } }); if (!url) return null; if (!$urlMatcherFactoryProvider.isMatcher(url)) throw new Error("Invalid url '" + url + "' in state '" + state + "'"); return (parsed && parsed.root) ? url : ((parent && parent.navigable) || root()).url.append(url); }; }; var getNavigableBuilder = function (isRoot) { return function navigableBuilder(state) { return !isRoot(state) && state.url ? state : (state.parent ? state.parent.navigable : null); }; }; var getParamsBuilder = function (paramFactory) { return function paramsBuilder(state) { var makeConfigParam = function (config, id) { return paramFactory.fromConfig(id, null, config); }; var urlParams = (state.url && state.url.parameters({ inherit: false })) || []; var nonUrlParams = common_1.values(common_1.mapObj(common_1.omit(state.params || {}, urlParams.map(hof_1.prop('id'))), makeConfigParam)); return urlParams.concat(nonUrlParams).map(function (p) { return [p.id, p]; }).reduce(common_1.applyPairs, {}); }; }; function pathBuilder(state) { return state.parent ? state.parent.path.concat(state) : [state]; } function includesBuilder(state) { var includes = state.parent ? common_1.extend({}, state.parent.includes) : {}; includes[state.name] = true; return includes; } /** * This is a [[StateBuilder.builder]] function for the `resolve:` block on a [[StateDeclaration]]. * * When the [[StateBuilder]] builds a [[StateObject]] object from a raw [[StateDeclaration]], this builder * validates the `resolve` property and converts it to a [[Resolvable]] array. * * resolve: input value can be: * * { * // analyzed but not injected * myFooResolve: function() { return "myFooData"; }, * * // function.toString() parsed, "DependencyName" dep as string (not min-safe) * myBarResolve: function(DependencyName) { return DependencyName.fetchSomethingAsPromise() }, * * // Array split; "DependencyName" dep as string * myBazResolve: [ "DependencyName", function(dep) { return dep.fetchSomethingAsPromise() }, * * // Array split; DependencyType dep as token (compared using ===) * myQuxResolve: [ DependencyType, function(dep) { return dep.fetchSometingAsPromise() }, * * // val.$inject used as deps * // where: * // corgeResolve.$inject = ["DependencyName"]; * // function corgeResolve(dep) { dep.fetchSometingAsPromise() } * // then "DependencyName" dep as string * myCorgeResolve: corgeResolve, * * // inject service by name * // When a string is found, desugar creating a resolve that injects the named service * myGraultResolve: "SomeService" * } * * or: * * [ * new Resolvable("myFooResolve", function() { return "myFooData" }), * new Resolvable("myBarResolve", function(dep) { return dep.fetchSomethingAsPromise() }, [ "DependencyName" ]), * { provide: "myBazResolve", useFactory: function(dep) { dep.fetchSomethingAsPromise() }, deps: [ "DependencyName" ] } * ] */ function resolvablesBuilder(state) { /** convert resolve: {} and resolvePolicy: {} objects to an array of tuples */ var objects2Tuples = function (resolveObj, resolvePolicies) { return Object.keys(resolveObj || {}).map(function (token) { return ({ token: token, val: resolveObj[token], deps: undefined, policy: resolvePolicies[token] }); }); }; /** fetch DI annotations from a function or ng1-style array */ var annotate = function (fn) { var $injector = coreservices_1.services.$injector; // ng1 doesn't have an $injector until runtime. // If the $injector doesn't exist, use "deferred" literal as a // marker indicating they should be annotated when runtime starts return fn['$inject'] || ($injector && $injector.annotate(fn, $injector.strictDi)) || "deferred"; }; /** true if the object has both `token` and `resolveFn`, and is probably a [[ResolveLiteral]] */ var isResolveLiteral = function (obj) { return !!(obj.token && obj.resolveFn); }; /** true if the object looks like a provide literal, or a ng2 Provider */ var isLikeNg2Provider = function (obj) { return !!((obj.provide || obj.token) && (obj.useValue || obj.useFactory || obj.useExisting || obj.useClass)); }; /** true if the object looks like a tuple from obj2Tuples */ var isTupleFromObj = function (obj) { return !!(obj && obj.val && (predicates_1.isString(obj.val) || predicates_1.isArray(obj.val) || predicates_1.isFunction(obj.val))); }; /** extracts the token from a Provider or provide literal */ var token = function (p) { return p.provide || p.token; }; /** Given a literal resolve or provider object, returns a Resolvable */ var literal2Resolvable = hof_1.pattern([ [hof_1.prop('resolveFn'), function (p) { return new resolvable_1.Resolvable(token(p), p.resolveFn, p.deps, p.policy); }], [hof_1.prop('useFactory'), function (p) { return new resolvable_1.Resolvable(token(p), p.useFactory, (p.deps || p.dependencies), p.policy); }], [hof_1.prop('useClass'), function (p) { return new resolvable_1.Resolvable(token(p), function () { return new p.useClass(); }, [], p.policy); }], [hof_1.prop('useValue'), function (p) { return new resolvable_1.Resolvable(token(p), function () { return p.useValue; }, [], p.policy, p.useValue); }], [hof_1.prop('useExisting'), function (p) { return new resolvable_1.Resolvable(token(p), common_1.identity, [p.useExisting], p.policy); }], ]); var tuple2Resolvable = hof_1.pattern([ [hof_1.pipe(hof_1.prop("val"), predicates_1.isString), function (tuple) { return new resolvable_1.Resolvable(tuple.token, common_1.identity, [tuple.val], tuple.policy); }], [hof_1.pipe(hof_1.prop("val"), predicates_1.isArray), function (tuple) { return new resolvable_1.Resolvable(tuple.token, common_1.tail(tuple.val), tuple.val.slice(0, -1), tuple.policy); }], [hof_1.pipe(hof_1.prop("val"), predicates_1.isFunction), function (tuple) { return new resolvable_1.Resolvable(tuple.token, tuple.val, annotate(tuple.val), tuple.policy); }], ]); var item2Resolvable = hof_1.pattern([ [hof_1.is(resolvable_1.Resolvable), function (r) { return r; }], [isResolveLiteral, literal2Resolvable], [isLikeNg2Provider, literal2Resolvable], [isTupleFromObj, tuple2Resolvable], [hof_1.val(true), function (obj) { throw new Error("Invalid resolve value: " + strings_1.stringify(obj)); }] ]); // If resolveBlock is already an array, use it as-is. // Otherwise, assume it's an object and convert to an Array of tuples var decl = state.resolve; var items = predicates_1.isArray(decl) ? decl : objects2Tuples(decl, state.resolvePolicy || {}); return items.map(item2Resolvable); } exports.resolvablesBuilder = resolvablesBuilder; /** * @internalapi A internal global service * * StateBuilder is a factory for the internal [[StateObject]] objects. * * When you register a state with the [[StateRegistry]], you register a plain old javascript object which * conforms to the [[StateDeclaration]] interface. This factory takes that object and builds the corresponding * [[StateObject]] object, which has an API and is used internally. * * Custom properties or API may be added to the internal [[StateObject]] object by registering a decorator function * using the [[builder]] method. */ var StateBuilder = (function () { function StateBuilder(matcher, urlMatcherFactory) { this.matcher = matcher; var self = this; var root = function () { return matcher.find(""); }; var isRoot = function (state) { return state.name === ""; }; function parentBuilder(state) { if (isRoot(state)) return null; return matcher.find(self.parentName(state)) || root(); } this.builders = { name: [nameBuilder], self: [selfBuilder], parent: [parentBuilder], data: [dataBuilder], // Build a URLMatcher if necessary, either via a relative or absolute URL url: [getUrlBuilder(urlMatcherFactory, root)], // Keep track of the closest ancestor state that has a URL (i.e. is navigable) navigable: [getNavigableBuilder(isRoot)], params: [getParamsBuilder(urlMatcherFactory.paramFactory)], // Each framework-specific ui-router implementation should define its own `views` builder // e.g., src/ng1/statebuilders/views.ts views: [], // Keep a full path from the root down to this state as this is needed for state activation. path: [pathBuilder], // Speed up $state.includes() as it's used a lot includes: [includesBuilder], resolvables: [resolvablesBuilder] }; } /** * Registers a [[BuilderFunction]] for a specific [[StateObject]] property (e.g., `parent`, `url`, or `path`). * More than one BuilderFunction can be registered for a given property. * * The BuilderFunction(s) will be used to define the property on any subsequently built [[StateObject]] objects. * * @param name The name of the State property being registered for. * @param fn The BuilderFunction which will be used to build the State property * @returns a function which deregisters the BuilderFunction */ StateBuilder.prototype.builder = function (name, fn) { var builders = this.builders; var array = builders[name] || []; // Backwards compat: if only one builder exists, return it, else return whole arary. if (predicates_1.isString(name) && !predicates_1.isDefined(fn)) return array.length > 1 ? array : array[0]; if (!predicates_1.isString(name) || !predicates_1.isFunction(fn)) return; builders[name] = array; builders[name].push(fn); return function () { return builders[name].splice(builders[name].indexOf(fn, 1)) && null; }; }; /** * Builds all of the properties on an essentially blank State object, returning a State object which has all its * properties and API built. * * @param state an uninitialized State object * @returns the built State object */ StateBuilder.prototype.build = function (state) { var _a = this, matcher = _a.matcher, builders = _a.builders; var parent = this.parentName(state); if (parent && !matcher.find(parent, undefined, false)) { return null; } for (var key in builders) { if (!builders.hasOwnProperty(key)) continue; var chain = builders[key].reduce(function (parentFn, step) { return function (_state) { return step(_state, parentFn); }; }, common_1.noop); state[key] = chain(state); } return state; }; StateBuilder.prototype.parentName = function (state) { var name = state.name || ""; var segments = name.split('.'); if (segments.length > 1) { if (state.parent) { throw new Error("States that specify the 'parent:' property should not have a '.' in their name (" + name + ")"); } var lastSegment = segments.pop(); if (lastSegment === '**') segments.pop(); return segments.join("."); } if (!state.parent) return ""; return predicates_1.isString(state.parent) ? state.parent : state.parent.name; }; StateBuilder.prototype.name = function (state) { var name = state.name; if (name.indexOf('.') !== -1 || !state.parent) return name; var parentName = predicates_1.isString(state.parent) ? state.parent : state.parent.name; return parentName ? parentName + "." + name : name; }; return StateBuilder; }()); exports.StateBuilder = StateBuilder; //# sourceMappingURL=stateBuilder.js.map