Closes #170. Corrected the behavior of select when options are ng:repeated

- Delete $postEval method, as it was a hack
This commit is contained in:
Misko Hevery
2010-12-01 20:29:54 -08:00
parent 41d5938883
commit 5a8ad8fe32
19 changed files with 328 additions and 229 deletions

28
regression/issue-170.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE HTML>
<html xmlns:ng="http://angularjs.org">
<head>
<script type="text/javascript" src="../lib/jquery/jquery-1.4.2.js"></script>
<script type="text/javascript" src="../src/angular-bootstrap.js" ng:autobind></script>
<head>
<body>
<select name='selection0' style="display:block;">
<option ng:repeat='value in ["FOO","BAR"]'">{{value}}</option>
</select>
{{selection0}} &lt;-- FOO should be shown here
<hr/>
<select ng:init="selection1='ignore'" name='selection1' style="display:block;">
<option ng:repeat='value in ["FOO","BAR"]' ng:bind-attr="{selected:'{{value==\'BAR\'}}'}">{{value}}</option>
</select>
{{selection1}} &lt;-- BAR should be shown here
<hr/>
<select ng:init="selection2=1" name="selection2" style="display:block;">
<option value="{{$index}}" ng:repeat="opt in ['zero', 'one']">{{opt}}</option>
</select>
{{selection2}} &lt;-- 1 should be shown here
</body>
</html>

View File

@@ -53,6 +53,9 @@ function fromCharCode(code) { return String.fromCharCode(code); }
var _undefined = undefined,
_null = null,
$$element = '$element',
$$update = '$update',
$$scope = '$scope',
$$validate = '$validate',
$angular = 'angular',
$array = 'array',
$boolean = 'boolean',
@@ -69,6 +72,8 @@ var _undefined = undefined,
$number = 'number',
$object = 'object',
$string = 'string',
$value = 'value',
$selected = 'selected',
$undefined = 'undefined',
NG_EXCEPTION = 'ng-exception',
NG_VALIDATION_ERROR = 'ng-validation-error',

View File

@@ -30,6 +30,7 @@ Template.prototype = {
if (this.newScope) {
childScope = createScope(scope);
scope.$onEval(childScope.$eval);
element.data($$scope, childScope);
}
foreach(this.inits, function(fn) {
queue.push(function() {
@@ -68,6 +69,17 @@ Template.prototype = {
}
};
/*
* Function walks up the element chain looking for the scope associated with the give element.
*/
function retrieveScope(element) {
var scope;
while (element && !(scope = element.data($$scope))) {
element = element.parent();
}
return scope;
}
///////////////////////////////////
//Compiler
//////////////////////////////////
@@ -97,6 +109,7 @@ Compiler.prototype = {
element = jqLite(element);
var scope = parentScope && parentScope.$eval ?
parentScope : createScope(parentScope);
element.data($$scope, scope);
return extend(scope, {
$element:element,
$init: function() {

View File

@@ -243,7 +243,6 @@ function createScope(parent, providers, instanceCache) {
parent = Parent.prototype = (parent || {});
var instance = new Parent();
var evalLists = {sorted:[]};
var postList = [], postHash = {}, postId = 0;
extend(instance, {
'this': instance,
@@ -371,11 +370,6 @@ function createScope(parent, providers, instanceCache) {
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') {
@@ -549,27 +543,6 @@ function createScope(parent, providers, instanceCache) {
});
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$postEval
* @function
*/
$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);
}
}
},
/**
* @workInProgress
* @ngdoc function

View File

@@ -304,7 +304,8 @@ angularDirective("ng:bind-template", function(expression, element){
var REMOVE_ATTRIBUTES = {
'disabled':'disabled',
'readonly':'readOnly',
'checked':'checked'
'checked':'checked',
'selected':'selected'
};
/**
* @workInProgress
@@ -359,27 +360,31 @@ var REMOVE_ATTRIBUTES = {
angularDirective("ng:bind-attr", function(expression){
return function(element){
var lastValue = {};
var updateFn = element.parent().data('$update');
var updateFn = element.data($$update) || noop;
this.$onEval(function(){
var values = this.$eval(expression);
var values = this.$eval(expression),
dirty = noop;
for(var key in values) {
var value = compileBindTemplate(values[key]).call(this, element),
specialName = REMOVE_ATTRIBUTES[lowercase(key)];
if (lastValue[key] !== value) {
lastValue[key] = value;
if (specialName) {
if (element[specialName] = toBoolean(value)) {
element.attr(specialName, value);
if (toBoolean(value)) {
element.attr(specialName, specialName);
element.attr('ng-' + specialName, value);
} else {
element.removeAttr(key);
element.removeAttr(specialName);
element.removeAttr('ng-' + specialName);
}
(element.data('$validate')||noop)();
(element.data($$validate)||noop)();
} else {
element.attr(key, value);
}
this.$postEval(updateFn);
dirty = updateFn;
}
}
dirty();
}, element);
};
});

View File

@@ -394,7 +394,7 @@ extend(angularValidator, {
element.removeClass('ng-input-indicator-wait');
scope.$invalidWidgets.markValid(element);
}
element.data('$validate')();
element.data($$validate)();
scope.$root.$eval();
});
} else if (inputState.inFlight) {

View File

@@ -282,7 +282,7 @@ function valueAccessor(scope, element) {
required = requiredExpr === '';
}
element.data('$validate', validate);
element.data($$validate, validate);
return {
get: function(){
if (lastError)
@@ -391,6 +391,7 @@ var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initW
// 'file': fileWidget???
};
function initWidgetValue(initValue) {
return function (model, view) {
var value = view.get();
@@ -461,18 +462,13 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn) {
this.$eval(element.attr('ng:init')||'');
// Don't register a handler if we are a button (noopAccessor) and there is no action
if (action || modelAccessor !== noopAccessor) {
element.bind(events, function(event){
element.bind(events, function (){
model.set(view.get());
lastValue = model.get();
scope.$tryEval(action, element);
scope.$root.$eval();
});
}
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);
@@ -494,15 +490,50 @@ angularWidget('select', function(element){
return inputWidgetSelector.call(this, element);
});
/*
* Consider this:
* <select name="selection">
* <option ng:repeat="x in [1,2]">{{x}}</option>
* </select>
*
* The issue is that the select gets evaluated before option is unrolled.
* This means that the selection is undefined, but the browser
* default behavior is to show the top selection in the list.
* To fix that we register a $update function on the select element
* and the option creation then calls the $update function when it is
* unrolled. The $update function then calls this update function, which
* then tries to determine if the model is unassigned, and if so it tries to
* chose one of the options from the list.
*/
angularWidget('option', function(){
this.descend(true);
this.directives(true);
return function(element) {
this.$postEval(element.parent().data('$update'));
var select = element.parent();
var scope = retrieveScope(select);
var model = modelFormattedAccessor(scope, select);
var view = valueAccessor(scope, select);
var option = element;
var lastValue = option.attr($value);
var lastSelected = option.attr('ng-' + $selected);
element.data($$update, function(){
var value = option.attr($value);
var selected = option.attr('ng-' + $selected);
var modelValue = model.get();
if (lastSelected != selected || lastValue != value) {
lastSelected = selected;
lastValue = value;
if (selected || modelValue == _null || modelValue == _undefined)
model.set(value);
if (value == modelValue) {
view.set(lastValue);
}
}
});
};
});
/**
* @workInProgress
* @ngdoc widget

View File

@@ -10,6 +10,7 @@ describe('Angular', function(){
scope.$init();
scope.$eval();
expect(onUpdateView).wasCalled();
dealoc(scope);
});
});

View File

@@ -5,6 +5,7 @@ BinderTest.prototype.setUp = function(){
this.compile = function(html, initialScope, parent) {
var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget);
if (self.element) dealoc(self.element);
var element = self.element = jqLite(html);
var scope = compiler.compile(element)(element);

View File

@@ -1,5 +1,5 @@
describe('compiler', function(){
var compiler, markup, directives, widgets, compile, log;
var compiler, markup, directives, widgets, compile, log, scope;
beforeEach(function(){
log = "";
@@ -32,6 +32,10 @@ describe('compiler', function(){
return scope;
};
});
afterEach(function(){
dealoc(scope);
});
it('should recognize a directive', function(){
var e = jqLite('<div directive="expr" ignore="me"></div>');
@@ -44,7 +48,8 @@ describe('compiler', function(){
};
};
var template = compiler.compile(e);
var init = template(e).$init;
scope = template(e);
var init = scope.$init;
expect(log).toEqual("found");
init();
expect(e.hasClass('ng-directive')).toEqual(true);
@@ -52,12 +57,12 @@ describe('compiler', function(){
});
it('should recurse to children', function(){
var scope = compile('<div><span hello="misko"/></div>');
scope = compile('<div><span hello="misko"/></div>');
expect(log).toEqual("hello misko");
});
it('should watch scope', function(){
var scope = compile('<span watch="name"/>');
scope = compile('<span watch="name"/>');
expect(log).toEqual("");
scope.$eval();
scope.$set('name', 'misko');
@@ -71,7 +76,7 @@ describe('compiler', function(){
it('should prevent descend', function(){
directives.stop = function(){ this.descend(false); };
var scope = compile('<span hello="misko" stop="true"><span hello="adam"/></span>');
scope = compile('<span hello="misko" stop="true"><span hello="adam"/></span>');
expect(log).toEqual("hello misko");
});
@@ -87,7 +92,7 @@ describe('compiler', function(){
});
};
};
var scope = compile('before<span duplicate="expr">x</span>after');
scope = compile('before<span duplicate="expr">x</span>after');
expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span>after</div>');
scope.$eval();
expect(sortedHtml(scope.$element)).toEqual('<div>before<#comment></#comment><span>x</span><span>x</span>after</div>');
@@ -103,7 +108,7 @@ describe('compiler', function(){
textNode[0].nodeValue = 'replaced';
}
});
var scope = compile('before<span>middle</span>after');
scope = compile('before<span>middle</span>after');
expect(sortedHtml(scope.$element[0], true)).toEqual('<div>before<span class="ng-directive" hello="middle">replaced</span>after</div>');
expect(log).toEqual("hello middle");
});
@@ -116,7 +121,7 @@ describe('compiler', function(){
log += 'init';
};
};
var scope = compile('<ng:button>push me</ng:button>');
scope = compile('<ng:button>push me</ng:button>');
expect(lowercase(scope.$element[0].innerHTML)).toEqual('<div>button</div>');
expect(log).toEqual('init');
});
@@ -135,7 +140,7 @@ describe('compiler', function(){
if (text == '{{1+2}}')
parent.text('3');
});
var scope = compile('<div><h1>ignore me</h1></div>');
scope = compile('<div><h1>ignore me</h1></div>');
expect(scope.$element.text()).toEqual('3');
});
@@ -158,7 +163,7 @@ describe('compiler', function(){
textNode.remove();
}
});
var scope = compile('A---B---C===D');
scope = compile('A---B---C===D');
expect(sortedHtml(scope.$element)).toEqual('<div>A<hr></hr>B<hr></hr>C<p></p>D</div>');
});

View File

@@ -169,6 +169,7 @@ describe("resource", function() {
var person = Person.get({id:123});
$browser.xhr.flush();
expect(person.name).toEqual('misko');
dealoc(scope);
});
it('should return the same object when verifying the cache', function(){
@@ -188,6 +189,7 @@ describe("resource", function() {
$browser.xhr.flush();
expect(person2Cache).toEqual(person2);
expect(person2[0].name).toEqual('rob');
dealoc(scope);
});
describe('failure mode', function(){

View File

@@ -1,52 +1,64 @@
describe("ScenarioSpec: Compilation", function(){
it("should compile dom node and return scope", function(){
var node = jqLite('<div ng:init="a=1">{{b=a+1}}</div>')[0];
var scope = compile(node);
scope.$init();
expect(scope.a).toEqual(1);
expect(scope.b).toEqual(2);
var scope;
beforeEach(function(){
scope = null;
});
it("should compile jQuery node and return scope", function(){
var scope = compile(jqLite('<div>{{a=123}}</div>')).$init();
expect(jqLite(scope.$element).text()).toEqual('123');
afterEach(function(){
dealoc(scope);
});
it("should compile text node and return scope", function(){
var scope = compile('<div>{{a=123}}</div>').$init();
expect(jqLite(scope.$element).text()).toEqual('123');
describe('compilation', function(){
it("should compile dom node and return scope", function(){
var node = jqLite('<div ng:init="a=1">{{b=a+1}}</div>')[0];
scope = compile(node);
scope.$init();
expect(scope.a).toEqual(1);
expect(scope.b).toEqual(2);
});
it("should compile jQuery node and return scope", function(){
scope = compile(jqLite('<div>{{a=123}}</div>')).$init();
expect(jqLite(scope.$element).text()).toEqual('123');
});
it("should compile text node and return scope", function(){
scope = compile('<div>{{a=123}}</div>').$init();
expect(jqLite(scope.$element).text()).toEqual('123');
});
});
});
describe("ScenarioSpec: Scope", function(){
it("should have set, get, eval, $init, updateView methods", function(){
var scope = compile('<div>{{a}}</div>').$init();
scope.$eval("$invalidWidgets.push({})");
expect(scope.$set("a", 2)).toEqual(2);
expect(scope.$get("a")).toEqual(2);
expect(scope.$eval("a=3")).toEqual(3);
scope.$eval();
expect(jqLite(scope.$element).text()).toEqual('3');
describe('scope', function(){
it("should have set, get, eval, $init, updateView methods", function(){
scope = compile('<div>{{a}}</div>').$init();
scope.$eval("$invalidWidgets.push({})");
expect(scope.$set("a", 2)).toEqual(2);
expect(scope.$get("a")).toEqual(2);
expect(scope.$eval("a=3")).toEqual(3);
scope.$eval();
expect(jqLite(scope.$element).text()).toEqual('3');
});
it("should have $ objects", function(){
scope = compile('<div></div>', {$config: {a:"b"}});
expect(scope.$get('$location')).toBeDefined();
expect(scope.$get('$eval')).toBeDefined();
expect(scope.$get('$config')).toBeDefined();
expect(scope.$get('$config.a')).toEqual("b");
});
});
it("should have $ objects", function(){
var scope = compile('<div></div>', {$config: {a:"b"}});
expect(scope.$get('$location')).toBeDefined();
expect(scope.$get('$eval')).toBeDefined();
expect(scope.$get('$config')).toBeDefined();
expect(scope.$get('$config.a')).toEqual("b");
describe("configuration", function(){
it("should take location object", function(){
var url = "http://server/#?book=moby";
scope = compile("<div>{{$location}}</div>");
var $location = scope.$location;
var $browser = scope.$inject('$browser');
expect($location.hashSearch.book).toBeUndefined();
$browser.setUrl(url);
$browser.poll();
expect($location.hashSearch.book).toEqual('moby');
});
});
});
describe("ScenarioSpec: configuration", function(){
it("should take location object", function(){
var url = "http://server/#?book=moby";
var scope = compile("<div>{{$location}}</div>");
var $location = scope.$location;
var $browser = scope.$inject('$browser');
expect($location.hashSearch.book).toBeUndefined();
$browser.setUrl(url);
$browser.poll();
expect($location.hashSearch.book).toEqual('moby');
});
});
});

View File

@@ -209,28 +209,6 @@ 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('.!@..@');
});
});
describe('$new', function(){
it('should $new should create new child scope and $become controller', function(){
var parent = createScope(null, {exampleService: function(){return 'Example Service';}});

View File

@@ -104,12 +104,6 @@ describe('Validator:asynchronous', function(){
afterEach(function(){
if (self.$element) self.$element.remove();
var oldCache = jqCache;
jqCache = {};
if (size(oldCache)) {
dump(oldCache);
}
expect(size(oldCache)).toEqual(0);
});
it('should make a request and show spinner', function(){

View File

@@ -13,8 +13,7 @@ describe("directive", function(){
});
afterEach(function() {
if (model && model.$element) model.$element.remove();
expect(size(jqCache)).toEqual(0);
dealoc(model);
});
it("should ng:init", function() {

View File

@@ -14,8 +14,7 @@ describe("markups", function(){
});
afterEach(function(){
if (element) element.remove();
expect(size(jqCache)).toEqual(0);
dealoc(element);
});
it('should translate {{}} in text', function(){
@@ -63,92 +62,91 @@ describe("markups", function(){
compile('<a ng:href="{{url}}" rel="{{rel}}"></a>');
expect(sortedHtml(element)).toEqual('<a ng:bind-attr="{"href":"{{url}}","rel":"{{rel}}"}"></a>');
});
it('should Parse Text With No Bindings', function(){
var parts = parseBindings("a");
assertEquals(parts.length, 1);
assertEquals(parts[0], "a");
assertTrue(!binding(parts[0]));
});
it('should Parse Empty Text', function(){
var parts = parseBindings("");
assertEquals(parts.length, 1);
assertEquals(parts[0], "");
assertTrue(!binding(parts[0]));
});
it('should Parse Inner Binding', function(){
var parts = parseBindings("a{{b}}C");
assertEquals(parts.length, 3);
assertEquals(parts[0], "a");
assertTrue(!binding(parts[0]));
assertEquals(parts[1], "{{b}}");
assertEquals(binding(parts[1]), "b");
assertEquals(parts[2], "C");
assertTrue(!binding(parts[2]));
});
it('should Parse Ending Binding', function(){
var parts = parseBindings("a{{b}}");
assertEquals(parts.length, 2);
assertEquals(parts[0], "a");
assertTrue(!binding(parts[0]));
assertEquals(parts[1], "{{b}}");
assertEquals(binding(parts[1]), "b");
});
it('should Parse Begging Binding', function(){
var parts = parseBindings("{{b}}c");
assertEquals(parts.length, 2);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
assertEquals(parts[1], "c");
assertTrue(!binding(parts[1]));
});
it('should Parse Loan Binding', function(){
var parts = parseBindings("{{b}}");
assertEquals(parts.length, 1);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
});
it('should Parse Two Bindings', function(){
var parts = parseBindings("{{b}}{{c}}");
assertEquals(parts.length, 2);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
assertEquals(parts[1], "{{c}}");
assertEquals(binding(parts[1]), "c");
});
it('should Parse Two Bindings With Text In Middle', function(){
var parts = parseBindings("{{b}}x{{c}}");
assertEquals(parts.length, 3);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
assertEquals(parts[1], "x");
assertTrue(!binding(parts[1]));
assertEquals(parts[2], "{{c}}");
assertEquals(binding(parts[2]), "c");
});
it('should Parse Multiline', function(){
var parts = parseBindings('"X\nY{{A\nB}}C\nD"');
assertTrue(!!binding('{{A\nB}}'));
assertEquals(parts.length, 3);
assertEquals(parts[0], '"X\nY');
assertEquals(parts[1], '{{A\nB}}');
assertEquals(parts[2], 'C\nD"');
});
it('should Has Binding', function(){
assertTrue(hasBindings(parseBindings("{{a}}")));
assertTrue(!hasBindings(parseBindings("a")));
assertTrue(hasBindings(parseBindings("{{b}}x{{c}}")));
});
});
var BindingMarkupTest = TestCase("BindingMarkupTest");
BindingMarkupTest.prototype.testParseTextWithNoBindings = function(){
var parts = parseBindings("a");
assertEquals(parts.length, 1);
assertEquals(parts[0], "a");
assertTrue(!binding(parts[0]));
};
BindingMarkupTest.prototype.testParseEmptyText = function(){
var parts = parseBindings("");
assertEquals(parts.length, 1);
assertEquals(parts[0], "");
assertTrue(!binding(parts[0]));
};
BindingMarkupTest.prototype.testParseInnerBinding = function(){
var parts = parseBindings("a{{b}}c");
assertEquals(parts.length, 3);
assertEquals(parts[0], "a");
assertTrue(!binding(parts[0]));
assertEquals(parts[1], "{{b}}");
assertEquals(binding(parts[1]), "b");
assertEquals(parts[2], "c");
assertTrue(!binding(parts[2]));
};
BindingMarkupTest.prototype.testParseEndingBinding = function(){
var parts = parseBindings("a{{b}}");
assertEquals(parts.length, 2);
assertEquals(parts[0], "a");
assertTrue(!binding(parts[0]));
assertEquals(parts[1], "{{b}}");
assertEquals(binding(parts[1]), "b");
};
BindingMarkupTest.prototype.testParseBeggingBinding = function(){
var parts = parseBindings("{{b}}c");
assertEquals(parts.length, 2);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
assertEquals(parts[1], "c");
assertTrue(!binding(parts[1]));
};
BindingMarkupTest.prototype.testParseLoanBinding = function(){
var parts = parseBindings("{{b}}");
assertEquals(parts.length, 1);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
};
BindingMarkupTest.prototype.testParseTwoBindings = function(){
var parts = parseBindings("{{b}}{{c}}");
assertEquals(parts.length, 2);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
assertEquals(parts[1], "{{c}}");
assertEquals(binding(parts[1]), "c");
};
BindingMarkupTest.prototype.testParseTwoBindingsWithTextInMiddle = function(){
var parts = parseBindings("{{b}}x{{c}}");
assertEquals(parts.length, 3);
assertEquals(parts[0], "{{b}}");
assertEquals(binding(parts[0]), "b");
assertEquals(parts[1], "x");
assertTrue(!binding(parts[1]));
assertEquals(parts[2], "{{c}}");
assertEquals(binding(parts[2]), "c");
};
BindingMarkupTest.prototype.testParseMultiline = function(){
var parts = parseBindings('"X\nY{{A\nB}}C\nD"');
assertTrue(!!binding('{{A\nB}}'));
assertEquals(parts.length, 3);
assertEquals(parts[0], '"X\nY');
assertEquals(parts[1], '{{A\nB}}');
assertEquals(parts[2], 'C\nD"');
};
BindingMarkupTest.prototype.testHasBinding = function(){
assertTrue(hasBindings(parseBindings("{{a}}")));
assertTrue(!hasBindings(parseBindings("a")));
assertTrue(hasBindings(parseBindings("{{b}}x{{c}}")));
};

View File

@@ -17,8 +17,7 @@ describe("service", function(){
});
afterEach(function(){
if (scope && scope.$element)
scope.$element.remove();
dealoc(scope);
});
@@ -202,7 +201,7 @@ describe("service", function(){
});
it('should update hash before any processing', function(){
var scope = compile('<div>');
scope = compile('<div>');
var log = '';
scope.$watch('$location.hash', function(){
log += this.$location.hashPath + ';';
@@ -259,7 +258,7 @@ describe("service", function(){
describe("$invalidWidgets", function(){
it("should count number of invalid widgets", function(){
var scope = compile('<input name="price" ng:required ng:validate="number"></input>');
scope = compile('<input name="price" ng:required ng:validate="number"></input>');
jqLite(document.body).append(scope.$element);
scope.$init();
expect(scope.$invalidWidgets.length).toEqual(1);
@@ -291,8 +290,8 @@ describe("service", function(){
function BookChapter() {
this.log = '<init>';
}
var scope = compile('<div></div>').$init();
var $route = scope.$inject('$route');
scope = compile('<div></div>').$init();
$route = scope.$inject('$route');
$route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'});
$route.when('/Blank');
$route.onChange(function(){

View File

@@ -50,6 +50,25 @@ beforeEach(function(){
});
});
afterEach(clearJqCache);
function clearJqCache(){
var count = 0;
foreachSorted(jqCache, function(value, key){
count ++;
delete jqCache[key];
foreach(value, function(value, key){
if (value.$element)
dump(key, sortedHtml(value.$element));
else
dump(key, toJson(value));
});
});
if (count) {
fail('Found jqCache references that were not deallocated!');
}
}
function nakedExpect(obj) {
return expect(angular.fromJson(angular.toJson(obj)));
}
@@ -58,6 +77,11 @@ function childNode(element, index) {
return jqLite(element[0].childNodes[index]);
}
function dealoc(obj) {
var element = (obj||{}).$element || obj;
if (element && element.dealoc) element.dealoc();
}
extend(angular, {
'element': jqLite,
'compile': compile,

View File

@@ -16,8 +16,7 @@ describe("widget", function(){
});
afterEach(function(){
if (element && element.dealoc) element.dealoc();
expect(size(jqCache)).toEqual(0);
dealoc(element);
});
describe("input", function(){
@@ -362,7 +361,7 @@ describe("widget", function(){
'<option value="{{$index}}" ng:repeat="name in [\'A\', \'B\', \'C\']">{{name}}</option>' +
'</select>');
// childNodes[0] is repeater comment
expect(scope.selection).toEqual(undefined);
expect(scope.selection).toEqual(0);
browserTrigger(element[0].childNodes[2], 'change');
expect(scope.selection).toEqual(1);
@@ -398,6 +397,32 @@ describe("widget", function(){
scope.$eval();
expect(element[0].childNodes[1].selected).toEqual(true);
});
it('should select default option on repeater', function(){
compile(
'<select name="selection">' +
'<option ng:repeat="no in [1,2]">{{no}}</option>' +
'</select>');
expect(scope.selection).toEqual('1');
});
it('should select selected option on repeater', function(){
compile(
'<select name="selection">' +
'<option ng:repeat="no in [1,2]">{{no}}</option>' +
'<option selected>ABC</option>' +
'</select>');
expect(scope.selection).toEqual('ABC');
});
it('should select dynamically selected option on repeater', function(){
compile(
'<select name="selection">' +
'<option ng:repeat="no in [1,2]" ng:bind-attr="{selected:\'{{no==2}}\'}">{{no}}</option>' +
'</select>');
expect(scope.selection).toEqual('2');
});
});
it('should support type="select-multiple"', function(){
@@ -476,6 +501,7 @@ describe("widget", function(){
scope.url = '/Book/Moby';
scope.$init();
expect(scope.$element.text()).toEqual('Moby');
dealoc(scope);
});
it("should match sandwich ids", function(){
@@ -491,6 +517,7 @@ describe("widget", function(){
scope.$init();
expect(scope.name).toEqual(undefined);
expect(scope.$element.text()).toEqual('works');
dealoc(scope);
});
});
@@ -504,6 +531,7 @@ describe("widget", function(){
scope.$inject('$xhr.cache').data.myUrl = {value:'{{name}}'};
scope.$init();
expect(element.text()).toEqual('misko');
dealoc(scope);
});
it('should remove previously included text if a falsy value is bound to src', function() {
@@ -521,6 +549,7 @@ describe("widget", function(){
scope.$eval();
expect(element.text()).toEqual('');
dealoc(scope);
});
it('should allow this for scope', function(){
@@ -532,6 +561,7 @@ describe("widget", function(){
// This should not be 4, but to fix this properly
// we need to have real events on the scopes.
expect(element.text()).toEqual('4');
dealoc(scope);
});
it('should evaluate onload expression when a partial is loaded', function() {
@@ -545,6 +575,7 @@ describe("widget", function(){
scope.$init();
expect(element.text()).toEqual('my partial');
expect(scope.loaded).toBe(true);
dealoc(scope);
});
});