Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit f2dfa89

Browse files
matskomhevery
authored andcommitted
feat($compile): support compile animation hooks classes
1 parent d45ac77 commit f2dfa89

File tree

3 files changed

+156
-50
lines changed

3 files changed

+156
-50
lines changed

src/ng/compile.js

+100-38
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ function $CompileProvider($provide) {
274274

275275
this.$get = [
276276
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
277-
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils',
277+
'$controller', '$rootScope', '$document', '$sce', '$$urlUtils', '$animate',
278278
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
279-
$controller, $rootScope, $document, $sce, $$urlUtils) {
279+
$controller, $rootScope, $document, $sce, $$urlUtils, $animate) {
280280

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

289289

290+
/**
291+
* @ngdoc function
292+
* @name ng.$compile.directive.Attributes#$addClass
293+
* @methodOf ng.$compile.directive.Attributes
294+
* @function
295+
*
296+
* @description
297+
* Adds the CSS class value specified by the classVal parameter to the element. If animations
298+
* are enabled then an animation will be triggered for the class addition.
299+
*
300+
* @param {string} classVal The className value that will be added to the element
301+
*/
302+
$addClass : function(classVal) {
303+
if(classVal && classVal.length > 0) {
304+
$animate.addClass(this.$$element, classVal);
305+
}
306+
},
307+
308+
/**
309+
* @ngdoc function
310+
* @name ng.$compile.directive.Attributes#$removeClass
311+
* @methodOf ng.$compile.directive.Attributes
312+
* @function
313+
*
314+
* @description
315+
* Removes the CSS class value specified by the classVal parameter from the element. If animations
316+
* are enabled then an animation will be triggered for the class removal.
317+
*
318+
* @param {string} classVal The className value that will be removed from the element
319+
*/
320+
$removeClass : function(classVal) {
321+
if(classVal && classVal.length > 0) {
322+
$animate.removeClass(this.$$element, classVal);
323+
}
324+
},
325+
290326
/**
291327
* Set a normalized attribute on the element in a way such that all directives
292328
* can share the attribute. This function properly handles boolean attributes.
@@ -297,61 +333,87 @@ function $CompileProvider($provide) {
297333
* @param {string=} attrName Optional none normalized name. Defaults to key.
298334
*/
299335
$set: function(key, value, writeAttr, attrName) {
300-
var booleanKey = getBooleanAttrName(this.$$element[0], key),
301-
$$observers = this.$$observers,
302-
normalizedVal,
303-
nodeName;
304-
305-
if (booleanKey) {
306-
this.$$element.prop(key, value);
307-
attrName = booleanKey;
308-
}
336+
//special case for class attribute addition + removal
337+
//so that class changes can tap into the animation
338+
//hooks provided by the $animate service
339+
if(key == 'class') {
340+
value = value || '';
341+
var current = this.$$element.attr('class') || '';
342+
this.$removeClass(tokenDifference(current, value).join(' '));
343+
this.$addClass(tokenDifference(value, current).join(' '));
344+
} else {
345+
var booleanKey = getBooleanAttrName(this.$$element[0], key),
346+
normalizedVal,
347+
nodeName;
309348

310-
this[key] = value;
349+
if (booleanKey) {
350+
this.$$element.prop(key, value);
351+
attrName = booleanKey;
352+
}
311353

312-
// translate normalized key to actual key
313-
if (attrName) {
314-
this.$attr[key] = attrName;
315-
} else {
316-
attrName = this.$attr[key];
317-
if (!attrName) {
318-
this.$attr[key] = attrName = snake_case(key, '-');
354+
this[key] = value;
355+
356+
// translate normalized key to actual key
357+
if (attrName) {
358+
this.$attr[key] = attrName;
359+
} else {
360+
attrName = this.$attr[key];
361+
if (!attrName) {
362+
this.$attr[key] = attrName = snake_case(key, '-');
363+
}
319364
}
320-
}
321365

322-
nodeName = nodeName_(this.$$element);
323-
324-
// sanitize a[href] and img[src] values
325-
if ((nodeName === 'A' && key === 'href') ||
326-
(nodeName === 'IMG' && key === 'src')) {
327-
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
328-
if (!msie || msie >= 8 ) {
329-
normalizedVal = $$urlUtils.resolve(value);
330-
if (normalizedVal !== '') {
331-
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
332-
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
333-
this[key] = value = 'unsafe:' + normalizedVal;
366+
nodeName = nodeName_(this.$$element);
367+
368+
// sanitize a[href] and img[src] values
369+
if ((nodeName === 'A' && key === 'href') ||
370+
(nodeName === 'IMG' && key === 'src')) {
371+
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
372+
if (!msie || msie >= 8 ) {
373+
normalizedVal = $$urlUtils.resolve(value);
374+
if (normalizedVal !== '') {
375+
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
376+
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
377+
this[key] = value = 'unsafe:' + normalizedVal;
378+
}
334379
}
335380
}
336381
}
337-
}
338382

339-
if (writeAttr !== false) {
340-
if (value === null || value === undefined) {
341-
this.$$element.removeAttr(attrName);
342-
} else {
343-
this.$$element.attr(attrName, value);
383+
if (writeAttr !== false) {
384+
if (value === null || value === undefined) {
385+
this.$$element.removeAttr(attrName);
386+
} else {
387+
this.$$element.attr(attrName, value);
388+
}
344389
}
345390
}
346391

347392
// fire observers
393+
var $$observers = this.$$observers;
348394
$$observers && forEach($$observers[key], function(fn) {
349395
try {
350396
fn(value);
351397
} catch (e) {
352398
$exceptionHandler(e);
353399
}
354400
});
401+
402+
function tokenDifference(str1, str2) {
403+
var values = [],
404+
tokens1 = str1.split(/\s+/),
405+
tokens2 = str2.split(/\s+/);
406+
407+
outer:
408+
for(var i=0;i<tokens1.length;i++) {
409+
var token = tokens1[i];
410+
for(var j=0;j<tokens2.length;j++) {
411+
if(token == tokens2[j]) continue outer;
412+
}
413+
values.push(token);
414+
}
415+
return values;
416+
};
355417
},
356418

357419

src/ng/directive/ngClass.js

+5-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
function classDirective(name, selector) {
44
name = 'ngClass' + name;
5-
return ['$animate', function($animate) {
5+
return function() {
66
return {
77
restrict: 'AC',
88
link: function(scope, element, attr) {
@@ -11,8 +11,7 @@ function classDirective(name, selector) {
1111
scope.$watch(attr[name], ngClassWatchAction, true);
1212

1313
attr.$observe('class', function(value) {
14-
var ngClass = scope.$eval(attr[name]);
15-
ngClassWatchAction(ngClass, ngClass);
14+
ngClassWatchAction(scope.$eval(attr[name]));
1615
});
1716

1817

@@ -42,18 +41,12 @@ function classDirective(name, selector) {
4241

4342

4443
function removeClass(classVal) {
45-
classVal = flattenClasses(classVal);
46-
if(classVal && classVal.length > 0) {
47-
$animate.removeClass(element, classVal);
48-
}
44+
attr.$removeClass(flattenClasses(classVal));
4945
}
5046

5147

5248
function addClass(classVal) {
53-
classVal = flattenClasses(classVal);
54-
if(classVal && classVal.length > 0) {
55-
$animate.addClass(element, classVal);
56-
}
49+
attr.$addClass(flattenClasses(classVal));
5750
}
5851

5952
function flattenClasses(classVal) {
@@ -73,7 +66,7 @@ function classDirective(name, selector) {
7366
};
7467
}
7568
};
76-
}];
69+
};
7770
}
7871

7972
/**

test/ng/compileSpec.js

+51
Original file line numberDiff line numberDiff line change
@@ -3268,4 +3268,55 @@ describe('$compile', function() {
32683268
expect(spans.eq(3)).toBeHidden();
32693269
}));
32703270
});
3271+
3272+
describe('$animate animation hooks', function() {
3273+
3274+
beforeEach(module('mock.animate'));
3275+
3276+
it('should automatically fire the addClass and removeClass animation hooks',
3277+
inject(function($compile, $animate, $rootScope) {
3278+
3279+
var data, element = jqLite('<div class="{{val1}} {{val2}} fire"></div>');
3280+
$compile(element)($rootScope);
3281+
3282+
$rootScope.$digest();
3283+
data = $animate.flushNext('removeClass');
3284+
3285+
expect(element.hasClass('fire')).toBe(true);
3286+
3287+
$rootScope.val1 = 'ice';
3288+
$rootScope.val2 = 'rice';
3289+
$rootScope.$digest();
3290+
3291+
data = $animate.flushNext('addClass');
3292+
expect(data.params[1]).toBe('ice rice');
3293+
3294+
expect(element.hasClass('ice')).toBe(true);
3295+
expect(element.hasClass('rice')).toBe(true);
3296+
expect(element.hasClass('fire')).toBe(true);
3297+
3298+
$rootScope.val2 = 'dice';
3299+
$rootScope.$digest();
3300+
3301+
data = $animate.flushNext('removeClass');
3302+
expect(data.params[1]).toBe('rice');
3303+
data = $animate.flushNext('addClass');
3304+
expect(data.params[1]).toBe('dice');
3305+
3306+
expect(element.hasClass('ice')).toBe(true);
3307+
expect(element.hasClass('dice')).toBe(true);
3308+
expect(element.hasClass('fire')).toBe(true);
3309+
3310+
$rootScope.val1 = '';
3311+
$rootScope.val2 = '';
3312+
$rootScope.$digest();
3313+
3314+
data = $animate.flushNext('removeClass');
3315+
expect(data.params[1]).toBe('ice dice');
3316+
3317+
expect(element.hasClass('ice')).toBe(false);
3318+
expect(element.hasClass('dice')).toBe(false);
3319+
expect(element.hasClass('fire')).toBe(true);
3320+
}));
3321+
});
32713322
});

0 commit comments

Comments
 (0)