Skip to content

feat(uiStateActive): directive to add/remove classes for active state #635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 5, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,24 @@ function ancestors(first, second) {
return path;
}

/**
* IE8-safe wrapper for `Object.keys()`.
*
* @param {Object} object A JavaScript object.
* @return {Array} Returns the keys of the object as an array.
*/
function keys(object) {
if (Object.keys) {
return Object.keys(object);
}
var result = [];

angular.forEach(object, function(val, key) {
result.push(key);
});
return result;
}

/**
* IE8-safe wrapper for `Array.prototype.indexOf()`.
*
Expand Down Expand Up @@ -91,6 +109,61 @@ function inheritParams(currentParams, newParams, $current, $to) {
return extend({}, inherited, newParams);
}

/**
* Normalizes a set of values to string or `null`, filtering them by a list of keys.
*
* @param {Array} keys The list of keys to normalize/return.
* @param {Object} values An object hash of values to normalize.
* @return {Object} Returns an object hash of normalized string values.
*/
function normalize(keys, values) {
var normalized = {};

forEach(keys, function (name) {
var value = values[name];
normalized[name] = (value != null) ? String(value) : null;
});
return normalized;
}

/**
* Performs a non-strict comparison of the subset of two objects, defined by a list of keys.
*
* @param {Object} a The first object.
* @param {Object} b The second object.
* @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified,
* it defaults to the list of keys in `a`.
* @return {Boolean} Returns `true` if the keys match, otherwise `false`.
*/
function equalForKeys(a, b, keys) {
if (!keys) {
keys = [];
for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility
}

for (var i=0; i<keys.length; i++) {
var k = keys[i];
if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized
}
return true;
}

/**
* Returns the subset of an object, based on a list of keys.
*
* @param {Array} keys
* @param {Object} values
* @return {Boolean} Returns a subset of `values`.
*/
function filterByKeys(keys, values) {
var filtered = {};

forEach(keys, function (name) {
filtered[name] = values[name];
});
return filtered;
}

angular.module('ui.router.util', ['ng']);
angular.module('ui.router.router', ['ui.router.util']);
angular.module('ui.router.state', ['ui.router.router', 'ui.router.util']);
Expand Down
37 changes: 2 additions & 35 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,13 +488,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
return url;
};

$state.get = function (stateOrName) {
$state.get = function (stateOrName, context) {
if (!isDefined(stateOrName)) {
var list = [];
forEach(states, function(state) { list.push(state.self); });
return list;
}
var state = findState(stateOrName);
var state = findState(stateOrName, context);
return (state && state.self) ? state.self : null;
};

Expand Down Expand Up @@ -546,39 +546,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
return $state;
}

function normalize(keys, values) {
var normalized = {};

forEach(keys, function (name) {
var value = values[name];
normalized[name] = (value != null) ? String(value) : null;
});
return normalized;
}

function equalForKeys(a, b, keys) {
// If keys not provided, assume keys from object 'a'
if (!keys) {
keys = [];
for (var n in a) keys.push(n); // Used instead of Object.keys() for IE8 compatibility
}

for (var i=0; i<keys.length; i++) {
var k = keys[i];
if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized
}
return true;
}

function filterByKeys(keys, values) {
var filtered = {};

forEach(keys, function (name) {
filtered[name] = values[name];
});
return filtered;
}

function shouldTriggerReload(to, from, locals, options) {
if ( to === from && ((locals === from.locals && !options.reload) || (to.self.reloadOnSearch === false)) ) {
return true;
Expand Down
61 changes: 52 additions & 9 deletions src/stateDirectives.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,25 @@ function parseStateRef(ref) {
return { state: parsed[1], paramExpr: parsed[3] || null };
}

function stateContext(el) {
var stateData = el.parent().inheritedData('$uiView');

if (stateData && stateData.state && stateData.state.name) {
return stateData.state;
}
}

$StateRefDirective.$inject = ['$state'];
function $StateRefDirective($state) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
require: '?^uiSrefActive',
link: function(scope, element, attrs, uiSrefActive) {
var ref = parseStateRef(attrs.uiSref);
var params = null, url = null, base = $state.$current;
var params = null, url = null, base = stateContext(element) || $state.$current;
var isForm = element[0].nodeName === "FORM";
var attr = isForm ? "action" : "href", nav = true;

var stateData = element.parent().inheritedData('$uiView');

if (stateData && stateData.state && stateData.state.name) {
base = stateData.state;
}

var update = function(newVal) {
if (newVal) params = newVal;
if (!nav) return;
Expand All @@ -31,6 +34,9 @@ function $StateRefDirective($state) {
return false;
}
element[0][attr] = newHref;
if (uiSrefActive) {
uiSrefActive.$$setStateInfo(ref.state, params);
}
};

if (ref.paramExpr) {
Expand All @@ -57,4 +63,41 @@ function $StateRefDirective($state) {
};
}

angular.module('ui.router.state').directive('uiSref', $StateRefDirective);
$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
function $StateActiveDirective($state, $stateParams, $interpolate) {
return {
restrict: "A",
controller: function($scope, $element, $attrs) {
var state, params, activeClass;

// There probably isn't much point in $observing this
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);

// Allow uiSref to communicate with uiSrefActive
this.$$setStateInfo = function(newState, newParams) {
state = $state.get(newState, stateContext($element));
params = newParams;
update();
};

$scope.$on('$stateChangeSuccess', update);

// Update route state
function update() {
if ($state.$current.self === state && matchesParams()) {
$element.addClass(activeClass);
} else {
$element.removeClass(activeClass);
}
}

function matchesParams() {
return !params || equalForKeys(params, $stateParams);
}
}
};
}

angular.module('ui.router.state')
.directive('uiSref', $StateRefDirective)
.directive('uiSrefActive', $StateActiveDirective);
76 changes: 76 additions & 0 deletions test/stateDirectivesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,79 @@ describe('uiStateRef', function() {
}));
});
});

describe('uiSrefActive', function() {
var el, template, scope, document;

beforeEach(module('ui.router'));

beforeEach(module(function($stateProvider) {
$stateProvider.state('index', {
url: '',
}).state('contacts', {
url: '/contacts',
views: {
'@': {
template: '<a ui-sref=".item({ id: 6 })" ui-sref-active="active">Contacts</a>'
}
}
}).state('contacts.item', {
url: '/:id',
}).state('contacts.item.detail', {
url: '/detail/:foo'
});
}));

beforeEach(inject(function($document) {
document = $document[0];
}));

it('should update class for sibling uiSref', inject(function($rootScope, $q, $compile, $state) {
el = angular.element('<div><a ui-sref="contacts" ui-sref-active="active">Contacts</a></div>');
template = $compile(el)($rootScope);
$rootScope.$digest();

expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
$state.transitionTo('contacts');
$q.flush();

expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');

$state.transitionTo('contacts.item', { id: 5 });
$q.flush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
}));

it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state) {
el = angular.element('<div><a ui-sref="contacts.item.detail({ foo: \'bar\' })" ui-sref-active="active">Contacts</a></div>');
template = $compile(el)($rootScope);
$rootScope.$digest();

expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
$state.transitionTo('contacts.item.detail', { id: 5, foo: 'bar' });
$q.flush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');

$state.transitionTo('contacts.item.detail', { id: 5, foo: 'baz' });
$q.flush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
}));

it('should resolve relative state refs', inject(function($rootScope, $q, $compile, $state) {
el = angular.element('<section><div ui-view></div></section>');
template = $compile(el)($rootScope);
$rootScope.$digest();

$state.transitionTo('contacts');
$q.flush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');

$state.transitionTo('contacts.item', { id: 6 });
$q.flush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active');

$state.transitionTo('contacts.item', { id: 5 });
$q.flush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope');
}));
});