From 2f96fbd17577685bc013a4f7ced06664af253944 Mon Sep 17 00:00:00 2001 From: Oren Avissar Date: Thu, 4 Apr 2013 18:17:58 -0700 Subject: [PATCH] feat(ngIf): add directive to remove and recreate DOM elements This directive is adapted from ui-if in the AngularUI project and provides a complement to the ngShow/ngHide directives that only change the visibility of the DOM element and ngSwitch which does change the DOM but is more verbose. --- angularFiles.js | 1 + src/AngularPublic.js | 1 + src/ng/directive/ngIf.js | 83 +++++++++++++++ test/ng/directive/ngIfSpec.js | 191 ++++++++++++++++++++++++++++++++++ 4 files changed, 276 insertions(+) mode change 100644 => 100755 angularFiles.js mode change 100644 => 100755 src/AngularPublic.js create mode 100755 src/ng/directive/ngIf.js create mode 100755 test/ng/directive/ngIfSpec.js diff --git a/angularFiles.js b/angularFiles.js old mode 100644 new mode 100755 index 30c65df16a8b..2c2e4e0cce93 --- a/angularFiles.js +++ b/angularFiles.js @@ -49,6 +49,7 @@ angularFiles = { 'src/ng/directive/ngController.js', 'src/ng/directive/ngCsp.js', 'src/ng/directive/ngEventDirs.js', + 'src/ng/directive/ngIf.js', 'src/ng/directive/ngInclude.js', 'src/ng/directive/ngInit.js', 'src/ng/directive/ngNonBindable.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js old mode 100644 new mode 100755 index a66c35b335c5..1fd18ce268eb --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -82,6 +82,7 @@ function publishExternalAPI(angular){ ngController: ngControllerDirective, ngForm: ngFormDirective, ngHide: ngHideDirective, + ngIf: ngIfDirective, ngInclude: ngIncludeDirective, ngInit: ngInitDirective, ngNonBindable: ngNonBindableDirective, diff --git a/src/ng/directive/ngIf.js b/src/ng/directive/ngIf.js new file mode 100755 index 000000000000..f1ceccdae031 --- /dev/null +++ b/src/ng/directive/ngIf.js @@ -0,0 +1,83 @@ +'use strict'; + +/** + * @ngdoc directive + * @name ng.directive:ngIf + * @restrict A + * + * @description + * The `ngIf` directive removes and recreates a portion of the DOM tree (HTML) + * conditionally based on **"falsy"** and **"truthy"** values, respectively, evaluated within + * an {expression}. In other words, if the expression assigned to **ngIf evaluates to a false + * value** then **the element is removed from the DOM** and **if true** then **a clone of the + * element is reinserted into the DOM**. + * + * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the + * element in the DOM rather than changing its visibility via the `display` css property. A common + * case when this difference is significant is when using css selectors that rely on an element's + * position within the DOM (HTML), such as the `:first-child` or `:last-child` pseudo-classes. + * + * Note that **when an element is removed using ngIf its scope is destroyed** and **a new scope + * is created when the element is restored**. The scope created within `ngIf` inherits from + * its parent scope using + * {@link https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance prototypal inheritance}. + * An important implication of this is if `ngModel` is used within `ngIf` to bind to + * a javascript primitive defined in the parent scope. In this case any modifications made to the + * variable within the child scope will override (hide) the value in the parent scope. + * + * Also, `ngIf` recreates elements using their compiled state. An example scenario of this behavior + * is if an element's class attribute is directly modified after it's compiled, using something like + * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element + * the added class will be lost because the original compiled state is used to regenerate the element. + * + * Additionally, you can provide animations via the ngAnimate attribute to animate the **enter** + * and **leave** effects. + * + * @animations + * enter - happens just after the ngIf contents change and a new DOM element is created and injected into the ngIf container + * leave - happens just before the ngIf contents are removed from the DOM + * + * @element ANY + * @scope + * @param {expression} ngIf If the {@link guide/expression expression} is falsy then + * the element is removed from the DOM tree (HTML). + * + * @example + + + Click me:
+ Show when checked: I'm removed when the checkbox is unchecked +
+
+ */ +var ngIfDirective = ['$animator', function($animator) { + return { + transclude: 'element', + priority: 1000, + terminal: true, + restrict: 'A', + compile: function (element, attr, transclude) { + return function ($scope, $element, $attr) { + var animate = $animator($scope, $attr); + var childElement, childScope; + $scope.$watch($attr.ngIf, function ngIfWatchAction(value) { + if (childElement) { + animate.leave(childElement); + childElement = undefined; + } + if (childScope) { + childScope.$destroy(); + childScope = undefined; + } + if (toBoolean(value)) { + childScope = $scope.$new(); + transclude(childScope, function (clone) { + childElement = clone; + animate.enter(clone, $element.parent(), $element); + }); + } + }); + } + } + } +}]; diff --git a/test/ng/directive/ngIfSpec.js b/test/ng/directive/ngIfSpec.js new file mode 100755 index 000000000000..081ba5bf873a --- /dev/null +++ b/test/ng/directive/ngIfSpec.js @@ -0,0 +1,191 @@ +'use strict'; + +describe('ngIf', function () { + var $scope, $compile, element; + + beforeEach(inject(function ($rootScope, _$compile_) { + $scope = $rootScope.$new(); + $compile = _$compile_; + element = $compile('
')($scope); + })); + + afterEach(function () { + dealoc(element); + }); + + function makeIf(expr) { + element.append($compile('
Hi
')($scope)); + $scope.$apply(); + } + + it('should immediately remove element if condition is false', function () { + makeIf('false'); + expect(element.children().length).toBe(0); + }); + + it('should leave the element if condition is true', function () { + makeIf('true'); + expect(element.children().length).toBe(1); + }); + + it('should create then remove the element if condition changes', function () { + $scope.hello = true; + makeIf('hello'); + expect(element.children().length).toBe(1); + $scope.$apply('hello = false'); + expect(element.children().length).toBe(0); + }); + + it('should create a new scope', function () { + $scope.$apply('value = true'); + element.append($compile( + '
' + )($scope)); + $scope.$apply(); + expect(element.children('div').length).toBe(1); + }); + + it('should play nice with other elements beside it', function () { + $scope.values = [1, 2, 3, 4]; + element.append($compile( + '
' + + '
' + + '
' + )($scope)); + $scope.$apply(); + expect(element.children().length).toBe(9); + $scope.$apply('values.splice(0,1)'); + expect(element.children().length).toBe(6); + $scope.$apply('values.push(1)'); + expect(element.children().length).toBe(9); + }); + + it('should restore the element to its compiled state', function() { + $scope.value = true; + makeIf('value'); + expect(element.children().length).toBe(1); + jqLite(element.children()[0]).removeClass('my-class'); + expect(element.children()[0].className).not.toContain('my-class'); + $scope.$apply('value = false'); + expect(element.children().length).toBe(0); + $scope.$apply('value = true'); + expect(element.children().length).toBe(1); + expect(element.children()[0].className).toContain('my-class'); + }); + +}); + +describe('ngIf ngAnimate', function () { + var vendorPrefix, window; + var body, element; + + function html(html) { + body.html(html); + element = body.children().eq(0); + return element; + } + + beforeEach(function() { + // we need to run animation on attached elements; + body = jqLite(document.body); + }); + + afterEach(function(){ + dealoc(body); + dealoc(element); + }); + + beforeEach(module(function($animationProvider, $provide) { + $provide.value('$window', window = angular.mock.createMockWindow()); + return function($sniffer, $animator) { + vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $animator.enabled(true); + }; + })); + + it('should fire off the enter animation + add and remove the css classes', + inject(function($compile, $rootScope, $sniffer) { + var $scope = $rootScope.$new(); + var style = vendorPrefix + 'transition: 1s linear all'; + element = $compile(html( + '
' + + '
Hi
' + + '
' + ))($scope); + + $rootScope.$digest(); + $scope.$apply('value = true'); + + + expect(element.children().length).toBe(1); + var first = element.children()[0]; + + if ($sniffer.supportsTransitions) { + expect(first.className).toContain('custom-enter-setup'); + window.setTimeout.expect(1).process(); + expect(first.className).toContain('custom-enter-start'); + window.setTimeout.expect(1000).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + + expect(first.className).not.toContain('custom-enter-setup'); + expect(first.className).not.toContain('custom-enter-start'); + })); + + it('should fire off the leave animation + add and remove the css classes', + inject(function ($compile, $rootScope, $sniffer) { + var $scope = $rootScope.$new(); + var style = vendorPrefix + 'transition: 1s linear all'; + element = $compile(html( + '
' + + '
Hi
' + + '
' + ))($scope); + $scope.$apply('value = true'); + + expect(element.children().length).toBe(1); + var first = element.children()[0]; + + if ($sniffer.supportsTransitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(1000).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + + $scope.$apply('value = false'); + expect(element.children().length).toBe($sniffer.supportsTransitions ? 1 : 0); + + if ($sniffer.supportsTransitions) { + expect(first.className).toContain('custom-leave-setup'); + window.setTimeout.expect(1).process(); + expect(first.className).toContain('custom-leave-start'); + window.setTimeout.expect(1000).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + + expect(element.children().length).toBe(0); + })); + + it('should catch and use the correct duration for animation', + inject(function ($compile, $rootScope, $sniffer) { + var $scope = $rootScope.$new(); + var style = vendorPrefix + 'transition: 0.5s linear all'; + element = $compile(html( + '
' + + '
Hi
' + + '
' + ))($scope); + $scope.$apply('value = true'); + + if ($sniffer.supportsTransitions) { + window.setTimeout.expect(1).process(); + window.setTimeout.expect(500).process(); + } else { + expect(window.setTimeout.queue).toEqual([]); + } + })); + +});