Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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.
  • Loading branch information...
commit 2f96fbd17577685bc013a4f7ced06664af253944 1 parent 8a2bfd7
@OrenAvissar OrenAvissar authored petebacondarwin committed
View
1  angularFiles.js 100644 → 100755
@@ -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',
View
1  src/AngularPublic.js 100644 → 100755
@@ -82,6 +82,7 @@ function publishExternalAPI(angular){
ngController: ngControllerDirective,
ngForm: ngFormDirective,
ngHide: ngHideDirective,
+ ngIf: ngIfDirective,
ngInclude: ngIncludeDirective,
ngInit: ngInitDirective,
ngNonBindable: ngNonBindableDirective,
View
83 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
+ <doc:example>
+ <doc:source>
+ Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/>
+ Show when checked: <span ng-if="checked">I'm removed when the checkbox is unchecked</span>
+ </doc:source>
+ </doc:example>
+ */
+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);
+ });
+ }
+ });
+ }
+ }
+ }
+}];
View
191 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('<div></div>')($scope);
+ }));
+
+ afterEach(function () {
+ dealoc(element);
+ });
+
+ function makeIf(expr) {
+ element.append($compile('<div class="my-class" ng-if="' + expr + '"><div>Hi</div></div>')($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(
+ '<div ng-if="value"><span ng-init="value=false"></span></div>'
+ )($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(
+ '<div ng-repeat="i in values"></div>' +
+ '<div ng-if="values.length==4"></div>' +
+ '<div ng-repeat="i in values"></div>'
+ )($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(
+ '<div>' +
+ '<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
+ '</div>'
+ ))($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(
+ '<div>' +
+ '<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
+ '</div>'
+ ))($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(
+ '<div>' +
+ '<div ng-if="value" style="' + style + '" ng-animate="{enter: \'custom-enter\', leave: \'custom-leave\'}"><div>Hi</div></div>' +
+ '</div>'
+ ))($scope);
+ $scope.$apply('value = true');
+
+ if ($sniffer.supportsTransitions) {
+ window.setTimeout.expect(1).process();
+ window.setTimeout.expect(500).process();
+ } else {
+ expect(window.setTimeout.queue).toEqual([]);
+ }
+ }));
+
+});
Please sign in to comment.
Something went wrong with that request. Please try again.