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

Commit c3a41ff

Browse files
mheveryIgorMinar
authored andcommitted
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.
1 parent 5c95b8c commit c3a41ff

File tree

5 files changed

+293
-218
lines changed

5 files changed

+293
-218
lines changed

docs/content/guide/directive.ngdoc

+28-56
Original file line numberDiff line numberDiff line change
@@ -321,34 +321,32 @@ compiler}. The attributes are:
321321
parent scope. <br/>
322322
The 'isolate' scope takes an object hash which defines a set of local scope properties
323323
derived from the parent scope. These local properties are useful for aliasing values for
324-
templates. Locals definition is a hash of normalized element attribute name to their
325-
corresponding binding strategy. Valid binding strategies are:
326-
327-
* `attribute` - one time read of element attribute value and save it to widget scope. <br/>
328-
Given `<widget my-attr='abc'>` and widget definition of `scope: {myAttr:'attribute'}`,
329-
then widget scope property `myAttr` will be `"abc"`.
330-
331-
* `evaluate` - one time evaluation of expression stored in the attribute. <br/> Given
332-
`<widget my-attr='name'>` and widget definition of `scope: {myAttr:'evaluate'}`, and
333-
parent scope `{name:'angular'}` then widget scope property `myAttr` will be `"angular"`.
334-
335-
* `bind` - Set up one way binding from the element attribute to the widget scope. <br/>
336-
Given `<widget my-attr='{{name}}'>` and widget definition of `scope: {myAttr:'bind'}`,
337-
and parent scope `{name:'angular'}` then widget scope property `myAttr` will be
338-
`"angular"`, but any changes in the parent scope will be reflected in the widget scope.
339-
340-
* `accessor` - Set up getter/setter function for the expression in the widget element
341-
attribute to the widget scope. <br/> Given `<widget my-attr='name'>` and widget definition
342-
of `scope: {myAttr:'prop'}`, and parent scope `{name:'angular'}` then widget scope
343-
property `myAttr` will be a function such that `myAttr()` will return `"angular"` and
344-
`myAttr('new value')` will update the parent scope `name` property. This is useful for
345-
treating the element as a data-model for reading/writing.
346-
347-
* `expression` - Treat element attribute as an expression to be executed on the parent scope.
348-
<br/>
349-
Given `<widget my-attr='doSomething()'>` and widget definition of `scope:
350-
{myAttr:'expression'}`, and parent scope `{doSomething:function() {}}` then calling the
351-
widget scope function `myAttr` will execute the expression against the parent scope.
324+
templates. Locals definition is a hash of local scope property to its source:
325+
326+
* `@` or `@attr` - bind a local scope property to the DOM attribute. The result is always a
327+
string since DOM attributes are strings. If no `attr` name is specified then the local name
328+
and attribute name are same. Given `<widget my-attr="hello {{name}}">` and widget definition
329+
of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect
330+
the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the
331+
`localName` property on the widget scope. The `name` is read from the parent scope (not
332+
component scope).
333+
334+
* `=` or `=expression` - set up bi-directional binding between a local scope property and the
335+
parent scope property. If no `attr` name is specified then the local name and attribute
336+
name are same. Given `<widget my-attr="parentModel">` and widget definition of
337+
`scope: { localModel:'=myAttr' }`, then widget scope property `localName` will reflect the
338+
value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
339+
in `localModel` and any changes in `localModel` will reflect in `parentModel`.
340+
341+
* `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
342+
If no `attr` name is specified then the local name and attribute name are same.
343+
Given `<widget my-attr="count = count + value">` and widget definition of
344+
`scope: { localFn:'increment()' }`, then isolate scope property `localFn` will point to
345+
a function wrapper for the `increment()` expression. Often it's desirable to pass data from
346+
the isolate scope via an expression and to the parent scope, this can be done by passing a
347+
map of local variable names and values into the expression wrapper fn. For example if the
348+
expression is `increment(amount)` then we can specify the amount value by calling the
349+
`localFn` as `localFn({amount: 22})`.
352350

353351
* `controller` - Controller constructor function. The controller is instantiated before the
354352
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:
369367
* `^` - Look for the controller on parent elements as well.
370368

371369

372-
* `inject` (object hash) - Specifies a way to inject bindings into a controller. Injection
373-
definition is a hash of normalized element attribute names to their corresponding binding
374-
strategy. Valid binding strategies are:
375-
376-
* `attribute` - inject attribute value. <br/>
377-
Given `<widget my-attr='abc'>` and widget definition of `inject: {myAttr:'attribute'}`, then
378-
`myAttr` will inject `"abc"`.
379-
380-
* `evaluate` - inject one time evaluation of expression stored in the attribute. <br/>
381-
Given `<widget my-attr='name'>` and widget definition of `inject: {myAttr:'evaluate'}`, and
382-
parent scope `{name:'angular'}` then `myAttr` will inject `"angular"`.
383-
384-
* `accessor` - inject a getter/setter function for the expression in the widget element
385-
attribute to the widget scope. <br/>
386-
Given `<widget my-attr='name'>` and widget definition of `inject: {myAttr:'prop'}`, and
387-
parent scope `{name:'angular'}` then injecting `myAttr` will inject a function such
388-
that `myAttr()` will return `"angular"` and `myAttr('new value')` will update the parent
389-
scope `name` property. This is usefull for treating the element as a data-model for
390-
reading/writing.
391-
392-
* `expression` - Inject expression function. <br/>
393-
Given `<widget my-attr='doSomething()'>` and widget definition of
394-
`inject: {myAttr:'expression'}`, and parent scope `{doSomething:function() {}}` then
395-
injecting `myAttr` will inject a function which when called will execute the expression
396-
against the parent scope.
397-
398370
* `restrict` - String of subset of `EACM` which restricts the directive to a specific directive
399371
declaration style. If omitted directives are allowed on attributes only.
400372

@@ -649,9 +621,9 @@ Following is an example of building a reusable widget.
649621
// This HTML will replace the zippy directive.
650622
replace: true,
651623
transclude: true,
652-
scope: { zippyTitle:'bind' },
624+
scope: { title:'@zippyTitle' },
653625
template: '<div>' +
654-
'<div class="title">{{zippyTitle}}</div>' +
626+
'<div class="title">{{title}}</div>' +
655627
'<div class="body" ng-transclude></div>' +
656628
'</div>',
657629
// The linking function will add behavior to the template

src/ng/compile.js

+68-53
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
*/
1919

2020

21+
var NON_ASSIGNABLE_MODEL_EXPRESSION = 'Non-assignable model expression: ';
22+
23+
2124
/**
2225
* @ngdoc function
2326
* @name angular.module.ng.$compile
@@ -225,47 +228,6 @@ function $CompileProvider($provide) {
225228
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
226229
$controller, $rootScope) {
227230

228-
var LOCAL_MODE = {
229-
attribute: function(localName, mode, parentScope, scope, attr) {
230-
scope[localName] = attr[localName];
231-
},
232-
233-
evaluate: function(localName, mode, parentScope, scope, attr) {
234-
scope[localName] = parentScope.$eval(attr[localName]);
235-
},
236-
237-
bind: function(localName, mode, parentScope, scope, attr) {
238-
var getter = $interpolate(attr[localName]);
239-
scope.$watch(
240-
function() { return getter(parentScope); },
241-
function(v) { scope[localName] = v; }
242-
);
243-
},
244-
245-
accessor: function(localName, mode, parentScope, scope, attr) {
246-
var getter = noop,
247-
setter = noop,
248-
exp = attr[localName];
249-
250-
if (exp) {
251-
getter = $parse(exp);
252-
setter = getter.assign || function() {
253-
throw Error("Expression '" + exp + "' not assignable.");
254-
};
255-
}
256-
257-
scope[localName] = function(value) {
258-
return arguments.length ? setter(parentScope, value) : getter(parentScope);
259-
};
260-
},
261-
262-
expression: function(localName, mode, parentScope, scope, attr) {
263-
scope[localName] = function(locals) {
264-
$parse(attr[localName])(parentScope, locals);
265-
};
266-
}
267-
};
268-
269231
var Attributes = function(element, attr) {
270232
this.$$element = element;
271233
this.$attr = attr || {};
@@ -746,9 +708,67 @@ function $CompileProvider($provide) {
746708
$element = attrs.$$element;
747709

748710
if (newScopeDirective && isObject(newScopeDirective.scope)) {
749-
forEach(newScopeDirective.scope, function(mode, name) {
750-
(LOCAL_MODE[mode] || wrongMode)(name, mode,
751-
scope.$parent || scope, scope, attrs);
711+
var LOCAL_REGEXP = /^\s*([@=&])\s*(\w*)\s*$/;
712+
713+
var parentScope = scope.$parent || scope;
714+
715+
forEach(newScopeDirective.scope, function(definiton, scopeName) {
716+
var match = definiton.match(LOCAL_REGEXP) || [],
717+
attrName = match[2]|| scopeName,
718+
mode = match[1], // @, =, or &
719+
lastValue,
720+
parentGet, parentSet;
721+
722+
switch (mode) {
723+
724+
case '@': {
725+
attrs.$observe(attrName, function(value) {
726+
scope[scopeName] = value;
727+
});
728+
attrs.$$observers[attrName].$$scope = parentScope;
729+
break;
730+
}
731+
732+
case '=': {
733+
parentGet = $parse(attrs[attrName]);
734+
parentSet = parentGet.assign || function() {
735+
// reset the change, or we will throw this exception on every $digest
736+
lastValue = scope[scopeName] = parentGet(parentScope);
737+
throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + attrs[attrName] +
738+
' (directive: ' + newScopeDirective.name + ')');
739+
};
740+
lastValue = scope[scopeName] = parentGet(parentScope);
741+
scope.$watch(function() {
742+
var parentValue = parentGet(parentScope);
743+
744+
if (parentValue !== scope[scopeName]) {
745+
// we are out of sync and need to copy
746+
if (parentValue !== lastValue) {
747+
// parent changed and it has precedence
748+
lastValue = scope[scopeName] = parentValue;
749+
} else {
750+
// if the parent can be assigned then do so
751+
parentSet(parentScope, lastValue = scope[scopeName]);
752+
}
753+
}
754+
return parentValue;
755+
});
756+
break;
757+
}
758+
759+
case '&': {
760+
parentGet = $parse(attrs[attrName]);
761+
scope[scopeName] = function(locals) {
762+
return parentGet(parentScope, locals);
763+
}
764+
break;
765+
}
766+
767+
default: {
768+
throw Error('Invalid isolate scope definition for directive ' +
769+
newScopeDirective.name + ': ' + definiton);
770+
}
771+
}
752772
});
753773
}
754774

@@ -761,12 +781,6 @@ function $CompileProvider($provide) {
761781
$transclude: boundTranscludeFn
762782
};
763783

764-
765-
forEach(directive.inject || {}, function(mode, name) {
766-
(LOCAL_MODE[mode] || wrongMode)(name, mode,
767-
newScopeDirective ? scope.$parent || scope : scope, locals, attrs);
768-
});
769-
770784
controller = directive.controller;
771785
if (controller == '@') {
772786
controller = attrs[directive.name];
@@ -1007,9 +1021,10 @@ function $CompileProvider($provide) {
10071021

10081022
attr[name] = undefined;
10091023
($$observers[name] || ($$observers[name] = [])).$$inter = true;
1010-
scope.$watch(interpolateFn, function(value) {
1011-
attr.$set(name, value);
1012-
});
1024+
(attr.$$observers && attr.$$observers[name].$$scope || scope).
1025+
$watch(interpolateFn, function(value) {
1026+
attr.$set(name, value);
1027+
});
10131028
})
10141029
});
10151030
}

src/ng/directive/input.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -857,8 +857,8 @@ var VALID_CLASS = 'ng-valid',
857857
* </example>
858858
*
859859
*/
860-
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$element',
861-
function($scope, $exceptionHandler, $attr, ngModel, $element) {
860+
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
861+
function($scope, $exceptionHandler, $attr, $element, $parse) {
862862
this.$viewValue = Number.NaN;
863863
this.$modelValue = Number.NaN;
864864
this.$parsers = [];
@@ -870,6 +870,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
870870
this.$invalid = false;
871871
this.$name = $attr.name;
872872

873+
var ngModelGet = $parse($attr.ngModel),
874+
ngModelSet = ngModelGet.assign;
875+
876+
if (!ngModelSet) {
877+
throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + $attr.ngModel +
878+
' (' + startingTag($element) + ')');
879+
}
880+
873881
/**
874882
* @ngdoc function
875883
* @name angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$render
@@ -974,7 +982,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
974982

975983
if (this.$modelValue !== value) {
976984
this.$modelValue = value;
977-
ngModel(value);
985+
ngModelSet($scope, value);
978986
forEach(this.$viewChangeListeners, function(listener) {
979987
try {
980988
listener();
@@ -987,9 +995,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
987995

988996
// model -> value
989997
var ctrl = this;
990-
$scope.$watch(function() {
991-
return ngModel();
992-
}, function(value) {
998+
$scope.$watch(ngModelGet, function(value) {
993999

9941000
// ignore change from view
9951001
if (ctrl.$modelValue === value) return;
@@ -1044,9 +1050,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e
10441050
*/
10451051
var ngModelDirective = function() {
10461052
return {
1047-
inject: {
1048-
ngModel: 'accessor'
1049-
},
10501053
require: ['ngModel', '^?form'],
10511054
controller: NgModelController,
10521055
link: function(scope, element, attr, ctrls) {

0 commit comments

Comments
 (0)