Skip to content

Commit

Permalink
feat(uiSrefActive): Also activate for child states.
Browse files Browse the repository at this point in the history
To limit activation to target state use new ui-sref-active-equals directive'

Breaking Change: Since ui-sref-active now activates even when child states are active you may need to swap out your ui-sref-active with ui-sref-active-equals, thought typically we think devs want the auto inheritance.

Fixes #818
  • Loading branch information
timkindberg committed Mar 4, 2014
1 parent cb1c9cf commit eec4932
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 23 deletions.
76 changes: 56 additions & 20 deletions src/stateDirectives.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function $StateRefDirective($state, $timeout) {

return {
restrict: 'A',
require: '?^uiSrefActive',
require: ['?^uiSrefActive', '?^uiSrefActiveEquals'],
link: function(scope, element, attrs, uiSrefActive) {
var ref = parseStateRef(attrs.uiSref);
var params = null, url = null, base = stateContext(element) || $state.$current;
Expand All @@ -103,8 +103,9 @@ function $StateRefDirective($state, $timeout) {

var newHref = $state.href(ref.state, params, options);

if (uiSrefActive) {
uiSrefActive.$$setStateInfo(ref.state, params);
var activeDirective = uiSrefActive[1] || uiSrefActive[0];
if (activeDirective) {
activeDirective.$$setStateInfo(ref.state, params);
}
if (!newHref) {
nav = false;
Expand Down Expand Up @@ -148,12 +149,19 @@ function $StateRefDirective($state, $timeout) {
* @restrict A
*
* @description
* A directive working alongside ui-sref to add classes to an element when the
* A directive working alongside ui-sref to add classes to an element when the
* related ui-sref directive's state is active, and removing them when it is inactive.
* The primary use-case is to simplify the special appearance of navigation menus
* The primary use-case is to simplify the special appearance of navigation menus
* relying on `ui-sref`, by having the "active" state's menu button appear different,
* distinguishing it from the inactive menu items.
*
* ui-sref-active can live on the same element as ui-sref or on a parent element. The first
* one found will be used.
*
* Only activates when the ui-sref's actual target state is active, *not* any of it's children.
* For adding a class based on active children use
* {@link ui.router.state.directive:ui-sref-active-equals ui-sref-active-equals}
*
* @example
* Given the following template:
* <pre>
Expand All @@ -163,8 +171,8 @@ function $StateRefDirective($state, $timeout) {
* </li>
* </ul>
* </pre>
*
* When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins",
*
* When the app state is "app.user", and contains the state parameter "user" with value "bilbobaggins",
* the resulting HTML will appear as (note the 'active' class):
* <pre>
* <ul>
Expand All @@ -173,10 +181,10 @@ function $StateRefDirective($state, $timeout) {
* </li>
* </ul>
* </pre>
*
* The class name is interpolated **once** during the directives link time (any further changes to the
* interpolated value are ignored).
*
*
* The class name is interpolated **once** during the directives link time (any further changes to the
* interpolated value are ignored).
*
* Multiple classes may be specified in a space-separated format:
* <pre>
* <ul>
Expand All @@ -186,18 +194,37 @@ function $StateRefDirective($state, $timeout) {
* </ul>
* </pre>
*/
$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
function $StateActiveDirective($state, $stateParams, $interpolate) {
return {

/**
* @ngdoc directive
* @name ui.router.state.directive:ui-sref-active-equals
*
* @requires ui.router.state.$state
* @requires ui.router.state.$stateParams
* @requires $interpolate
*
* @restrict A
*
* @description
* The same as {@link ui.router.state.directive:ui-sref-active ui-sref-active} but will also activate
* when any descendant state of the ui-sref's target is active. It uses `$state.includes(uiSrefState)`
* to check for activation.
*
*/
$StateRefActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
function $StateRefActiveDirective($state, $stateParams, $interpolate) {
return {
restrict: "A",
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
var state, params, activeClass;

// There probably isn't much point in $observing this
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
// uiSrefActive and uiSrefActiveEquals share the same directive object with some
// slight difference in logic routing
activeClass = $interpolate($attrs.uiSrefActiveEquals || $attrs.uiSrefActive || '', false)($scope);

// Allow uiSref to communicate with uiSrefActive
this.$$setStateInfo = function(newState, newParams) {
// Allow uiSref to communicate with uiSrefActive[Equals]
this.$$setStateInfo = function (newState, newParams) {
state = $state.get(newState, stateContext($element));
params = newParams;
update();
Expand All @@ -207,13 +234,21 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {

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

function isMatch() {
if (typeof $attrs.uiSrefActiveEquals !== 'undefined') {
return $state.$current.self === state && matchesParams();
} else {
return $state.includes(state.name) && matchesParams();
}
}

function matchesParams() {
return !params || equalForKeys(params, $stateParams);
}
Expand All @@ -223,4 +258,5 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {

angular.module('ui.router.state')
.directive('uiSref', $StateRefDirective)
.directive('uiSrefActive', $StateActiveDirective);
.directive('uiSrefActive', $StateRefActiveDirective)
.directive('uiSrefActiveEquals', $StateRefActiveDirective);
36 changes: 33 additions & 3 deletions test/stateDirectivesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,8 @@ describe('uiSrefActive', function() {
url: '/:id',
}).state('contacts.item.detail', {
url: '/detail/:foo'
}).state('contacts.item.edit', {
url: '/edit'
});
}));

Expand All @@ -312,17 +314,17 @@ describe('uiSrefActive', function() {
}));

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>');
el = angular.element('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a><a ui-sref="contacts.item({ id: 2 })" 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');
$state.transitionTo('contacts.item', { id: 1 });
$q.flush();

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

$state.transitionTo('contacts.item', { id: 5 });
$state.transitionTo('contacts.item', { id: 2 });
$q.flush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
}));
Expand All @@ -342,6 +344,34 @@ describe('uiSrefActive', function() {
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('');
}));

it('should match on child states', inject(function($rootScope, $q, $compile, $state) {
template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a></div>')($rootScope);
$rootScope.$digest();
var a = angular.element(template[0].getElementsByTagName('a')[0]);

$state.transitionTo('contacts.item.edit', { id: 1 });
$q.flush();
expect(a.attr('class')).toMatch(/active/);

$state.transitionTo('contacts.item.edit', { id: 4 });
$q.flush();
expect(a.attr('class')).not.toMatch(/active/);
}));

it('should NOT match on child states when active-equals is used', inject(function($rootScope, $q, $compile, $state) {
template = $compile('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active-equals="active">Contacts</a></div>')($rootScope);
$rootScope.$digest();
var a = angular.element(template[0].getElementsByTagName('a')[0]);

$state.transitionTo('contacts.item', { id: 1 });
$q.flush();
expect(a.attr('class')).toMatch(/active/);

$state.transitionTo('contacts.item.edit', { id: 1 });
$q.flush();
expect(a.attr('class')).not.toMatch(/active/);
}));

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);
Expand Down

0 comments on commit eec4932

Please sign in to comment.