Skip to content

Commit

Permalink
feat(uiSrefActive): provide a ng-{class,style} like interface
Browse files Browse the repository at this point in the history
resolves #1431
  • Loading branch information
fpipita committed Nov 14, 2015
1 parent db79d76 commit a9ff6fe
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 11 deletions.
3 changes: 2 additions & 1 deletion src/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ var isDefined = angular.isDefined,
isArray = angular.isArray,
forEach = angular.forEach,
extend = angular.extend,
copy = angular.copy;
copy = angular.copy,
toJson = angular.toJson;

function inherit(parent, extra) {
return extend(new (extend(function() {}, { prototype: parent }))(), extra);
Expand Down
78 changes: 68 additions & 10 deletions src/stateDirectives.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,24 @@ function $StateRefDynamicDirective($state, $timeout) {
* </li>
* </ul>
* </pre>
*
* It is also possible to pass ui-sref-active an expression that evaluates
* to an object hash, whose keys represent active class names and whose
* values represent the respective state names/globs.
* ui-sref-active will match if the current active state **includes** any of
* the specified state names/globs, even the abstract ones.
*
* @Example
* Given the following template, with "admin" being an abstract state:
* <pre>
* <div ui-sref-active="{'active': 'admin.*'}">
* <a ui-sref-active="active" ui-sref="admin.roles">Roles</a>
* </div>
* </pre>
*
* When the current state is "admin.roles" the "active" class will be applied
* to both the <div> and <a> elements. It is important to note that the state
* names/globs passed to ui-sref-active shadow the state provided by ui-sref.
*/

/**
Expand All @@ -271,35 +289,75 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
return {
restrict: "A",
controller: ['$scope', '$element', '$attrs', '$timeout', function ($scope, $element, $attrs, $timeout) {
var states = [], activeClass, activeEqClass;
var states = [], activeClasses = {}, activeEqClass;

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

var uiSrefActive = $scope.$eval($attrs.uiSrefActive) || $interpolate($attrs.uiSrefActive || '', false)($scope);
if (isObject(uiSrefActive)) {
forEach(uiSrefActive, function(stateOrName, activeClass) {
if (isString(stateOrName)) {
var ref = parseStateRef(stateOrName, $state.current.name);
addState(ref.state, $scope.$eval(ref.paramExpr), activeClass);
}
});
}

// Allow uiSref to communicate with uiSrefActive[Equals]
this.$$addStateInfo = function (newState, newParams) {
var state = $state.get(newState, stateContext($element));
// we already got an explicit state provided by ui-sref-active, so we
// shadow the one that comes from ui-sref
if (isObject(uiSrefActive) && states.length > 0) {
return;
}
addState(newState, newParams, uiSrefActive);
update();
};

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

function addState(stateName, stateParams, activeClass) {
var state = $state.get(stateName, stateContext($element));
var stateHash = createStateHash(stateName, stateParams);

states.push({
state: state || { name: newState },
params: newParams
state: state || { name: stateName },
params: stateParams,
hash: stateHash
});

update();
};
activeClasses[stateHash] = activeClass;
}

$scope.$on('$stateChangeSuccess', update);
/**
* @param {string} state
* @param {Object|string} [params]
* @return {string}
*/
function createStateHash(state, params) {
if (!isString(state)) {
throw new Error('state should be a string');
}
if (isObject(params)) {
return state + toJson(params);
}
params = $scope.$eval(params);
if (isObject(params)) {
return state + toJson(params);
}
return state;
}

// Update route state
function update() {
for (var i = 0; i < states.length; i++) {
if (anyMatch(states[i].state, states[i].params)) {
addClass($element, activeClass);
addClass($element, activeClasses[states[i].hash]);
} else {
removeClass($element, activeClass);
removeClass($element, activeClasses[states[i].hash]);
}

if (exactMatch(states[i].state, states[i].params)) {
Expand Down
53 changes: 53 additions & 0 deletions test/stateDirectivesSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,12 @@ describe('uiSrefActive', function() {
url: '/detail/:foo'
}).state('contacts.item.edit', {
url: '/edit'
}).state('admin', {
url: '/admin',
abstract: true,
template: '<ui-view/>'
}).state('admin.roles', {
url: '/roles?page'
});
}));

Expand Down Expand Up @@ -628,6 +634,53 @@ describe('uiSrefActive', function() {
timeoutFlush();
expect(angular.element(template[0].querySelector('a')).attr('class')).toBe('active');
}));

describe('ng-{class,style} interface', function() {
it('should match on abstract states that are included by the current state', inject(function($rootScope, $compile, $state, $q) {
el = $compile('<div ui-sref-active="{active: \'admin.*\'}"><a ui-sref-active="active" ui-sref="admin.roles">Roles</a></div>')($rootScope);
$state.transitionTo('admin.roles');
$q.flush();
timeoutFlush();
var abstractParent = el[0];
expect(abstractParent.className).toMatch(/active/);
var child = el[0].querySelector('a');
expect(child.className).toMatch(/active/);
}));

it('should match on state parameters', inject(function($compile, $rootScope, $state, $q) {
el = $compile('<div ui-sref-active="{active: \'admin.roles({page: 1})\'}"></div>')($rootScope);
$state.transitionTo('admin.roles', {page: 1});
$q.flush();
timeoutFlush();
expect(el[0].className).toMatch(/active/);
}));

it('should shadow the state provided by ui-sref', inject(function($compile, $rootScope, $state, $q) {
el = $compile('<div ui-sref-active="{active: \'admin.roles({page: 1})\'}"><a ui-sref="admin.roles"></a></div>')($rootScope);
$state.transitionTo('admin.roles');
$q.flush();
timeoutFlush();
expect(el[0].className).not.toMatch(/active/);
$state.transitionTo('admin.roles', {page: 1});
$q.flush();
timeoutFlush();
expect(el[0].className).toMatch(/active/);
}));

it('should support multiple <className, stateOrName> pairs', inject(function($compile, $rootScope, $state, $q) {
el = $compile('<div ui-sref-active="{contacts: \'contacts.*\', admin: \'admin.roles({page: 1})\'}"></div>')($rootScope);
$state.transitionTo('contacts');
$q.flush();
timeoutFlush();
expect(el[0].className).toMatch(/contacts/);
expect(el[0].className).not.toMatch(/admin/);
$state.transitionTo('admin.roles', {page: 1});
$q.flush();
timeoutFlush();
expect(el[0].className).toMatch(/admin/);
expect(el[0].className).not.toMatch(/contacts/);
}));
});
});

describe('uiView controllers or onEnter handlers', function() {
Expand Down

0 comments on commit a9ff6fe

Please sign in to comment.