Skip to content

Commit

Permalink
feat(params): Add uiOnParamsChanged controller callback
Browse files Browse the repository at this point in the history
closes #2608
closes #2470
closes #2391
closes #1967
  • Loading branch information
christopherthielen committed Mar 26, 2016
1 parent 1541b90 commit 961c96d
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 331 deletions.
4 changes: 0 additions & 4 deletions src/ng1/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,6 @@ const resolveFactory = () => ({
});

function $stateParamsFactory(ng1UIRouter, $rootScope) {
$rootScope.$watch(function() {
router.stateParams.$digest();
});

return router.stateParams;
}

Expand Down
57 changes: 55 additions & 2 deletions src/ng1/viewDirective.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @module view */ /** for typedoc */
"use strict";
import {extend, map} from "../common/common";
import {extend, map, unnestR, filter} from "../common/common";
import {isDefined, isFunction} from "../common/predicates";
import {trace} from "../common/trace";
import {ActiveUIView} from "../view/interface";
Expand All @@ -9,6 +9,9 @@ import {RejectType} from "../transition/rejectFactory";
import {TransitionService} from "../transition/transitionService";
import {parse} from "../common/hof";
import {ResolveContext} from "../resolve/resolveContext";
import {Transition} from "../transition/transition";
import {Node} from "../path/node";
import {Param} from "../params/param";

export type UIViewData = {
$cfg: Ng1ViewConfig;
Expand Down Expand Up @@ -340,9 +343,11 @@ function $ViewDirectiveFill ( $compile, $controller, $transitions, $view,
scope[controllerAs] = controllerInstance;
scope[controllerAs][resolveAs] = locals;
}
if (isFunction(controllerInstance.$onInit)) controllerInstance.$onInit();

$element.data('$ngControllerController', controllerInstance);
$element.children().data('$ngControllerController', controllerInstance);

registerControllerCallbacks($transitions, controllerInstance, scope, cfg);
}

link(scope);
Expand All @@ -351,5 +356,53 @@ function $ViewDirectiveFill ( $compile, $controller, $transitions, $view,
};
}

// TODO: move these callbacks to $view and/or `/hooks/components.ts` or something
function registerControllerCallbacks($transitions: TransitionService, controllerInstance, $scope, cfg: Ng1ViewConfig) {
// Call $onInit() ASAP
if (isFunction(controllerInstance.$onInit)) controllerInstance.$onInit();

// Add component-level hook for onParamsChange
if (isFunction(controllerInstance.uiOnParamsChanged)) {
// Fire callback on any successful transition
const paramsUpdated = ($transition$: Transition) => {
let ctx: ResolveContext = cfg.node.resolveContext;
let viewCreationTrans = ctx.getResolvables()['$transition$'].data;
// Exit early if the $transition$ is the same as the view was created within.
// Exit early if the $transition$ will exit the state the view is for.
if ($transition$ === viewCreationTrans || $transition$.exiting().indexOf(cfg.node.state.self) !== -1) return;

let toParams = $transition$.params("to");
let fromParams = $transition$.params("from");
let toSchema: Param[] = $transition$.treeChanges().to.map((node: Node) => node.paramSchema).reduce(unnestR, []);
let fromSchema: Param[] = $transition$.treeChanges().from.map((node: Node) => node.paramSchema).reduce(unnestR, []);

// Find the to params that have different values than the from params
let changedToParams = toSchema.filter((param: Param) => {
let idx = fromSchema.indexOf(param);
return idx === -1 || !fromSchema[idx].type.equals(toParams[param.id], fromParams[param.id]);
});

// Only trigger callback if a to param has changed or is new
if (changedToParams.length) {
let changedKeys = changedToParams.map(x => x.id);
// Filter the params to only changed/new to params. `$transition$.params()` may be used to get all params.
controllerInstance.uiOnParamsChanged(filter(toParams, (val, key) => changedKeys.indexOf(key) !== -1), $transition$);
}
};
$scope.$on('$destroy', $transitions.onSuccess({}, ['$transition$', paramsUpdated]));

// Fire callback on any IGNORED transition
let onDynamic = ($error$, $transition$) => {
if ($error$.type === RejectType.IGNORED) paramsUpdated($transition$);
};
$scope.$on('$destroy', $transitions.onError({}, ['$error$', '$transition$', onDynamic]));
}

// Add component-level hook for canDeactivate
if (isFunction(controllerInstance.canDeactivate)) {
$scope.$on('$destroy', $transitions.onBefore({exiting: cfg.node.state.name}, controllerInstance.canDeactivate.bind(controllerInstance)));
}
}

angular.module('ui.router.state').directive('uiView', $ViewDirective);
angular.module('ui.router.state').directive('uiView', $ViewDirectiveFill);
44 changes: 34 additions & 10 deletions src/params/param.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @module params */ /** for typedoc */
import {extend, filter, map, applyPairs} from "../common/common";
import {extend, filter, map, applyPairs, allTrueR} from "../common/common";
import {prop, propEq} from "../common/hof";
import {isInjectable, isDefined, isString, isArray} from "../common/predicates";
import {RawParams} from "../params/interface";
Expand Down Expand Up @@ -135,31 +135,55 @@ export class Param {
return `{Param:${this.id} ${this.type} squash: '${this.squash}' optional: ${this.isOptional}}`;
}

/** Creates a new [[Param]] from a CONFIG block */
static fromConfig(id: string, type: Type, config: any): Param {
return new Param(id, type, config, DefType.CONFIG);
}

/** Creates a new [[Param]] from a url PATH */
static fromPath(id: string, type: Type, config: any): Param {
return new Param(id, type, config, DefType.PATH);
}

/** Creates a new [[Param]] from a url SEARCH */
static fromSearch(id: string, type: Type, config: any): Param {
return new Param(id, type, config, DefType.SEARCH);
}

static values(params: Param[], values): RawParams {
values = values || {};
static values(params: Param[], values = {}): RawParams {
return <RawParams> params.map(param => [param.id, param.value(values[param.id])]).reduce(applyPairs, {});
}

static equals(params: Param[], values1, values2): boolean {
values1 = values1 || {};
values2 = values2 || {};
return params.map(param => param.type.equals(values1[param.id], values2[param.id])).indexOf(false) === -1;
/**
* Finds [[Param]] objects which have different param values
*
* Filters a list of [[Param]] objects to only those whose parameter values differ in two param value objects
*
* @param params: The list of Param objects to filter
* @param values1: The first set of parameter values
* @param values2: the second set of parameter values
*
* @returns any Param objects whose values were different between values1 and values2
*/
static changed(params: Param[], values1 = {}, values2 = {}): Param[] {
return params.filter(param => !param.type.equals(values1[param.id], values2[param.id]));
}

/**
* Checks if two param value objects are equal (for a set of [[Param]] objects)
*
* @param params The list of [[Param]] objects to check
* @param values1 The first set of param values
* @param values2 The second set of param values
*
* @returns true if the param values in values1 and values2 are equal
*/
static equals(params: Param[], values1 = {}, values2 = {}): boolean {
return Param.changed(params, values1, values2).length === 0;
}

static validates(params: Param[], values): boolean {
values = values || {};
return params.map(param => param.validates(values[param.id])).indexOf(false) === -1;
/** Returns true if a the parameter values are valid, according to the Param definitions */
static validates(params: Param[], values = {}): boolean {
return params.map(param => param.validates(values[param.id])).reduce(allTrueR, true);
}
}
136 changes: 28 additions & 108 deletions src/params/stateParams.ts
Original file line number Diff line number Diff line change
@@ -1,117 +1,37 @@
/** @module params */ /** for typedoc */
import {forEach, ancestors, extend, copy, pick, omit} from "../common/common";
import {extend, ancestors} from "../common/common";

export class StateParams {
constructor(params: Object = {}) {
extend(this, params);
}

$digest() {}
$inherit(newParams, $current, $to) {}
$set(params, url) {}
$sync() {}
$off() {}
$raw() {}
$localize(state, params) {}
$observe(key: string, fn: Function) {}
}


export function stateParamsFactory() {
let observers = {}, current = {};

function unhook(key, func) {
return () => {
forEach(key.split(" "), k => observers[k].splice(observers[k].indexOf(func), 1));
};
}

function observeChange(key, val?: any) {
if (!observers[key] || !observers[key].length) return;
forEach(observers[key], func => func(val));
}


StateParams.prototype.$digest = function() {
forEach(this, (val, key) => {
if (val === current[key] || !this.hasOwnProperty(key)) return;
current[key] = val;
observeChange(key, val);
});
};

/**
* Merges a set of parameters with all parameters inherited between the common parents of the
* current state and a given destination state.
*
* @param {Object} newParams The set of parameters which will be composited with inherited params.
* @param {Object} $current Internal definition of object representing the current state.
* @param {Object} $to Internal definition of object representing state to transition to.
*/
StateParams.prototype.$inherit = function(newParams, $current, $to) {
let parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];

for (let i in parents) {
if (!parents[i] || !parents[i].params) continue;
parentParams = Object.keys(parents[i].params);
if (!parentParams.length) continue;

for (let j in parentParams) {
if (inheritList.indexOf(parentParams[j]) >= 0) continue;
inheritList.push(parentParams[j]);
inherited[parentParams[j]] = this[parentParams[j]];
}
}
return extend({}, inherited, newParams);
};

StateParams.prototype.$set = function(params, url) {
let hasChanged = false, abort = false;

if (url) {
forEach(params, function(val, key) {
if ((url.parameter(key) || {}).dynamic !== true) abort = true;
});
/**
* Merges a set of parameters with all parameters inherited between the common parents of the
* current state and a given destination state.
*
* @param {Object} newParams The set of parameters which will be composited with inherited params.
* @param {Object} $current Internal definition of object representing the current state.
* @param {Object} $to Internal definition of object representing state to transition to.
*/
$inherit(newParams, $current, $to) {
let parents = ancestors($current, $to), parentParams, inherited = {}, inheritList = [];

for (let i in parents) {
if (!parents[i] || !parents[i].params) continue;
parentParams = Object.keys(parents[i].params);
if (!parentParams.length) continue;

for (let j in parentParams) {
if (inheritList.indexOf(parentParams[j]) >= 0) continue;
inheritList.push(parentParams[j]);
inherited[parentParams[j]] = this[parentParams[j]];
}
if (abort) return false;

forEach(params, (val, key) => {
if (val !== this[key]) {
this[key] = val;
observeChange(key);
hasChanged = true;
}
});

this.$sync();
return hasChanged;
};

StateParams.prototype.$sync = function() {
copy(this, current);
return this;
};

StateParams.prototype.$off = function() {
observers = {};
return this;
};

StateParams.prototype.$raw = function() {
return omit(
this,
Object.keys(this).filter(StateParams.prototype.hasOwnProperty.bind(StateParams.prototype))
);
};

StateParams.prototype.$localize = function(state, params) {
return new StateParams(pick(params || this, Object.keys(state.params)));
};

StateParams.prototype.$observe = function(key: string, fn: Function) {
forEach(key.split(" "), k => (observers[k] || (observers[k] = [])).push(fn));
return unhook(key, fn);
};
}
return extend({}, inherited, newParams);
};
}

return new StateParams();
}
export function stateParamsFactory() {
return new StateParams();
}
1 change: 0 additions & 1 deletion src/state/hooks/transitionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ export class TransitionManager {
let options = transition.options();
$state.params = transition.params();
copy($state.params, $stateParams);
$stateParams.$sync().$off();

if (options.location && $state.$current.navigable) {
$urlRouter.push($state.$current.navigable.url, $stateParams, { replace: options.location === 'replace' });
Expand Down
Loading

0 comments on commit 961c96d

Please sign in to comment.