diff options
| author | Misko Hevery | 2012-06-06 13:58:10 -0700 | 
|---|---|---|
| committer | Igor Minar | 2012-06-08 15:50:13 -0700 | 
| commit | c3a41ff9fefe894663c4d4f40a83794521deb14f (patch) | |
| tree | b44037cfb0089cfea42f253b6ad1a09ccb7e2d86 /test/ng | |
| parent | 5c95b8cccc0d72f7ca3afb1162b9528c1222eb3c (diff) | |
| download | angular.js-c3a41ff9fefe894663c4d4f40a83794521deb14f.tar.bz2 | |
feat($compile): simplify isolate scope bindings
Changed the isolate scope binding options to:
  - @attr - attribute binding (including interpolation)
  - =model - by-directional model binding
  - &expr - expression execution binding
This change simplifies the terminology as well as
number of choices available to the developer. It
also supports local name aliasing from the parent.
BREAKING CHANGE: isolate scope bindings definition has changed and
the inject option for the directive controller injection was removed.
To migrate the code follow the example below:
Before:
scope: {
  myAttr: 'attribute',
  myBind: 'bind',
  myExpression: 'expression',
  myEval: 'evaluate',
  myAccessor: 'accessor'
}
After:
scope: {
  myAttr: '@',
  myBind: '@',
  myExpression: '&',
  // myEval - usually not useful, but in cases where the expression is assignable, you can use '='
  myAccessor: '=' // in directive's template change myAccessor() to myAccessor
}
The removed `inject` wasn't generaly useful for directives so there should be no code using it.
Diffstat (limited to 'test/ng')
| -rw-r--r-- | test/ng/compileSpec.js | 256 | ||||
| -rw-r--r-- | test/ng/directive/inputSpec.js | 29 | 
2 files changed, 185 insertions, 100 deletions
| diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index d2b0360c..93183b93 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -1,7 +1,7 @@  'use strict';  describe('$compile', function() { -  var element, directive; +  var element, directive, $compile, $rootScope;    beforeEach(module(provideLog, function($provide, $compileProvider){      element = null; @@ -54,8 +54,17 @@ describe('$compile', function() {        priority: -100, // even with negative priority we still should be able to stop descend        terminal: true      })); + +    return function(_$compile_, _$rootScope_) { +      $rootScope = _$rootScope_; +      $compile = _$compile_; +    };    })); +  function compile(html) { +    element = angular.element(html); +    $compile(element)($rootScope); +  }    afterEach(function(){      dealoc(element); @@ -1633,105 +1642,166 @@ describe('$compile', function() {    }); -  describe('locals', function() { -    it('should marshal to locals', function() { -      module(function() { -        directive('widget', function(log) { -          return { -            scope: { -              attr: 'attribute', -              prop: 'evaluate', -              bind: 'bind', -              assign: 'accessor', -              read: 'accessor', -              exp: 'expression', -              nonExist: 'accessor', -              nonExistExpr: 'expression' -            }, -            link: function(scope, element, attrs) { -              scope.nonExist(); // noop -              scope.nonExist(123); // noop -              scope.nonExistExpr(); // noop -              scope.nonExistExpr(123); // noop -              log(scope.attr); -              log(scope.prop); -              log(scope.assign()); -              log(scope.read()); -              log(scope.assign('ng')); -              scope.exp({myState:'OK'}); -              expect(function() { scope.read(undefined); }). -                  toThrow("Expression ''D'' not assignable."); -              scope.$watch('bind', log); -            } -          }; -        }); +  describe('isolated locals', function() { +    var componentScope; + +    beforeEach(module(function() { +      directive('myComponent', function() { +        return { +          scope: { +            attr: '@', +            attrAlias: '@attr', +            ref: '=', +            refAlias: '= ref', +            expr: '&', +            exprAlias: '&expr' +          }, +          link: function(scope) { +            componentScope = scope; +          } +        };        }); -      inject(function(log, $compile, $rootScope) { -        $rootScope.myProp = 'B'; -        $rootScope.bi = {nd: 'C'}; -        $rootScope.name = 'C'; -        element = $compile( -            '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' + -                'exp="state=myState">{{bind}}</div></div>') -            ($rootScope); -        expect(log).toEqual('A; B; C; D; ng'); -        expect($rootScope.name).toEqual('ng'); -        expect($rootScope.state).toEqual('OK'); -        log.reset(); +      directive('badDeclaration', function() { +        return { +          scope: { attr: 'xxx' } +        }; +      }); +    })); + +    describe('attribute', function() { +      it('should copy simple attribute', inject(function() { +        compile('<div><span my-component attr="some text">'); +        expect(componentScope.attr).toEqual(undefined); +        expect(componentScope.attrAlias).toEqual(undefined); +          $rootScope.$apply(); -        expect(element.text()).toEqual('C'); -        expect(log).toEqual('C'); -        $rootScope.bi.nd = 'c'; + +        expect(componentScope.attr).toEqual('some text'); +        expect(componentScope.attrAlias).toEqual('some text'); +        expect(componentScope.attrAlias).toEqual(componentScope.attr); +      })); + + +      it('should update when interpolated attribute updates', inject(function() { +        compile('<div><span my-component attr="hello {{name}}">'); +        expect(componentScope.attr).toEqual(undefined); +        expect(componentScope.attrAlias).toEqual(undefined); + +        $rootScope.name = 'misko';          $rootScope.$apply(); -        expect(log).toEqual('C; c'); -      }); + +        expect(componentScope.attr).toEqual('hello misko'); +        expect(componentScope.attrAlias).toEqual('hello misko'); + +        $rootScope.name = 'igor'; +        $rootScope.$apply(); + +        expect(componentScope.attr).toEqual('hello igor'); +        expect(componentScope.attrAlias).toEqual('hello igor'); +      }));      }); -  }); -  describe('controller', function() { -    it('should inject locals to controller', function() { -      module(function() { -        directive('widget', function(log) { -          return { -            controller: function(attr, prop, assign, read, exp){ -              log(attr); -              log(prop); -              log(assign()); -              log(read()); -              log(assign('ng')); -              exp(); -              expect(function() { read(undefined); }). -                  toThrow("Expression ''D'' not assignable."); -              this.result = 'OK'; -            }, -            inject: { -              attr: 'attribute', -              prop: 'evaluate', -              assign: 'accessor', -              read: 'accessor', -              exp: 'expression' -            }, -            link: function(scope, element, attrs, controller) { -              log(controller.result); -            } -          }; -        }); -      }); -      inject(function(log, $compile, $rootScope) { -        $rootScope.myProp = 'B'; -        $rootScope.bi = {nd: 'C'}; -        $rootScope.name = 'C'; -        element = $compile( -            '<div><div widget attr="A" prop="myProp" bind="{{bi.nd}}" assign="name" read="\'D\'" ' + -                'exp="state=\'OK\'">{{bind}}</div></div>') -            ($rootScope); -        expect(log).toEqual('A; B; C; D; ng; OK'); -        expect($rootScope.name).toEqual('ng'); -      }); +    describe('object reference', function() { +      it('should update local when origin changes', inject(function() { +        compile('<div><span my-component ref="name">'); +        expect(componentScope.ref).toBe(undefined); +        expect(componentScope.refAlias).toBe(componentScope.ref); + +        $rootScope.name = 'misko'; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); + +        $rootScope.name = {}; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); +      })); + + +      it('should update local when origin changes', inject(function() { +        compile('<div><span my-component ref="name">'); +        expect(componentScope.ref).toBe(undefined); +        expect(componentScope.refAlias).toBe(componentScope.ref); + +        componentScope.ref = 'misko'; +        $rootScope.$apply(); +        expect($rootScope.name).toBe('misko'); +        expect(componentScope.ref).toBe('misko'); +        expect($rootScope.name).toBe(componentScope.ref); +        expect(componentScope.refAlias).toBe(componentScope.ref); + +        componentScope.name = {}; +        $rootScope.$apply(); +        expect($rootScope.name).toBe(componentScope.ref); +        expect(componentScope.refAlias).toBe(componentScope.ref); +      })); + + +      it('should update local when both change', inject(function() { +        compile('<div><span my-component ref="name">'); +        $rootScope.name = {mark:123}; +        componentScope.ref = 'misko'; + +        $rootScope.$apply(); +        expect($rootScope.name).toEqual({mark:123}) +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); + +        $rootScope.name = 'igor'; +        componentScope.ref = {}; +        $rootScope.$apply(); +        expect($rootScope.name).toEqual('igor') +        expect(componentScope.ref).toBe($rootScope.name); +        expect(componentScope.refAlias).toBe($rootScope.name); +      })); + +      it('should complain on non assignable changes', inject(function() { +        compile('<div><span my-component ref="\'hello \' + name">'); +        $rootScope.name = 'world'; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe('hello world'); + +        componentScope.ref = 'ignore me'; +        expect($rootScope.$apply). +            toThrow("Non-assignable model expression: 'hello ' + name (directive: myComponent)"); +        expect(componentScope.ref).toBe('hello world'); +        // reset since the exception was rethrown which prevented phase clearing +        $rootScope.$$phase = null; + +        $rootScope.name = 'misko'; +        $rootScope.$apply(); +        expect(componentScope.ref).toBe('hello misko'); +      })); +    }); + + +    describe('executable expression', function() { +      it('should allow expression execution with locals', inject(function() { +        compile('<div><span my-component expr="count = count + offset">'); +        $rootScope.count = 2; + +        expect(typeof componentScope.expr).toBe('function'); +        expect(typeof componentScope.exprAlias).toBe('function'); + +        expect(componentScope.expr({offset: 1})).toEqual(3); +        expect($rootScope.count).toEqual(3); + +        expect(componentScope.exprAlias({offset: 10})).toEqual(13); +        expect($rootScope.count).toEqual(13); +      }));      }); +    it('should throw on unknown definition', inject(function() { +      expect(function() { +        compile('<div><span bad-declaration>'); +      }).toThrow('Invalid isolate scope definition for directive badDeclaration: xxx'); +    })); +  }); + +  describe('controller', function() {      it('should get required controller', function() {        module(function() {          directive('main', function(log) { @@ -1986,11 +2056,11 @@ describe('$compile', function() {        module(function() {          directive('box', valueFn({            transclude: 'content', -          scope: { name: 'evaluate', show: 'accessor' }, +          scope: { name: '=', show: '=' },            template: '<div><h1>Hello: {{name}}!</h1><div ng-transclude></div></div>',            link: function(scope, element) {              scope.$watch( -                function() { return scope.show(); }, +                'show',                  function(show) {                    if (!show) {                      element.find('div').find('div').remove(); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index d7ca7aea..3b511011 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -4,7 +4,7 @@ describe('NgModelController', function() {    var ctrl, scope, ngModelAccessor, element, parentFormCtrl;    beforeEach(inject(function($rootScope, $controller) { -    var attrs = {name: 'testAlias'}; +    var attrs = {name: 'testAlias', ngModel: 'value'};      parentFormCtrl = {        $setValidity: jasmine.createSpy('$setValidity'), @@ -17,12 +17,7 @@ describe('NgModelController', function() {      scope = $rootScope;      ngModelAccessor = jasmine.createSpy('ngModel accessor');      ctrl = $controller(NgModelController, { -      $scope: scope, $element: element.find('input'), ngModel: ngModelAccessor, $attrs: attrs -    }); -    // mock accessor (locals) -    ngModelAccessor.andCallFake(function(val) { -      if (isDefined(val)) scope.value = val; -      return scope.value; +      $scope: scope, $element: element.find('input'), $attrs: attrs      });    })); @@ -32,6 +27,26 @@ describe('NgModelController', function() {    }); +  it('should fail on non-assignable model binding', inject(function($controller) { +    var exception; + +    try { +      $controller(NgModelController, { +        $scope: null, +        $element: jqLite('<input ng-model="1+2">'), +        $attrs: { +          ngModel: '1+2' +        } +      }); +    } catch (e) { +      exception = e; +    } + +    expect(exception.message). +        toMatch(/Non-assignable model expression: 1\+2 \(<input( value="")? ng-model="1\+2">\)/); +  })); + +    it('should init the properties', function() {      expect(ctrl.$dirty).toBe(false);      expect(ctrl.$pristine).toBe(true); | 
