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() {