diff --git a/src/ng/compile.js b/src/ng/compile.js index 604333dc5486..f63f1dd3e4b6 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -186,6 +186,30 @@ * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional). * + * * `<` or `` and directive definition of + * `scope: { localModel:'` and widget definition of @@ -826,7 +850,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; function parseIsolateBindings(scope, directiveName, isController) { - var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; + var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/; var bindings = {}; @@ -2962,7 +2986,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { optional = definition.optional, mode = definition.mode, // @, =, or & lastValue, - parentGet, parentSet, compare; + parentGet, parentSet, compare, removeWatch; switch (mode) { @@ -3023,7 +3047,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return lastValue = parentValue; }; parentValueWatch.$stateful = true; - var removeWatch; if (definition.collection) { removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); } else { @@ -3032,6 +3055,24 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { removeWatchCollection.push(removeWatch); break; + case '<': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + attrs[attrName] = void 0; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + + destination[scopeName] = parentGet(scope); + + removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) { + destination[scopeName] = newParentValue; + }, parentGet.literal); + + removeWatchCollection.push(removeWatch); + break; + case '&': // Don't assign Object.prototype method to scope parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 240906721a33..83c6c8bf8955 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3533,6 +3533,10 @@ describe('$compile', function() { optreference: '=?', colref: '=*', colrefAlias: '=* colref', + owRef: '<', + owRefAlias: '< owRef', + owOptref: ''); @@ -4135,9 +4139,7 @@ describe('$compile', function() { $rootScope.$apply(); expect(componentScope.reference).toBe(literalValue); dealoc(element); - } - })); }); @@ -4219,6 +4221,265 @@ describe('$compile', function() { }); + describe('one-way binding', function() { + it('should update isolate when the identity of origin changes', inject(function() { + compile('
'); + expect(componentScope.owRef).toBe(undefined); + expect(componentScope.owRefAlias).toBe(componentScope.owRef); + + $rootScope.obj = {value: 'initial'}; + $rootScope.$apply(); + + expect($rootScope.obj).toEqual({value: 'initial'}); + expect(componentScope.owRef).toEqual({value: 'initial'}); + expect(componentScope.owRefAlias).toBe(componentScope.owRef); + + // This changes in both scopes because of reference + $rootScope.obj.value = 'origin1'; + $rootScope.$apply(); + expect(componentScope.owRef.value).toBe('origin1'); + expect(componentScope.owRefAlias.value).toBe('origin1'); + + componentScope.owRef = {value: 'isolate1'}; + componentScope.$apply(); + expect($rootScope.obj.value).toBe('origin1'); + + // Change does not propagate because object identity hasn't changed + $rootScope.obj.value = 'origin2'; + $rootScope.$apply(); + expect(componentScope.owRef.value).toBe('isolate1'); + expect(componentScope.owRefAlias.value).toBe('origin2'); + + // Change does propagate because object identity changes + $rootScope.obj = {value: 'origin3'}; + $rootScope.$apply(); + expect(componentScope.owRef.value).toBe('origin3'); + expect(componentScope.owRef).toBe($rootScope.obj); + expect(componentScope.owRefAlias).toBe($rootScope.obj); + })); + + it('should update isolate when both change', inject(function() { + compile('
'); + $rootScope.name = {mark:123}; + componentScope.owRef = 'misko'; + + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark:123}); + expect(componentScope.owRef).toBe($rootScope.name); + expect(componentScope.owRefAlias).toBe($rootScope.name); + + $rootScope.name = 'igor'; + componentScope.owRef = {}; + $rootScope.$apply(); + expect($rootScope.name).toEqual('igor'); + expect(componentScope.owRef).toBe($rootScope.name); + expect(componentScope.owRefAlias).toBe($rootScope.name); + })); + + + it('should not break when isolate and origin both change to the same value', inject(function() { + $rootScope.name = 'aaa'; + compile('
'); + + //change both sides to the same item within the same digest cycle + componentScope.owRef = 'same'; + $rootScope.name = 'same'; + $rootScope.$apply(); + + //change origin back to its previous value + $rootScope.name = 'aaa'; + $rootScope.$apply(); + + expect($rootScope.name).toBe('aaa'); + expect(componentScope.owRef).toBe('aaa'); + })); + + + it('should not update origin when identity of isolate changes', inject(function() { + $rootScope.name = {mark:123}; + compile('
'); + + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark:123}); + expect(componentScope.owRef).toBe($rootScope.name); + expect(componentScope.owRefAlias).toBe($rootScope.name); + + componentScope.owRef = 'martin'; + $rootScope.$apply(); + expect($rootScope.name).toEqual({mark: 123}); + expect(componentScope.owRef).toBe('martin'); + expect(componentScope.owRefAlias).toEqual({mark: 123}); + })); + + + it('should update origin when property of isolate object reference changes', inject(function() { + $rootScope.obj = {mark:123}; + compile('
'); + + $rootScope.$apply(); + expect($rootScope.obj).toEqual({mark:123}); + expect(componentScope.owRef).toBe($rootScope.obj); + + componentScope.owRef.mark = 789; + $rootScope.$apply(); + expect($rootScope.obj).toEqual({mark: 789}); + expect(componentScope.owRef).toBe($rootScope.obj); + })); + + + it('should not throw on non assignable expressions in the parent', inject(function() { + compile('
'); + $rootScope.name = 'world'; + $rootScope.$apply(); + expect(componentScope.owRef).toBe('hello world'); + + componentScope.owRef = 'ignore me'; + expect(componentScope.owRef).toBe('ignore me'); + expect($rootScope.name).toBe('world'); + + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.owRef).toBe('hello misko'); + })); + + + it('should not throw when assigning to undefined', inject(function() { + compile('
'); + $rootScope.$apply(); + expect(componentScope.owRef).toBeUndefined(); + + componentScope.owRef = 'ignore me'; + expect(componentScope.owRef).toBe('ignore me'); + + $rootScope.$apply(); + expect(componentScope.owRef).toBe('ignore me'); + })); + + + it('should update isolate scope when "<"-bound NaN changes', inject(function() { + $rootScope.num = NaN; + compile('
'); + var isolateScope = element.isolateScope(); + expect(isolateScope.owRef).toBeNaN(); + + $rootScope.num = 64; + $rootScope.$apply(); + expect(isolateScope.owRef).toBe(64); + })); + + + describe('literal objects', function() { + it('should copy parent changes', inject(function() { + compile('
'); + + $rootScope.name = 'a'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'a'}); + + $rootScope.name = 'b'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'b'}); + })); + + + it('should not change the isolated scope when origin does not change', inject(function() { + compile('
'); + + $rootScope.name = 'a'; + $rootScope.$apply(); + var lastComponentValue = componentScope.owRef; + $rootScope.$apply(); + expect(componentScope.owRef).toBe(lastComponentValue); + })); + + + it('should deep-watch array literals', inject(function() { + $rootScope.name = 'georgios'; + $rootScope.obj = {name: 'pete'}; + compile('
'); + $rootScope.$apply(); + expect(componentScope.owRef).toEqual([{name: 'georgios'}, {name: 'pete'}]); + + $rootScope.name = 'lucas'; + $rootScope.obj = {name: 'martin'}; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual([{name: 'lucas'}, {name: 'martin'}]); + })); + + + it('should deep-watch object literals', inject(function() { + $rootScope.name = 'georgios'; + $rootScope.obj = {name: 'pete'}; + compile('
'); + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'georgios', item: {name: 'pete'}}); + + $rootScope.name = 'lucas'; + $rootScope.obj = {name: 'martin'}; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'lucas', item: {name: 'martin'}}); + })); + + + it('should not complain when the isolated scope changes', inject(function() { + compile('
'); + + $rootScope.name = 'a'; + $rootScope.$apply(); + componentScope.owRef = {name: 'b'}; + componentScope.$apply(); + + expect(componentScope.owRef).toEqual({name: 'b'}); + expect($rootScope.name).toBe('a'); + + $rootScope.name = 'c'; + $rootScope.$apply(); + expect(componentScope.owRef).toEqual({name: 'c'}); + })); + + it('should work for primitive literals', inject(function() { + test('1', 1); + test('null', null); + test('undefined', undefined); + test("'someString'", 'someString'); + test('true', true); + + function test(literalString, literalValue) { + compile('
'); + + $rootScope.$apply(); + expect(componentScope.owRef).toBe(literalValue); + dealoc(element); + } + })); + + describe('optional one-way binding', function() { + it('should update local when origin changes', inject(function() { + compile('
'); + expect(componentScope.owOptref).toBe(undefined); + expect(componentScope.owOptrefAlias).toBe(componentScope.owOptref); + + $rootScope.name = 'misko'; + $rootScope.$apply(); + expect(componentScope.owOptref).toBe($rootScope.name); + expect(componentScope.owOptrefAlias).toBe($rootScope.name); + + $rootScope.name = {}; + $rootScope.$apply(); + expect(componentScope.owOptref).toBe($rootScope.name); + expect(componentScope.owOptrefAlias).toBe($rootScope.name); + })); + + it('should not throw exception when reference does not exist', inject(function() { + compile('
'); + + expect(componentScope.owOptref).toBe(undefined); + expect(componentScope.owOptrefAlias).toBe(undefined); + })); + }); + }); + }); + describe('executable expression', function() { it('should allow expression execution with locals', inject(function() { compile('
'); @@ -4254,6 +4515,9 @@ describe('$compile', function() { expect(componentScope.$$isolateBindings.refAlias.attrName).toBe('ref'); expect(componentScope.$$isolateBindings.reference.mode).toBe('='); expect(componentScope.$$isolateBindings.reference.attrName).toBe('reference'); + expect(componentScope.$$isolateBindings.owRef.mode).toBe('<'); + expect(componentScope.$$isolateBindings.owRef.attrName).toBe('owRef'); + expect(componentScope.$$isolateBindings.owRefAlias.attrName).toBe('owRef'); expect(componentScope.$$isolateBindings.expr.mode).toBe('&'); expect(componentScope.$$isolateBindings.expr.attrName).toBe('expr'); expect(componentScope.$$isolateBindings.exprAlias.attrName).toBe('expr'); @@ -4268,13 +4532,14 @@ describe('$compile', function() { })); - it('should expose isolate scope variables on controller with controllerAs when bindToController is true', function() { + it('should expose isolate scope variables on controller with controllerAs when bindToController is true (template)', function() { var controllerCalled = false; module(function($compileProvider) { $compileProvider.directive('fooDir', valueFn({ template: '

isolate

', scope: { 'data': '=dirData', + 'oneway': 'isolate

', scope: { 'data': '=dirData', + 'oneway': '' + + 'from-parent-3="ctrl.value3" ' + + 'from-parent-4="ctrl.value4">' + '' + '
')($rootScope); $rootScope.$digest(); @@ -4794,6 +5102,8 @@ describe('$compile', function() { expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); + expect(childCtrl.fromParent4).toBe(parentCtrl.value4); + expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); childCtrl.fromParent2 = 'modified'; $rootScope.$digest(); @@ -4813,11 +5123,13 @@ describe('$compile', function() { this.value1 = 'parent1'; this.value2 = 'parent2'; this.value3 = function() { return 'parent3'; }; + this.value4 = 'parent4'; }, 'ChildCtrl': function() { this.value1 = 'child1'; this.value2 = 'child2'; this.value3 = function() { return 'child3'; }; + this.value4 = 'child4'; } }); @@ -4827,7 +5139,8 @@ describe('$compile', function() { bindToController: { fromParent1: '@', fromParent2: '=', - fromParent3: '&' + fromParent3: '&', + fromParent4: '<' }, template: '' })); @@ -4839,7 +5152,8 @@ describe('$compile', function() { '' + + 'from-parent-3="ctrl.value3" ' + + 'from-parent-4="ctrl.value4">' + '' + '
')($rootScope); $rootScope.$digest(); @@ -4853,6 +5167,8 @@ describe('$compile', function() { expect(childCtrl.fromParent2).not.toBe(childCtrl.value2); expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3()); expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3()); + expect(childCtrl.fromParent4).toBe(parentCtrl.value4); + expect(childCtrl.fromParent4).not.toBe(childCtrl.value4); childCtrl.fromParent2 = 'modified'; $rootScope.$digest(); @@ -4902,6 +5218,10 @@ describe('$compile', function() { 'foo': 'bar', 'baz': 'biz' }); + expect(this.oneway).toEqualData({ + 'foo': 'bar', + 'baz': 'biz' + }); expect(this.str).toBe('Hello, world!'); expect(this.fn()).toBe('called!'); }; @@ -4916,6 +5236,7 @@ describe('$compile', function() { templateUrl: 'test.html', bindToController: { 'data': '=dirData', + 'oneway': '