From 006fd2ca252400e87a419b929e00ea0277ff86ad Mon Sep 17 00:00:00 2001
From: Misko Hevery
Date: Tue, 21 Sep 2010 19:20:34 +0200
Subject: HEAD is now at 10c0151 Fixes on issue when a SELECT has OPTION which
are data bound (ie OPTION has repeater or OPTION.value is bound), then SELECT
does not update to match the correct OPTION after the change in model (ie
after the OPTION repeater unrolls or OPTION.value is changed.)
---
src/Angular.js | 6 +++---
src/Compiler.js | 4 ++--
src/Scope.js | 40 ++++++++++++++++++++++++++++++++--------
src/directives.js | 2 ++
src/widgets.js | 14 +++++++++++++-
test/ScopeSpec.js | 23 +++++++++++++++++++++++
test/testabilityPatch.js | 3 ++-
test/widgetsSpec.js | 32 +++++++++++++++++++++++++++++---
8 files changed, 106 insertions(+), 18 deletions(-)
diff --git a/src/Angular.js b/src/Angular.js
index 7ea43c98..ef1187f2 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -124,9 +124,9 @@ function jqLiteWrap(element) {
}
function isUndefined(value){ return typeof value == $undefined; }
function isDefined(value){ return typeof value != $undefined; }
-function isObject(value){ return value!=_null && typeof value == 'object';}
-function isString(value){ return typeof value == 'string';}
-function isNumber(value){ return typeof value == 'number';}
+function isObject(value){ return value!=_null && typeof value == $object;}
+function isString(value){ return typeof value == $string;}
+function isNumber(value){ return typeof value == $number;}
function isArray(value) { return value instanceof Array; }
function isFunction(value){ return typeof value == $function;}
function isTextNode(node) { return nodeName(node) == '#text'; }
diff --git a/src/Compiler.js b/src/Compiler.js
index c45dd46e..6252e0c4 100644
--- a/src/Compiler.js
+++ b/src/Compiler.js
@@ -8,7 +8,7 @@ function Template(priority) {
this.paths = [];
this.children = [];
this.inits = [];
- this.priority = priority || 0;
+ this.priority = priority;
}
Template.prototype = {
@@ -130,7 +130,7 @@ Compiler.prototype = {
priority = priority || 0;
}
if (isString(priority)) {
- priority = PRIORITY[uppercase(priority)] || 0;
+ priority = PRIORITY[uppercase(priority)] || parseInt(priority);
}
template = new Template(priority);
eachAttribute(element, function(value, name){
diff --git a/src/Scope.js b/src/Scope.js
index c2a4f098..53228a0d 100644
--- a/src/Scope.js
+++ b/src/Scope.js
@@ -22,7 +22,7 @@ function getter(instance, path, unboundFn) {
}
}
}
- if (!unboundFn && isFunction(instance) && !instance['$$factory']) {
+ if (!unboundFn && isFunction(instance)) {
return bind(lastInstance, instance);
}
return instance;
@@ -113,12 +113,13 @@ function createScope(parent, services, existing) {
function API(){}
function Behavior(){}
- var instance, behavior, api, evalLists = {sorted:[]}, servicesCache = extend({}, existing);
-
parent = Parent.prototype = (parent || {});
- api = API.prototype = new Parent();
- behavior = Behavior.prototype = new API();
- instance = new Behavior();
+ var evalLists = {sorted:[]};
+ var postList = [], postHash = {}, postId = 0;
+ var servicesCache = extend({}, existing);
+ var api = API.prototype = new Parent();
+ var behavior = Behavior.prototype = new API();
+ var instance = new Behavior();
extend(api, {
'this': instance,
@@ -130,14 +131,23 @@ function createScope(parent, services, existing) {
$eval: function $eval(exp) {
var type = typeof exp;
+ var i, iSize;
+ var j, jSize;
+ var queue;
+ var fn;
if (type == $undefined) {
- for ( var i = 0, iSize = evalLists.sorted.length; i < iSize; i++) {
- for ( var queue = evalLists.sorted[i],
+ for ( i = 0, iSize = evalLists.sorted.length; i < iSize; i++) {
+ for ( queue = evalLists.sorted[i],
jSize = queue.length,
j= 0; j < jSize; j++) {
instance.$tryEval(queue[j].fn, queue[j].handler);
}
}
+ while(postList.length) {
+ fn = postList.shift();
+ delete postHash[fn.$postEvalId];
+ instance.$tryEval(fn);
+ }
} else if (type === $function) {
return exp.call(instance);
} else if (type === 'string') {
@@ -202,6 +212,20 @@ function createScope(parent, services, existing) {
});
},
+ $postEval: function(expr) {
+ if (expr) {
+ var fn = expressionCompile(expr);
+ var id = fn.$postEvalId;
+ if (!id) {
+ id = '$' + instance.$id + "_" + (postId++);
+ fn.$postEvalId = id;
+ }
+ if (!postHash[id]) {
+ postList.push(postHash[id] = fn);
+ }
+ }
+ },
+
$become: function(Class) {
// remove existing
foreach(behavior, function(value, key){ delete behavior[key]; });
diff --git a/src/directives.js b/src/directives.js
index 994ef90e..69648e31 100644
--- a/src/directives.js
+++ b/src/directives.js
@@ -115,6 +115,7 @@ var REMOVE_ATTRIBUTES = {
angularDirective("ng:bind-attr", function(expression){
return function(element){
var lastValue = {};
+ var updateFn = element.parent().data('$update');
this.$onEval(function(){
var values = this.$eval(expression);
for(var key in values) {
@@ -132,6 +133,7 @@ angularDirective("ng:bind-attr", function(expression){
} else {
element.attr(key, value);
}
+ this.$postEval(updateFn);
}
}
}, element);
diff --git a/src/widgets.js b/src/widgets.js
index b70c4dcb..fbca8436 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -209,7 +209,11 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn) {
event.preventDefault();
});
}
- view.set(lastValue = model.get());
+ function updateView(){
+ view.set(lastValue = model.get());
+ }
+ updateView();
+ element.data('$update', updateView);
scope.$watch(model.get, function(value){
if (lastValue !== value) {
view.set(lastValue = value);
@@ -231,6 +235,14 @@ angularWidget('select', function(element){
return inputWidgetSelector.call(this, element);
});
+angularWidget('option', function(){
+ this.descend(true);
+ this.directives(true);
+ return function(element) {
+ this.$postEval(element.parent().data('$update'));
+ };
+});
+
angularWidget('ng:include', function(element){
var compiler = this,
diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js
index ea63fea4..66e9d489 100644
--- a/test/ScopeSpec.js
+++ b/test/ScopeSpec.js
@@ -192,4 +192,27 @@ describe('scope/model', function(){
});
});
+
+ describe('$postEval', function(){
+ it('should eval function once and last', function(){
+ var log = '';
+ var scope = createScope();
+ function onceOnly(){log+= '@';}
+ scope.$onEval(function(){log+= '.';});
+ scope.$postEval(function(){log+= '!';});
+ scope.$postEval(onceOnly);
+ scope.$postEval(onceOnly);
+ scope.$postEval(); // ignore
+ scope.$eval();
+ expect(log).toEqual('.!@');
+ scope.$eval();
+ expect(log).toEqual('.!@.');
+
+ scope.$postEval(onceOnly);
+ scope.$postEval(onceOnly);
+ scope.$eval();
+ expect(log).toEqual('.!@..@');
+ });
+ });
+
});
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index bbe2876e..955dccfa 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -59,7 +59,7 @@ extend(angular, {
function sortedHtml(element) {
var html = "";
- foreach(element, function toString(node) {
+ foreach(jqLite(element), function toString(node) {
if (node.nodeName == "#text") {
html += escapeHtml(node.nodeValue);
} else {
@@ -188,6 +188,7 @@ function click(element) {
}
}
if (name == 'option') {
+ element.parent().val(element.val());
JQLite.prototype.trigger.call(element.parent(), 'change');
} else {
JQLite.prototype.trigger.call(element, 'click');
diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js
index 3861ef4f..d113e6ee 100644
--- a/test/widgetsSpec.js
+++ b/test/widgetsSpec.js
@@ -355,9 +355,9 @@ describe("widget", function(){
it('should honor the value field in option', function(){
compile(
'');
- // childNodes[0] is repeater
+ // childNodes[0] is repeater comment
expect(scope.selection).toEqual(undefined);
click(element[0].childNodes[1]);
@@ -367,6 +367,32 @@ describe("widget", function(){
scope.$eval();
expect(element[0].childNodes[2].selected).toEqual(true);
});
+
+ it('should unroll select options before eval', function(){
+ compile(
+ '');
+ scope.selection = 1;
+ scope.options = ['one', 'two'];
+ scope.$eval();
+ expect(element[0].value).toEqual('1');
+ expect(element.hasClass(NG_VALIDATION_ERROR)).toEqual(false);
+ });
+
+ it('should update select when value changes', function(){
+ compile(
+ '');
+ scope.selection = 'B';
+ scope.$eval();
+ expect(element[0].childNodes[1].selected).toEqual(false);
+ scope.value = 'B';
+ scope.$eval();
+ expect(element[0].childNodes[1].selected).toEqual(true);
+ });
});
it('should support type="select-multiple"', function(){
@@ -468,7 +494,7 @@ describe("widget", function(){
scope.url = undefined;
scope.$eval();
-
+
expect(element.text()).toEqual('');
});
});
--
cgit v1.2.3