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-eq` 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-eq, thought typically we think devs want the auto inheritance.

Fixes #818
  • Loading branch information
timkindberg committed Mar 10, 2014
1 parent e3ba1bf commit bf163ad
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 23 deletions.
77 changes: 57 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', '?^uiSrefActiveEq'],
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,20 @@ 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
* ui-sref-active found at the same level or above the ui-sref will be used.
*
* Will activate when the ui-sref's target state or any child state is active. If you
* need to activate only when the ui-sref target state is active and *not* any of
* it's children, then you will use
* {@link ui.router.state.directive:ui-sref-active-eq ui-sref-active-eq}
*
* @example
* Given the following template:
* <pre>
Expand All @@ -163,8 +172,9 @@ 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" (or any children states), 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 +183,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 +196,36 @@ 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-eq
*
* @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 will only activate
* when the exact target state used in the `ui-sref` is active; no child states.
*
*/
$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 uiSrefActiveEq share the same directive object with some
// slight difference in logic routing
activeClass = $interpolate($attrs.uiSrefActiveEq || $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 +235,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.uiSrefActiveEq !== '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 +259,5 @@ function $StateActiveDirective($state, $stateParams, $interpolate) {

angular.module('ui.router.state')
.directive('uiSref', $StateRefDirective)
.directive('uiSrefActive', $StateActiveDirective);
.directive('uiSrefActive', $StateRefActiveDirective)
.directive('uiSrefActiveEq', $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-eq="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

16 comments on commit bf163ad

@bpampuch
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change didn't get into released version (0.2.10)

@nateabele
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...Because it didn't exist yet?

@bpampuch
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, looks like a good reason :) any plans for 0.2.11?

@nateabele
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The next version will probably be 0.3, released in the next 2-3 weeks.

@timkindberg
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhm it's in the changelog though for 0.2.10.

@nateabele
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timkindberg If you used the grunt command to generate the changelog, then the command needs to be fixed, because it's not reading the correct commit range.

@timkindberg
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I did. I'll take a look

@keithmancuso
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any updates on when this change will be released?

@swissmanu
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@Joosakur
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@Rickinio
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@mortware
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@PazkaL
Copy link

@PazkaL PazkaL commented on bf163ad Jul 16, 2014

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@DOrlov77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you shouldn't limit by using the first only "nearest" uiSrefActive directive but all the parents. It will be helpful to mark as "active" not the selected menu only but the root menu(s) as well. Also - why not to set "active" as default class name? Or/and make a provider to allow user to define the default active class-name.

@timkindberg
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been released!!

@DOrlov77
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timkindberg
I'm not so sure you got me:

  1. It would be helpful to mark the menu item as "active" as well, but this will not work:
el = angular.element('<li dropdown ui-sref-active="active"><a dropdown-toggle>Menu</a><ul class="dropdown-menu"><li><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a></li></ul></li>');
  1. The active class is "" by default:
    Line 3583: activeClass = $interpolate($attrs.uiSrefActiveEq || $attrs.uiSrefActive || '', false)($scope);
    But why not simplify:
el = angular.element('<li dropdown ui-sref-active><a dropdown-toggle>Menu</a><ul class="dropdown-menu"><li><a ui-sref="contacts.item({ id: 1 })" ui-sref-active>Contacts</a></li></ul></li>');

?

Please sign in to comment.