Permalink
Browse files

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.
  • Loading branch information...
1 parent 5c95b8c commit c3a41ff9fefe894663c4d4f40a83794521deb14f @mhevery mhevery committed with IgorMinar Jun 6, 2012
Showing with 293 additions and 218 deletions.
  1. +28 −56 docs/content/guide/directive.ngdoc
  2. +68 −53 src/ng/compile.js
  3. +12 −9 src/ng/directive/input.js
  4. +163 −93 test/ng/compileSpec.js
  5. +22 −7 test/ng/directive/inputSpec.js
@@ -321,34 +321,32 @@ compiler}. The attributes are:
parent scope. <br/>
The 'isolate' scope takes an object hash which defines a set of local scope properties
derived from the parent scope. These local properties are useful for aliasing values for
- templates. Locals definition is a hash of normalized element attribute name to their
- corresponding binding strategy. Valid binding strategies are:
-
- * `attribute` - one time read of element attribute value and save it to widget scope. <br/>
- Given `<widget my-attr='abc'>` and widget definition of `scope: {myAttr:'attribute'}`,
- then widget scope property `myAttr` will be `"abc"`.
-
- * `evaluate` - one time evaluation of expression stored in the attribute. <br/> Given
- `<widget my-attr='name'>` and widget definition of `scope: {myAttr:'evaluate'}`, and
- parent scope `{name:'angular'}` then widget scope property `myAttr` will be `"angular"`.
-
- * `bind` - Set up one way binding from the element attribute to the widget scope. <br/>
- Given `<widget my-attr='{{name}}'>` and widget definition of `scope: {myAttr:'bind'}`,
- and parent scope `{name:'angular'}` then widget scope property `myAttr` will be
- `"angular"`, but any changes in the parent scope will be reflected in the widget scope.
-
- * `accessor` - Set up getter/setter function for the expression in the widget element
- attribute to the widget scope. <br/> Given `<widget my-attr='name'>` and widget definition
- of `scope: {myAttr:'prop'}`, and parent scope `{name:'angular'}` then widget scope
- property `myAttr` will be a function such that `myAttr()` will return `"angular"` and
- `myAttr('new value')` will update the parent scope `name` property. This is useful for
- treating the element as a data-model for reading/writing.
-
- * `expression` - Treat element attribute as an expression to be executed on the parent scope.
- <br/>
- Given `<widget my-attr='doSomething()'>` and widget definition of `scope:
- {myAttr:'expression'}`, and parent scope `{doSomething:function() {}}` then calling the
- widget scope function `myAttr` will execute the expression against the parent scope.
+ templates. Locals definition is a hash of local scope property to its source:
+
+ * `@` or `@attr` - bind a local scope property to the DOM attribute. The result is always a
+ string since DOM attributes are strings. If no `attr` name is specified then the local name
+ and attribute name are same. Given `<widget my-attr="hello {{name}}">` and widget definition
+ of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect
+ the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the
+ `localName` property on the widget scope. The `name` is read from the parent scope (not
+ component scope).
+
+ * `=` or `=expression` - set up bi-directional binding between a local scope property and the
+ parent scope property. If no `attr` name is specified then the local name and attribute
+ name are same. Given `<widget my-attr="parentModel">` and widget definition of
+ `scope: { localModel:'=myAttr' }`, then widget scope property `localName` will reflect the
+ value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
+ in `localModel` and any changes in `localModel` will reflect in `parentModel`.
+
+ * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
+ If no `attr` name is specified then the local name and attribute name are same.
+ Given `<widget my-attr="count = count + value">` and widget definition of
+ `scope: { localFn:'increment()' }`, then isolate scope property `localFn` will point to
+ a function wrapper for the `increment()` expression. Often it's desirable to pass data from
+ the isolate scope via an expression and to the parent scope, this can be done by passing a
+ map of local variable names and values into the expression wrapper fn. For example if the
+ expression is `increment(amount)` then we can specify the amount value by calling the
+ `localFn` as `localFn({amount: 22})`.
* `controller` - Controller constructor function. The controller is instantiated before the
pre-linking phase and it is shared with other directives if they request it by name (see
@@ -369,32 +367,6 @@ compiler}. The attributes are:
* `^` - Look for the controller on parent elements as well.
- * `inject` (object hash) - Specifies a way to inject bindings into a controller. Injection
- definition is a hash of normalized element attribute names to their corresponding binding
- strategy. Valid binding strategies are:
-
- * `attribute` - inject attribute value. <br/>
- Given `<widget my-attr='abc'>` and widget definition of `inject: {myAttr:'attribute'}`, then
- `myAttr` will inject `"abc"`.
-
- * `evaluate` - inject one time evaluation of expression stored in the attribute. <br/>
- Given `<widget my-attr='name'>` and widget definition of `inject: {myAttr:'evaluate'}`, and
- parent scope `{name:'angular'}` then `myAttr` will inject `"angular"`.
-
- * `accessor` - inject a getter/setter function for the expression in the widget element
- attribute to the widget scope. <br/>
- Given `<widget my-attr='name'>` and widget definition of `inject: {myAttr:'prop'}`, and
- parent scope `{name:'angular'}` then injecting `myAttr` will inject a function such
- that `myAttr()` will return `"angular"` and `myAttr('new value')` will update the parent
- scope `name` property. This is usefull for treating the element as a data-model for
- reading/writing.
-
- * `expression` - Inject expression function. <br/>
- Given `<widget my-attr='doSomething()'>` and widget definition of
- `inject: {myAttr:'expression'}`, and parent scope `{doSomething:function() {}}` then
- injecting `myAttr` will inject a function which when called will execute the expression
- against the parent scope.
-
* `restrict` - String of subset of `EACM` which restricts the directive to a specific directive
declaration style. If omitted directives are allowed on attributes only.
@@ -649,9 +621,9 @@ Following is an example of building a reusable widget.
// This HTML will replace the zippy directive.
replace: true,
transclude: true,
- scope: { zippyTitle:'bind' },
+ scope: { title:'@zippyTitle' },
template: '<div>' +
- '<div class="title">{{zippyTitle}}</div>' +
+ '<div class="title">{{title}}</div>' +
'<div class="body" ng-transclude></div>' +
'</div>',
// The linking function will add behavior to the template
View
@@ -18,6 +18,9 @@
*/
+var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: ';
+
+
/**
* @ngdoc function
* @name angular.module.ng.$compile
@@ -225,47 +228,6 @@ function $CompileProvider($provide) {
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
$controller, $rootScope) {
- var LOCAL_MODE = {
- attribute: function(localName, mode, parentScope, scope, attr) {
- scope[localName] = attr[localName];
- },
-
- evaluate: function(localName, mode, parentScope, scope, attr) {
- scope[localName] = parentScope.$eval(attr[localName]);
- },
-
- bind: function(localName, mode, parentScope, scope, attr) {
- var getter = $interpolate(attr[localName]);
- scope.$watch(
- function() { return getter(parentScope); },
- function(v) { scope[localName] = v; }
- );
- },
-
- accessor: function(localName, mode, parentScope, scope, attr) {
- var getter = noop,
- setter = noop,
- exp = attr[localName];
-
- if (exp) {
- getter = $parse(exp);
- setter = getter.assign || function() {
- throw Error("Expression '" + exp + "' not assignable.");
- };
- }
-
- scope[localName] = function(value) {
- return arguments.length ? setter(parentScope, value) : getter(parentScope);
- };
- },
-
- expression: function(localName, mode, parentScope, scope, attr) {
- scope[localName] = function(locals) {
- $parse(attr[localName])(parentScope, locals);
- };
- }
- };
-
var Attributes = function(element, attr) {
this.$$element = element;
this.$attr = attr || {};
@@ -746,9 +708,67 @@ function $CompileProvider($provide) {
$element = attrs.$$element;
if (newScopeDirective && isObject(newScopeDirective.scope)) {
- forEach(newScopeDirective.scope, function(mode, name) {
- (LOCAL_MODE[mode] || wrongMode)(name, mode,
- scope.$parent || scope, scope, attrs);
+ var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/;
+
+ var parentScope = scope.$parent || scope;
+
+ forEach(newScopeDirective.scope, function(definiton, scopeName) {
+ var match = definiton.match(LOCAL_REGEXP) || [],
+ attrName = match[2]|| scopeName,
+ mode = match[1], // @, =, or &
+ lastValue,
+ parentGet, parentSet;
+
+ switch (mode) {
+
+ case '@': {
+ attrs.$observe(attrName, function(value) {
+ scope[scopeName] = value;
+ });
+ attrs.$$observers[attrName].$$scope = parentScope;
+ break;
+ }
+
+ case '=': {
+ parentGet = $parse(attrs[attrName]);
+ parentSet = parentGet.assign || function() {
+ // reset the change, or we will throw this exception on every $digest
+ lastValue = scope[scopeName] = parentGet(parentScope);
+ throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] +
+ ' (directive: ' + newScopeDirective.name + ')');
+ };
+ lastValue = scope[scopeName] = parentGet(parentScope);
+ scope.$watch(function() {
+ var parentValue = parentGet(parentScope);
+
+ if (parentValue !== scope[scopeName]) {
+ // we are out of sync and need to copy
+ if (parentValue !== lastValue) {
+ // parent changed and it has precedence
+ lastValue = scope[scopeName] = parentValue;
+ } else {
+ // if the parent can be assigned then do so
+ parentSet(parentScope, lastValue = scope[scopeName]);
+ }
+ }
+ return parentValue;
+ });
+ break;
+ }
+
+ case '&': {
+ parentGet = $parse(attrs[attrName]);
+ scope[scopeName] = function(locals) {
+ return parentGet(parentScope, locals);
+ }
+ break;
+ }
+
+ default: {
+ throw Error('Invalid isolate scope definition for directive ' +
+ newScopeDirective.name + ': ' + definiton);
+ }
+ }
});
}
@@ -761,12 +781,6 @@ function $CompileProvider($provide) {
$transclude: boundTranscludeFn
};
-
- forEach(directive.inject || {}, function(mode, name) {
- (LOCAL_MODE[mode] || wrongMode)(name, mode,
- newScopeDirective ? scope.$parent || scope : scope, locals, attrs);
- });
-
controller = directive.controller;
if (controller == '@') {
controller = attrs[directive.name];
@@ -1007,9 +1021,10 @@ function $CompileProvider($provide) {
attr[name] = undefined;
($$observers[name] || ($$observers[name] = [])).$$inter = true;
- scope.$watch(interpolateFn, function(value) {
- attr.$set(name, value);
- });
+ (attr.$$observers && attr.$$observers[name].$$scope || scope).
+ $watch(interpolateFn, function(value) {
+ attr.$set(name, value);
+ });
})
});
}
@@ -857,8 +857,8 @@ var VALID_CLASS = 'ng-valid',
* </example>
*
*/
-var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$element',
- function($scope, $exceptionHandler, $attr, ngModel, $element) {
+var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
+ function($scope, $exceptionHandler, $attr, $element, $parse) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$parsers = [];
@@ -870,6 +870,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
this.$invalid = false;
this.$name = $attr.name;
+ var ngModelGet = $parse($attr.ngModel),
+ ngModelSet = ngModelGet.assign;
+
+ if (!ngModelSet) {
+ throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + $attr.ngModel +
+ ' (' + startingTag($element) + ')');
+ }
+
/**
* @ngdoc function
* @name angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$render
@@ -974,7 +982,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
if (this.$modelValue !== value) {
this.$modelValue = value;
- ngModel(value);
+ ngModelSet($scope, value);
forEach(this.$viewChangeListeners, function(listener) {
try {
listener();
@@ -987,9 +995,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
// model -> value
var ctrl = this;
- $scope.$watch(function() {
- return ngModel();
- }, function(value) {
+ $scope.$watch(ngModelGet, function(value) {
// ignore change from view
if (ctrl.$modelValue === value) return;
@@ -1044,9 +1050,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
*/
var ngModelDirective = function() {
return {
- inject: {
- ngModel: 'accessor'
- },
require: ['ngModel', '^?form'],
controller: NgModelController,
link: function(scope, element, attr, ctrls) {
Oops, something went wrong.

0 comments on commit c3a41ff

Please sign in to comment.