Skip to content

Commit

Permalink
Merge pull request #2375 from fpipita/feature-1.0
Browse files Browse the repository at this point in the history
feat(uiSrefActive): provide a ng-{class,style} like interface
  • Loading branch information
christopherthielen committed Nov 25, 2015
2 parents 141d7b0 + eeec65b commit 4398690
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 10 deletions.
75 changes: 65 additions & 10 deletions src/state/stateDirectives.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// <reference path='../../typings/angularjs/angular.d.ts' />
import {copy, defaults} from "../common/common";
import {copy, defaults, isString, isObject, forEach, toJson} from "../common/common";
import {defaultTransOpts} from "../transition/transitionService";

function parseStateRef(ref, current) {
Expand Down Expand Up @@ -199,6 +199,24 @@ function $StateRefDirective($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 @@ -221,37 +239,74 @@ function $StateRefActiveDirective($state, $stateParams, $interpolate) {
return {
restrict: "A",
controller: ['$scope', '$element', '$attrs', '$timeout', '$transitions', function ($scope, $element, $attrs, $timeout, $transitions) {
let states = [], activeClass, activeEqClass;
let 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) {
let 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;
}

let updateAfterTransition = function ($transition$) { $transition$.promise.then(update); };
let deregisterFn = $transitions.onStart({}, updateAfterTransition);
$scope.$on('$destroy', deregisterFn);

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 (let 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 @@ -424,6 +424,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 @@ -609,4 +615,51 @@ 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/);
}));
});
});

0 comments on commit 4398690

Please sign in to comment.