Skip to content
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

feat(uiSrefActive): nested state and DOM decendant support for ui-sref-active #821

Closed
wants to merge 1 commit into from
Closed
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
89 changes: 56 additions & 33 deletions src/stateDirectives.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function stateContext(el) {
* @name ui.router.state.directive:ui-sref
*
* @requires ui.router.state.$state
* @requires ui.router.state.$stateParams
* @requires $timeout
*
* @restrict A
Expand Down Expand Up @@ -48,14 +49,13 @@ function stateContext(el) {
*
* @param {string} ui-sref 'stateName' can be any valid absolute or relative state
*/
$StateRefDirective.$inject = ['$state', '$timeout'];
function $StateRefDirective($state, $timeout) {
$StateRefDirective.$inject = ['$state', '$timeout', '$stateParams'];
function $StateRefDirective($state, $timeout, $stateParams) {
return {
restrict: 'A',
require: '?^uiSrefActive',
link: function(scope, element, attrs, uiSrefActive) {
link: function(scope, element, attrs) {
var ref = parseStateRef(attrs.uiSref);
var params = null, url = null, base = stateContext(element) || $state.$current;
var state = null, params = null, url = null, base = stateContext(element) || $state.$current;
var isForm = element[0].nodeName === "FORM";
var attr = isForm ? "action" : "href", nav = true;

Expand All @@ -64,10 +64,8 @@ function $StateRefDirective($state, $timeout) {
if (!nav) return;

var newHref = $state.href(ref.state, params, { relative: base });
state = $state.get(ref.state, base);

if (uiSrefActive) {
uiSrefActive.$$setStateInfo(ref.state, params);
}
if (!newHref) {
nav = false;
return false;
Expand Down Expand Up @@ -95,6 +93,37 @@ function $StateRefDirective($state, $timeout) {
e.preventDefault();
}
});

var emitEvents = function(){
// HACK:
// Emits events only after
// 1. The execution of link functions of ancestor's ui-sref-active
// or,
// 2. The ancestor ui-sref-active has removed their previously appended classes.
//
$timeout(function(){
if($state.$current.self === state && matchesParams()){
// Exact match of current state
scope.$emit('$uiSrefActivated');
}else if($state.includes(state.name) && matchesParams()){
// The current state is a child of reference state
scope.$emit('$uiSrefChildStateActivated');
}
});
};

// Emits $uiSref*Activated events.
scope.$on('$stateChangeSuccess', emitEvents);

// Also emits the events when the element is first created (linked).
// This makes sure the events are emitted if a state is directly navigated
// through the browser navigation bar.
//
emitEvents();

function matchesParams() {
return !params || equalForKeys(params, $stateParams);
}
}
};
}
Expand All @@ -104,7 +133,6 @@ function $StateRefDirective($state, $timeout) {
* @name ui.router.state.directive:ui-sref-active
*
* @requires ui.router.state.$state
* @requires ui.router.state.$stateParams
* @requires $interpolate
*
* @restrict A
Expand All @@ -126,38 +154,33 @@ function $StateRefDirective($state, $timeout) {
* </ul>
* </pre>
*/
$StateActiveDirective.$inject = ['$state', '$stateParams', '$interpolate'];
function $StateActiveDirective($state, $stateParams, $interpolate) {
$StateActiveDirective.$inject = ['$state', '$interpolate'];
function $StateActiveDirective($state, $interpolate) {
return {
restrict: "A",
controller: ['$scope', '$element', '$attrs', function($scope, $element, $attrs) {
var state, params, activeClass;
scope: true, // Catching $uiSref*Activated events without sibling's interferance.
link: function(scope, element, attrs) {
var activeClass, activeClassNested, activeClassList;

// There probably isn't much point in $observing this
activeClass = $interpolate($attrs.uiSrefActive || '', false)($scope);
activeClass = $interpolate(attrs.uiSrefActive || '', false)(scope);
activeClassNested = activeClass + '-nested';
activeClassList = [activeClass, activeClassNested].join(' '); // space-separated list of all appended classes

// Allow uiSref to communicate with uiSrefActive
this.$$setStateInfo = function(newState, newParams) {
state = $state.get(newState, stateContext($element));
params = newParams;
update();
};
// Remove all previously appended classes.
scope.$on('$stateChangeSuccess', function(){
element.removeClass(activeClassList);
});

$scope.$on('$stateChangeSuccess', update);
scope.$on('$uiSrefActivated', function(){
element.addClass(activeClass);
});

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

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

Expand Down
87 changes: 78 additions & 9 deletions test/stateDirectivesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,58 +278,127 @@ describe('uiSrefActive', function() {
url: '/:id',
}).state('contacts.item.detail', {
url: '/detail/:foo'
}).state('contacts.item.edit', {
url: '/edit'
});
}));

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

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

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

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

$state.transitionTo('contacts.item', { id: 5 });
$timeout.flush(); // emitEvent timeout hacks
$q.flush();

expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('ng-scope active-nested');
}));

it('should update class for decendant uiSrefs', inject(function($rootScope, $q, $compile, $state, $timeout) {
el = angular.element('<section><div ui-sref-active="active"><p ng-init="newScope=1"><a ui-sref="contacts">Contacts</a></p></div></section>');
template = $compile(el)($rootScope);
$rootScope.$digest();
$timeout.flush(); // emitEvent timeout hacks

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

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

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

expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope active-nested');
}));

it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state) {
it('should not update class for sibling elements with uiSrefs', inject(function($rootScope, $q, $compile, $state, $timeout) {
el = angular.element('<section><div ui-sref-active="active"></div><a ui-sref="contacts">Contacts</a></section>');
template = $compile(el)($rootScope);
$rootScope.$digest();
$timeout.flush(); // emitEvent timeout hacks

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

expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope');

$state.transitionTo('contacts.item', { id: 5 });
$timeout.flush(); // emitEvent timeout hacks
$q.flush();

expect(angular.element(template[0].querySelector('div')).attr('class')).toBe('ng-scope');
}));

it('should match state\'s parameters', inject(function($rootScope, $q, $compile, $state, $timeout) {
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();
$timeout.flush(); // emitEvent timeout hacks

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

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

it('should match child states', inject(function($rootScope, $q, $compile, $state, $timeout) {
el = angular.element('<div><a ui-sref="contacts.item({ id: 1 })" ui-sref-active="active">Contacts</a></div>');
template = $compile(el)($rootScope);
$rootScope.$digest();
$timeout.flush(); // emitEvent timeout hacks

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

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

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

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

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

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