Skip to content

Commit

Permalink
feat($compile): support compile animation hooks classes
Browse files Browse the repository at this point in the history
  • Loading branch information
matsko authored and mhevery committed Aug 3, 2013
1 parent d45ac77 commit f2dfa89
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 50 deletions.
138 changes: 100 additions & 38 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,9 +274,9 @@ function $CompileProvider($provide) {

this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils',
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils', '$animate',
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
$controller, $rootScope, $document, $sce, $$urlUtils) {
$controller, $rootScope, $document, $sce, $$urlUtils, $animate) {

var Attributes = function(element, attr) {
this.$$element = element;
Expand All @@ -287,6 +287,42 @@ function $CompileProvider($provide) {
$normalize: directiveNormalize,


/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$addClass
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Adds the CSS class value specified by the classVal parameter to the element. If animations
* are enabled then an animation will be triggered for the class addition.
*
* @param {string} classVal The className value that will be added to the element
*/
$addClass : function(classVal) {
if(classVal && classVal.length > 0) {
$animate.addClass(this.$$element, classVal);
}
},

/**
* @ngdoc function
* @name ng.$compile.directive.Attributes#$removeClass
* @methodOf ng.$compile.directive.Attributes
* @function
*
* @description
* Removes the CSS class value specified by the classVal parameter from the element. If animations
* are enabled then an animation will be triggered for the class removal.
*
* @param {string} classVal The className value that will be removed from the element
*/
$removeClass : function(classVal) {
if(classVal && classVal.length > 0) {
$animate.removeClass(this.$$element, classVal);
}
},

/**
* Set a normalized attribute on the element in a way such that all directives
* can share the attribute. This function properly handles boolean attributes.
Expand All @@ -297,61 +333,87 @@ function $CompileProvider($provide) {
* @param {string=} attrName Optional none normalized name. Defaults to key.
*/
$set: function(key, value, writeAttr, attrName) {
var booleanKey = getBooleanAttrName(this.$$element[0], key),
$$observers = this.$$observers,
normalizedVal,
nodeName;

if (booleanKey) {
this.$$element.prop(key, value);
attrName = booleanKey;
}
//special case for class attribute addition + removal
//so that class changes can tap into the animation
//hooks provided by the $animate service
if(key == 'class') {
value = value || '';
var current = this.$$element.attr('class') || '';
this.$removeClass(tokenDifference(current, value).join(' '));
this.$addClass(tokenDifference(value, current).join(' '));
} else {
var booleanKey = getBooleanAttrName(this.$$element[0], key),
normalizedVal,
nodeName;

this[key] = value;
if (booleanKey) {
this.$$element.prop(key, value);
attrName = booleanKey;
}

// translate normalized key to actual key
if (attrName) {
this.$attr[key] = attrName;
} else {
attrName = this.$attr[key];
if (!attrName) {
this.$attr[key] = attrName = snake_case(key, '-');
this[key] = value;

// translate normalized key to actual key
if (attrName) {
this.$attr[key] = attrName;
} else {
attrName = this.$attr[key];
if (!attrName) {
this.$attr[key] = attrName = snake_case(key, '-');
}
}
}

nodeName = nodeName_(this.$$element);

// sanitize a[href] and img[src] values
if ((nodeName === 'A' && key === 'href') ||
(nodeName === 'IMG' && key === 'src')) {
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
if (!msie || msie >= 8 ) {
normalizedVal = $$urlUtils.resolve(value);
if (normalizedVal !== '') {
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
this[key] = value = 'unsafe:' + normalizedVal;
nodeName = nodeName_(this.$$element);

// sanitize a[href] and img[src] values
if ((nodeName === 'A' && key === 'href') ||
(nodeName === 'IMG' && key === 'src')) {
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
if (!msie || msie >= 8 ) {
normalizedVal = $$urlUtils.resolve(value);
if (normalizedVal !== '') {
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
this[key] = value = 'unsafe:' + normalizedVal;
}
}
}
}
}

if (writeAttr !== false) {
if (value === null || value === undefined) {
this.$$element.removeAttr(attrName);
} else {
this.$$element.attr(attrName, value);
if (writeAttr !== false) {
if (value === null || value === undefined) {
this.$$element.removeAttr(attrName);
} else {
this.$$element.attr(attrName, value);
}
}
}

// fire observers
var $$observers = this.$$observers;
$$observers && forEach($$observers[key], function(fn) {
try {
fn(value);
} catch (e) {
$exceptionHandler(e);
}
});

function tokenDifference(str1, str2) {
var values = [],
tokens1 = str1.split(/\s+/),
tokens2 = str2.split(/\s+/);

outer:
for(var i=0;i<tokens1.length;i++) {
var token = tokens1[i];
for(var j=0;j<tokens2.length;j++) {
if(token == tokens2[j]) continue outer;
}
values.push(token);
}
return values;
};
},


Expand Down
17 changes: 5 additions & 12 deletions src/ng/directive/ngClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

function classDirective(name, selector) {
name = 'ngClass' + name;
return ['$animate', function($animate) {
return function() {
return {
restrict: 'AC',
link: function(scope, element, attr) {
Expand All @@ -11,8 +11,7 @@ function classDirective(name, selector) {
scope.$watch(attr[name], ngClassWatchAction, true);

attr.$observe('class', function(value) {
var ngClass = scope.$eval(attr[name]);
ngClassWatchAction(ngClass, ngClass);
ngClassWatchAction(scope.$eval(attr[name]));
});


Expand Down Expand Up @@ -42,18 +41,12 @@ function classDirective(name, selector) {


function removeClass(classVal) {
classVal = flattenClasses(classVal);
if(classVal && classVal.length > 0) {
$animate.removeClass(element, classVal);
}
attr.$removeClass(flattenClasses(classVal));
}


function addClass(classVal) {
classVal = flattenClasses(classVal);
if(classVal && classVal.length > 0) {
$animate.addClass(element, classVal);
}
attr.$addClass(flattenClasses(classVal));
}

function flattenClasses(classVal) {
Expand All @@ -73,7 +66,7 @@ function classDirective(name, selector) {
};
}
};
}];
};
}

/**
Expand Down
51 changes: 51 additions & 0 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3268,4 +3268,55 @@ describe('$compile', function() {
expect(spans.eq(3)).toBeHidden();
}));
});

describe('$animate animation hooks', function() {

beforeEach(module('mock.animate'));

it('should automatically fire the addClass and removeClass animation hooks',
inject(function($compile, $animate, $rootScope) {

var data, element = jqLite('<div class="{{val1}} {{val2}} fire"></div>');
$compile(element)($rootScope);

$rootScope.$digest();
data = $animate.flushNext('removeClass');

expect(element.hasClass('fire')).toBe(true);

$rootScope.val1 = 'ice';
$rootScope.val2 = 'rice';
$rootScope.$digest();

data = $animate.flushNext('addClass');
expect(data.params[1]).toBe('ice rice');

expect(element.hasClass('ice')).toBe(true);
expect(element.hasClass('rice')).toBe(true);
expect(element.hasClass('fire')).toBe(true);

$rootScope.val2 = 'dice';
$rootScope.$digest();

data = $animate.flushNext('removeClass');
expect(data.params[1]).toBe('rice');
data = $animate.flushNext('addClass');
expect(data.params[1]).toBe('dice');

expect(element.hasClass('ice')).toBe(true);
expect(element.hasClass('dice')).toBe(true);
expect(element.hasClass('fire')).toBe(true);

$rootScope.val1 = '';
$rootScope.val2 = '';
$rootScope.$digest();

data = $animate.flushNext('removeClass');
expect(data.params[1]).toBe('ice dice');

expect(element.hasClass('ice')).toBe(false);
expect(element.hasClass('dice')).toBe(false);
expect(element.hasClass('fire')).toBe(true);
}));
});
});

0 comments on commit f2dfa89

Please sign in to comment.