From fad58695a588ace5f5e76d76f633843356b057f1 Mon Sep 17 00:00:00 2001 From: Daniel Herman Date: Fri, 2 Dec 2016 12:33:47 -0500 Subject: [PATCH] feat($compile): support expansion of special `ngBindon` attributes In order to reduce some of the verbosity associated with conforming to the ideas behind unidirectional data-flow, we can introduce a special attribute syntax that will be automatically expanded in order to simulate two-way data binding. For example, `` will be expanded into `` Fixes #15455 --- docs/content/guide/component.ngdoc | 15 +++++++++++++++ src/ng/compile.js | 22 ++++++++++++++++------ test/ng/compileSpec.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/docs/content/guide/component.ngdoc b/docs/content/guide/component.ngdoc index afc5273640b6..8e7988144869 100644 --- a/docs/content/guide/component.ngdoc +++ b/docs/content/guide/component.ngdoc @@ -147,6 +147,21 @@ components should follow a few simple conventions: }); } ``` + - A two-way binding can be simulated by using both a one-way binding as well as an output event. + ```js + bindings: { + hero: '<', + heroChange: '&' + } + ``` + ```html + + ``` + - Since that can be rather verbose, especially with repeated properties, we provide syntactic sugar for that pattern + via `ng-bindon-` attributes which expand to match the example above. + ```html + + ``` - **Components have a well-defined lifecycle** Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life diff --git a/src/ng/compile.js b/src/ng/compile.js index 90cedf61146e..005551800d4d 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -1803,7 +1803,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); }, - NG_ATTR_BINDING = /^ngAttr[A-Z]/; + NG_SPECIAL_ATTR = /^ng(Attr|Bindon)[A-Z]/; var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/; compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { @@ -2139,8 +2139,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, - j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { + for (var attr, name, nName, ngAttrName, value, isNgAttr, isSpecialAttr, isNgBindon, + nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; @@ -2150,10 +2150,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // support ngAttr attribute binding ngAttrName = directiveNormalize(name); - isNgAttr = NG_ATTR_BINDING.test(ngAttrName); - if (isNgAttr) { + + isSpecialAttr = NG_SPECIAL_ATTR.exec(ngAttrName); + isNgAttr = isSpecialAttr && isSpecialAttr[1] === 'Attr'; + isNgBindon = isSpecialAttr && isSpecialAttr[1] === 'Bindon'; + + if (isSpecialAttr) { name = name.replace(PREFIX_REGEXP, '') - .substr(8).replace(/_(.)/g, function(match, letter) { + .substr(isNgAttr ? 8 : 10).replace(/_(.)/g, function(match, letter) { return letter.toUpperCase(); }); } @@ -2173,6 +2177,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attrs[nName] = true; // presence means true } } + + if (isNgBindon) { + attrs[nName] = value; + attrs[nName + 'Change'] = value + '=$event'; + } + addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, attrEndName); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 355e11bc73e7..b7d42af6ffe6 100644 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -11441,6 +11441,36 @@ describe('$compile', function() { }); }); + describe('ngBindon attributes', function() { + it('should expand `ng-bindon-foo` to both an input and an output expression', function() { + module(function($compileProvider) { + $compileProvider.component('test', { + bindings: { + foo: '<', + fooChange: '&' + } + }); + }); + + inject(function($compile, $rootScope) { + $rootScope.bar = 0; + + element = $compile('')($rootScope); + var testController = element.controller('test'); + $rootScope.$digest(); + + expect(testController.foo).toBe(0); + $rootScope.$apply('bar=1'); + expect(testController.foo).toBe(1); + $rootScope.$apply(function() { + testController.fooChange({ $event: 2 }); + }); + + expect($rootScope.bar).toBe(2); + expect(testController.foo).toBe(2); + }); + }); + }); describe('when an attribute has an underscore-separated name', function() {