Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat($compile): add one-way binding to the isolate scope definition
Browse files Browse the repository at this point in the history
This change allows the developer to bind an isolate scope / controller property
to an expression, using a `<` binding, in such a way that if the value of the
expression changes, the scope/controller property is updated but not the
converse.

The binding is implemented as a single simple watch, which can also provide
performance benefits over two way bindings.

Closes #13928
Closes #13854
Closes #12835
Closes #13900
  • Loading branch information
Narretz authored and petebacondarwin committed Feb 3, 2016
1 parent 507cf31 commit 4ac23c0
Show file tree
Hide file tree
Showing 2 changed files with 380 additions and 12 deletions.
47 changes: 44 additions & 3 deletions src/ng/compile.js
Expand Up @@ -189,6 +189,30 @@
* equality check is done by value (using the {@link angular.equals} function). It's also possible
* to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection
* `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional).
*
* * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an
* expression passed via the attribute `attr`. The expression is evaluated in the context of the
* parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the
* local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`.
*
* For example, given `<my-component my-attr="parentModel">` and directive definition of
* `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the
* value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
* in `localModel`, but changes in `localModel` will not reflect in `parentModel`. There are however
* two caveats:
* 1. one-way binding does not copy the value from the parent to the isolate scope, it simply
* sets the same value. That means if your bound value is an object, changes to its properties
* in the isolated scope will be reflected in the parent scope (because both reference the same object).
* 2. one-way binding watches changes to the **identity** of the parent value. That means the
* {@link ng.$rootScope.Scope#$watch `$watch`} on the parent value only fires if the reference
* to the value has changed. In most cases, this should not be of concern, but can be important
* to know if you one-way bind to an object, and then replace that object in the isolated scope.
* If you now change a property of the object in your parent scope, the change will not be
* propagated to the isolated scope, because the identity of the object on the parent scope
* has not changed. Instead you must assign a new object.
*
* One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings
* back to the parent. However, it does not make this completely impossible.
*
* * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If
* no `attr` name is specified then the attribute name is assumed to be the same as the local name.
Expand Down Expand Up @@ -829,7 +853,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 = {};

Expand Down Expand Up @@ -2965,7 +2989,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
optional = definition.optional,
mode = definition.mode, // @, =, or &
lastValue,
parentGet, parentSet, compare;
parentGet, parentSet, compare, removeWatch;

switch (mode) {

Expand Down Expand Up @@ -3026,7 +3050,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
var removeWatch;
if (definition.collection) {
removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
Expand All @@ -3035,6 +3058,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;
Expand Down

0 comments on commit 4ac23c0

Please sign in to comment.