diff options
| -rwxr-xr-x[-rw-r--r--] | angularFiles.js | 1 | ||||
| -rwxr-xr-x[-rw-r--r--] | src/AngularPublic.js | 1 | ||||
| -rwxr-xr-x | src/ng/directive/ngIf.js | 83 | ||||
| -rwxr-xr-x | test/ng/directive/ngIfSpec.js | 191 | 
4 files changed, 276 insertions, 0 deletions
| diff --git a/angularFiles.js b/angularFiles.js index 30c65df1..2c2e4e0c 100644..100755 --- 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 index a66c35b3..1fd18ce2 100644..100755 --- 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 00000000..f1ceccda --- /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 +   <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); +            }); +          } +        }); +      } +    } +  } +}]; diff --git a/test/ng/directive/ngIfSpec.js b/test/ng/directive/ngIfSpec.js new file mode 100755 index 00000000..081ba5bf --- /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('<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([]); +      } +  })); + +}); | 
