From c3a41ff9fefe894663c4d4f40a83794521deb14f Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Wed, 6 Jun 2012 13:58:10 -0700 Subject: 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. --- test/ng/compileSpec.js | 256 ++++++++++++++++++++++++++--------------- test/ng/directive/inputSpec.js | 29 +++-- 2 files changed, 185 insertions(+), 100 deletions(-) (limited to 'test/ng') 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( - '
{{bind}}
') - ($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('
'); + 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('
'); + 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( - '
{{bind}}
') - ($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('
'); + 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('
'); + 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('
'); + $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('
'); + $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('
'); + $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('
'); + }).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: '

Hello: {{name}}!

', 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(''), + $attrs: { + ngModel: '1+2' + } + }); + } catch (e) { + exception = e; + } + + expect(exception.message). + toMatch(/Non-assignable model expression: 1\+2 \(\)/); + })); + + it('should init the properties', function() { expect(ctrl.$dirty).toBe(false); expect(ctrl.$pristine).toBe(true); -- cgit v1.2.3