aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md4
-rw-r--r--src/Angular.js5
-rw-r--r--src/directives.js6
-rw-r--r--src/formatters.js2
-rw-r--r--src/parser.js3
-rw-r--r--src/widgets.js288
-rw-r--r--test/BinderSpec.js6
-rw-r--r--test/testabilityPatch.js2
-rw-r--r--test/widgetsSpec.js341
9 files changed, 498 insertions, 159 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5acb5832..83aaa8c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,15 +3,19 @@
### New Features
- Added prepend() to jqLite
+- Added ng:options directive (http://docs.angularjs.org/#!angular.directive.ng:options)
### Bug Fixes
- Number filter would return incorrect value when fractional part had leading zeros.
+
### Breaking changes
- $service now has $service.invoke for method injection ($service(self, fn) no longer works)
- injection name inference no longer supports method curry and linking functions. Both must be
explicitly specified using $inject property.
+- Dynamic Iteration (ng:repeater) on <option> elements is no longer supported. Use ng:options
+
<a name="0.9.16"><a/>
diff --git a/src/Angular.js b/src/Angular.js
index 26026cf3..b5e7e12f 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -52,8 +52,9 @@ if ('i' !== 'I'.toLowerCase()) {
function fromCharCode(code) { return String.fromCharCode(code); }
-var $$element = '$element',
- $$update = '$update',
+var _undefined = undefined,
+ _null = null,
+ $$element = '$element',
$$scope = '$scope',
$$validate = '$validate',
$angular = 'angular',
diff --git a/src/directives.js b/src/directives.js
index 34a1b27d..016ba9fe 100644
--- a/src/directives.js
+++ b/src/directives.js
@@ -443,10 +443,8 @@ var REMOVE_ATTRIBUTES = {
angularDirective("ng:bind-attr", function(expression){
return function(element){
var lastValue = {};
- var updateFn = element.data($$update) || noop;
this.$onEval(function(){
- var values = this.$eval(expression),
- dirty = noop;
+ var values = this.$eval(expression);
for(var key in values) {
var value = compileBindTemplate(values[key]).call(this, element),
specialName = REMOVE_ATTRIBUTES[lowercase(key)];
@@ -464,10 +462,8 @@ angularDirective("ng:bind-attr", function(expression){
} else {
element.attr(key, value);
}
- dirty = updateFn;
}
}
- dirty();
}, element);
};
});
diff --git a/src/formatters.js b/src/formatters.js
index 906db619..b484f26f 100644
--- a/src/formatters.js
+++ b/src/formatters.js
@@ -204,6 +204,7 @@ angularFormatter.trim = formatter(
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.index
+ * @deprecated
* @description
* Index formatter is meant to be used with `select` input widget. It is useful when one needs
* to select from a set of objects. To create pull-down one can iterate over the array of object
@@ -250,6 +251,7 @@ angularFormatter.trim = formatter(
</doc:scenario>
</doc:example>
*/
+//TODO: delete me since this is replaced by ng:options
angularFormatter.index = formatter(
function(object, array){
return '' + indexOf(array || [], object);
diff --git a/src/parser.js b/src/parser.js
index 59d7899a..0c4a391a 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -239,6 +239,8 @@ function parser(text, json){
pipeFunction =
function (){ throwError("is not valid json", {text:text, index:0}); };
}
+ //TODO: Shouldn't all of the public methods have assertAllConsumed?
+ //TODO: I think these should be public as part of the parser api instead of scope.$eval().
return {
assignable: assertConsumed(assignable),
primary: assertConsumed(primary),
@@ -659,4 +661,3 @@ function parser(text, json){
-
diff --git a/src/widgets.js b/src/widgets.js
index 4245f99c..a9d42bdf 100644
--- a/src/widgets.js
+++ b/src/widgets.js
@@ -166,18 +166,19 @@
function modelAccessor(scope, element) {
var expr = element.attr('name');
- var assign;
+ var exprFn, assignFn;
if (expr) {
- assign = parser(expr).assignable().assign;
- if (!assign) throw new Error("Expression '" + expr + "' is not assignable.");
+ exprFn = parser(expr).assignable();
+ assignFn = exprFn.assign;
+ if (!assignFn) throw new Error("Expression '" + expr + "' is not assignable.");
return {
get: function() {
- return scope.$eval(expr);
+ return exprFn(scope);
},
set: function(value) {
if (value !== undefined) {
return scope.$tryEval(function(){
- assign(scope, value);
+ assignFn(scope, value);
}, element);
}
}
@@ -561,64 +562,241 @@ function inputWidgetSelector(element){
angularWidget('input', inputWidgetSelector);
angularWidget('textarea', inputWidgetSelector);
angularWidget('button', inputWidgetSelector);
-angularWidget('select', function(element){
- this.descend(true);
- return inputWidgetSelector.call(this, element);
-});
+/**
+ * @workInProgress
+ * @ngdoc directive
+ * @name angular.directive.ng:options
+ *
+ * @description
+ * Dynamically generate a list of `<option>` elements for a `<select>` element using the array
+ * obtained by evaluating the `ng:options` expression.
+ *
+ * When an item in the select menu is select, the array element represented by the selected option
+ * will be bound to the model identified by the `name` attribute of the parent select element.
+ *
+ * Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
+ * be nested into the `<select>` element. This element will then represent `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * Note: `ng:options` provides iterator facility for `<option>` element which must be used instead
+ * of {@link angular.widget.@ng:repeat ng:repeat}. `ng:repeat` is not suitable for use with
+ * `<option>` element because of the following reasons:
+ *
+ * * value attribute of the option element that we need to bind to requires a string, but the
+ * source of data for the iteration might be in a form of array containing objects instead of
+ * strings
+ * * {@link angular.widget.@ng:repeat ng:repeat} unrolls after the select binds causing
+ * incorect rendering on most browsers.
+ * * binding to a value not in list confuses most browsers.
+ *
+ * @element select
+ * @param {comprehension_expression} comprehension _expresion_ `for` _item_ `in` _array_.
+ *
+ * * _array_: an expression which evaluates to an array of objects to bind.
+ * * _item_: local variable which will reffer to the item in the _array_ during the itteration
+ * * _expression_: The result of this expression will is `option` label. The
+ * `expression` most likely reffers to the _item_ varibale.
+ *
+ * @example
+ <doc:example>
+ <doc:source>
+ <script>
+ function MyCntrl(){
+ this.colors = [
+ {name:'black'},
+ {name:'white'},
+ {name:'red'},
+ {name:'blue'},
+ {name:'green'}
+ ];
+ this.color = this.colors[2]; // red
+ }
+ </script>
+ <div ng:controller="MyCntrl">
+ <ul>
+ <li ng:repeat="color in colors">
+ Name: <input name="color.name"/> [<a href ng:click="colors.$remove(color)">X</a>]
+ </li>
+ <li>
+ [<a href ng:click="colors.push({})">add</a>]
+ </li>
+ </ul>
+ <hr/>
+ Color (null not allowed):
+ <select name="color" ng:options="c.name for c in colors"></select><br/>
+
+ Color (null allowed):
+ <select name="color" ng:options="c.name for c in colors">
+ <option value="">-- chose color --</option>
+ </select><br/>
-/*
- * 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.
+ Select <a href ng:click="color={name:'not in list'}">bogus</a>. <br/>
+ <hr/>
+ Currently selected: {{ {selected_color:color} }}
+ <div style="border:solid 1px black;"
+ ng:style="{'background-color':color.name}">
+ &nbsp;
+ </div>
+ </div>
+ </doc:source>
+ <doc:scenario>
+ it('should check ng:options', function(){
+ expect(binding('color')).toMatch('red');
+ select('color').option('0');
+ expect(binding('color')).toMatch('black');
+ select('color').option('');
+ expect(binding('color')).toMatch('null');
+ });
+ </doc:scenario>
+ </doc:example>
*/
-angularWidget('option', function(){
+
+var NG_OPTIONS_REGEXP = /^(.*)\s+for\s+([\$\w][\$\w\d]*)\s+in\s+(.*)$/;
+angularWidget('select', function(element){
this.descend(true);
this.directives(true);
- return function(option) {
- var select = option.parent();
- var isMultiple = select[0].type == 'select-multiple';
- var scope = select.scope();
- var model = modelAccessor(scope, select);
-
- //if parent select doesn't have a name, don't bother doing anything any more
- if (!model) return;
-
- var formattedModel = modelFormattedAccessor(scope, select);
- var view = isMultiple
- ? optionsAccessor(scope, select)
- : valueAccessor(scope, select);
- var lastValue = option.attr($value);
- var wasSelected = option.attr('ng-' + $selected);
- option.data($$update, isMultiple
- ? function(){
- view.set(model.get());
+ var isMultiselect = element.attr('multiple');
+ var expression = element.attr('ng:options');
+ var match;
+ if (!expression) {
+ return inputWidgetSelector.call(this, element);
+ }
+ if (! (match = expression.match(NG_OPTIONS_REGEXP))) {
+ throw Error(
+ "Expected ng:options in form of '_expresion_ for _item_ in _collection_' but got '" +
+ expression + "'.");
+ }
+ var displayFn = expressionCompile(match[1]).fnSelf;
+ var itemName = match[2];
+ var collectionFn = expressionCompile(match[3]).fnSelf;
+ // we can't just jqLite('<option>') since jqLite is not smart enough
+ // to create it in <select> and IE barfs otherwise.
+ var option = jqLite(document.createElement('option'));
+ return function(select){
+ var scope = this;
+ var optionElements = [];
+ var optionTexts = [];
+ var lastSelectValue = isMultiselect ? {} : false;
+ var nullOption = option.clone().val('');
+ var missingOption = option.clone().val('?');
+ var model = modelAccessor(scope, element);
+
+ // find existing special options
+ forEach(select.children(), function(option){
+ if (option.value == '') nullOption = false;
+ });
+
+ select.bind('change', function(){
+ var collection = collectionFn(scope) || [];
+ var value = select.val();
+ var index, length;
+ if (isMultiselect) {
+ value = [];
+ for (index = 0, length = optionElements.length; index < length; index++) {
+ if (optionElements[index][0].selected) {
+ value.push(collection[index]);
+ }
+ }
+ } else {
+ if (value == '?') {
+ value = undefined;
+ } else {
+ value = (value == '' ? null : collection[value]);
+ }
+ }
+ if (!isUndefined(value)) model.set(value);
+ scope.$tryEval(function(){
+ scope.$root.$eval();
+ });
+ });
+
+ scope.$onEval(function(){
+ var scope = this;
+ var collection = collectionFn(scope) || [];
+ var value;
+ var length;
+ var fragment;
+ var index;
+ var optionText;
+ var optionElement;
+ var optionScope = scope.$new();
+ var modelValue = model.get();
+ var currentItem;
+ var selectValue = '';
+ var isMulti = isMultiselect;
+
+ if (isMulti) {
+ selectValue = new HashMap();
+ if (modelValue && isNumber(length = modelValue.length)) {
+ for (index = 0; index < length; index++) {
+ selectValue.put(modelValue[index], true);
+ }
}
- : function(){
- var currentValue = option.attr($value);
- var isSelected = option.attr('ng-' + $selected);
- var modelValue = model.get();
- if (wasSelected != isSelected || lastValue != currentValue) {
- wasSelected = isSelected;
- lastValue = currentValue;
- if (isSelected || !modelValue == null || modelValue == undefined )
- formattedModel.set(currentValue);
- if (currentValue == modelValue) {
- view.set(lastValue);
+ }
+ try {
+ for (index = 0, length = collection.length; index < length; index++) {
+ currentItem = optionScope[itemName] = collection[index];
+ optionText = displayFn(optionScope);
+ if (optionTexts.length > index) {
+ // reuse
+ optionElement = optionElements[index];
+ if (optionText != optionTexts[index]) {
+ (optionElement).text(optionTexts[index] = optionText);
+ }
+ } else {
+ // grow
+ if (!fragment) {
+ fragment = document.createDocumentFragment();
}
+ optionTexts.push(optionText);
+ optionElements.push(optionElement = option.clone());
+ optionElement.attr('value', index).text(optionText);
+ fragment.appendChild(optionElement[0]);
}
+ if (isMulti) {
+ if (lastSelectValue[index] != (value = selectValue.remove(currentItem))) {
+ optionElement[0].selected = !!(lastSelectValue[index] = value);
+ }
+ } else {
+ if (modelValue == currentItem) {
+ selectValue = index;
+ }
+ }
+ }
+ if (fragment) select.append(jqLite(fragment));
+ // shrink children
+ while(optionElements.length > index) {
+ optionElements.pop().remove();
+ delete lastSelectValue[optionElements.length];
}
- );
+
+ if (!isMulti) {
+ if (selectValue === '' && modelValue) {
+ // We could not find a match
+ selectValue = '?';
+ }
+
+ // update the selected item
+ if (lastSelectValue !== selectValue) {
+ if (nullOption) {
+ if (lastSelectValue == '') nullOption.remove();
+ if (selectValue === '') select.prepend(nullOption);
+ }
+
+ if (missingOption) {
+ if (lastSelectValue == '?') missingOption.remove();
+ if (selectValue === '?') select.prepend(missingOption);
+ }
+
+ select.val(lastSelectValue = selectValue);
+ }
+ }
+
+ } finally {
+ optionScope = null;
+ }
+ });
};
});
@@ -932,7 +1110,7 @@ angularWidget('@ng:repeat', function(expression, element){
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
lhs, rhs, valueIdent, keyIdent;
if (! match) {
- throw Error("Expected ng:repeat in form of 'item in collection' but got '" +
+ throw Error("Expected ng:repeat in form of '_item_ in _collection_' but got '" +
expression + "'.");
}
lhs = match[1];
diff --git a/test/BinderSpec.js b/test/BinderSpec.js
index d78573bb..85387d8f 100644
--- a/test/BinderSpec.js
+++ b/test/BinderSpec.js
@@ -463,12 +463,6 @@ describe('Binder', function(){
assertEquals('123{{a}}{{b}}{{c}}', scope.$element.text());
});
- it('OptionShouldUpdateParentToGetProperBinding', function(){
- var scope = this.compile('<select name="s"><option ng:repeat="i in [0,1]" value="{{i}}" ng:bind="i"></option></select>');
- scope.$set('s', 1);
- scope.$eval();
- assertEquals(1, scope.$element[0].selectedIndex);
- });
it('RepeaterShouldBindInputsDefaults', function () {
var scope = this.compile('<div><input value="123" name="item.name" ng:repeat="item in items"></div>');
diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js
index 0776f620..121d6900 100644
--- a/test/testabilityPatch.js
+++ b/test/testabilityPatch.js
@@ -212,7 +212,7 @@ function sortedHtml(element, showNgClass) {
attr.value !='auto' &&
attr.value !='false' &&
attr.value !='inherit' &&
- attr.value !='0' &&
+ (attr.value !='0' || attr.name =='value') &&
attr.name !='loop' &&
attr.name !='complete' &&
attr.name !='maxLength' &&
diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js
index b412cd6d..176f6b9c 100644
--- a/test/widgetsSpec.js
+++ b/test/widgetsSpec.js
@@ -367,93 +367,6 @@ describe("widget", function(){
expect(element[0].childNodes[0].selected).toEqual(true);
});
- it('should honor the value field in option', function(){
- compile(
- '<select name="selection" ng:format="number">' +
- '<option value="{{$index}}" ng:repeat="name in [\'A\', \'B\', \'C\']">{{name}}</option>' +
- '</select>');
- // childNodes[0] is repeater comment
- expect(scope.selection).toEqual(0);
-
- browserTrigger(element[0].childNodes[2], 'change');
- expect(scope.selection).toEqual(1);
-
- scope.selection = 2;
- scope.$eval();
- expect(element[0].childNodes[3].selected).toEqual(true);
- });
-
- it('should unroll select options before eval', function(){
- compile(
- '<select name="selection" ng:required>' +
- '<option value="{{$index}}" ng:repeat="opt in options">{{opt}}</option>' +
- '</select>',
- jqLite(document.body));
- 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(
- '<select name="selection">' +
- '<option value="...">...</option>' +
- '<option value="{{value}}">B</option>' +
- '</select>');
- 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 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 allow binding to objects through JSON', function(){
- compile(
- '<select name="selection" ng:format="json">' +
- '<option ng:repeat="obj in objs" value="{{obj}}">{{obj.name}}</option>' +
- '</select>');
- scope.objs = [{name:'A'}, {name:'B'}];
- scope.$eval();
- expect(scope.selection).toEqual({name:'A'});
- });
-
- it('should allow binding to objects through index', function(){
- compile(
- '<select name="selection" ng:format="index:objs">' +
- '<option ng:repeat="obj in objs" value="{{$index}}">{{obj.name}}</option>' +
- '</select>');
- scope.objs = [{name:'A'}, {name:'B'}];
- scope.$eval();
- expect(scope.selection).toBe(scope.objs[0]);
- });
it('should compile children of a select without a name, but not create a model for it',
function() {
@@ -695,6 +608,256 @@ describe("widget", function(){
});
});
+ describe('ng:options', function(){
+ var select, scope;
+
+ function createSelect(multiple, blank, unknown){
+ select = jqLite(
+ '<select name="selected" ' + (multiple ? ' multiple' : '') +
+ ' ng:options="value.name for value in values">' +
+ (blank ? '<option value="">blank</option>' : '') +
+ (unknown ? '<option value="?">unknown</option>' : '') +
+ '</select>');
+ scope = compile(select);
+ };
+
+ function createSingleSelect(blank, unknown){
+ createSelect(false, blank, unknown);
+ };
+
+ function createMultiSelect(blank, unknown){
+ createSelect(true, blank, unknown);
+ };
+
+ afterEach(function(){
+ dealoc(select);
+ dealoc(scope);
+ });
+
+ it('should throw when not formated "? for ? in ?"', function(){
+ expect(function(){
+ compile('<select name="selected" ng:options="i dont parse"></select>');
+ }).toThrow("Expected ng:options in form of '_expresion_ for _item_ in _collection_' but got 'i dont parse'.");
+
+ $logMock.error.logs.shift();
+ });
+
+ it('should render a list', function(){
+ createSingleSelect();
+ scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+ scope.selected = scope.values[0];
+ scope.$eval();
+ var options = select.find('option');
+ expect(options.length).toEqual(3);
+ expect(sortedHtml(options[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(options[1])).toEqual('<option value="1">B</option>');
+ expect(sortedHtml(options[2])).toEqual('<option value="2">C</option>');
+ });
+
+ it('should grow list', function(){
+ createSingleSelect();
+ scope.values = [];
+ scope.$eval();
+ expect(select.find('option').length).toEqual(1); // because we add special empty option
+ expect(sortedHtml(select.find('option')[0])).toEqual('<option></option>');
+
+ scope.values.push({name:'A'});
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.find('option').length).toEqual(1);
+ expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
+
+ scope.values.push({name:'B'});
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>');
+ });
+
+ it('should shrink list', function(){
+ createSingleSelect();
+ scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.find('option').length).toEqual(3);
+
+ scope.values.pop();
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
+ expect(sortedHtml(select.find('option')[1])).toEqual('<option value="1">B</option>');
+
+ scope.values.pop();
+ scope.$eval();
+ expect(select.find('option').length).toEqual(1);
+ expect(sortedHtml(select.find('option')[0])).toEqual('<option value="0">A</option>');
+
+ scope.values.pop();
+ scope.selected = null;
+ scope.$eval();
+ expect(select.find('option').length).toEqual(1); // we add back the special empty option
+ });
+
+ it('should update list', function(){
+ createSingleSelect();
+ scope.values = [{name:'A'}, {name:'B'}, {name:'C'}];
+ scope.selected = scope.values[0];
+ scope.$eval();
+
+ scope.values = [{name:'B'}, {name:'C'}, {name:'D'}];
+ scope.selected = scope.values[0];
+ scope.$eval();
+ var options = select.find('option');
+ expect(options.length).toEqual(3);
+ expect(sortedHtml(options[0])).toEqual('<option value="0">B</option>');
+ expect(sortedHtml(options[1])).toEqual('<option value="1">C</option>');
+ expect(sortedHtml(options[2])).toEqual('<option value="2">D</option>');
+ });
+
+ it('should preserve existing options', function(){
+ createSingleSelect(true);
+
+ scope.$eval();
+ expect(select.find('option').length).toEqual(1);
+
+ scope.values = [{name:'A'}];
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(jqLite(select.find('option')[0]).text()).toEqual('blank');
+ expect(jqLite(select.find('option')[1]).text()).toEqual('A');
+
+ scope.values = [];
+ scope.selected = null;
+ scope.$eval();
+ expect(select.find('option').length).toEqual(1);
+ expect(jqLite(select.find('option')[0]).text()).toEqual('blank');
+ });
+
+ describe('binding', function(){
+ it('should bind to scope value', function(){
+ createSingleSelect();
+ scope.values = [{name:'A'}, {name:'B'}];
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.val()).toEqual('0');
+
+ scope.selected = scope.values[1];
+ scope.$eval();
+ expect(select.val()).toEqual('1');
+ });
+
+ it('should insert a blank option if bound to null', function(){
+ createSingleSelect();
+ scope.values = [{name:'A'}];
+ scope.selected = null;
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(select.val()).toEqual('');
+ expect(jqLite(select.find('option')[0]).val()).toEqual('');
+
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.val()).toEqual('0');
+ expect(select.find('option').length).toEqual(1);
+ });
+
+ it('should reuse blank option if bound to null', function(){
+ createSingleSelect(true);
+ scope.values = [{name:'A'}];
+ scope.selected = null;
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(select.val()).toEqual('');
+ expect(jqLite(select.find('option')[0]).val()).toEqual('');
+
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.val()).toEqual('0');
+ expect(select.find('option').length).toEqual(2);
+ });
+
+ it('should insert a unknown option if bound to not in list', function(){
+ createSingleSelect();
+ scope.values = [{name:'A'}];
+ scope.selected = {};
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(select.val()).toEqual('?');
+ expect(jqLite(select.find('option')[0]).val()).toEqual('?');
+
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.val()).toEqual('0');
+ expect(select.find('option').length).toEqual(1);
+ });
+ });
+
+ describe('on change', function(){
+ it('should update model on change', function(){
+ createSingleSelect();
+ scope.values = [{name:'A'}, {name:'B'}];
+ scope.selected = scope.values[0];
+ scope.$eval();
+ expect(select.val()).toEqual('0');
+
+ select.val('1');
+ browserTrigger(select, 'change');
+ expect(scope.selected).toEqual(scope.values[1]);
+ });
+
+ it('should update model to null on change', function(){
+ createSingleSelect(true);
+ scope.values = [{name:'A'}, {name:'B'}];
+ scope.selected = scope.values[0];
+ select.val('0');
+ scope.$eval();
+
+ select.val('');
+ browserTrigger(select, 'change');
+ expect(scope.selected).toEqual(null);
+ });
+ });
+
+ describe('select-many', function(){
+ it('should read multiple selection', function(){
+ createMultiSelect();
+ scope.values = [{name:'A'}, {name:'B'}];
+
+ scope.selected = [];
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(jqLite(select.find('option')[0]).attr('selected')).toEqual(false);
+ expect(jqLite(select.find('option')[1]).attr('selected')).toEqual(false);
+
+ scope.selected.push(scope.values[1]);
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(select.find('option')[0].selected).toEqual(false);
+ expect(select.find('option')[1].selected).toEqual(true);
+
+ scope.selected.push(scope.values[0]);
+ scope.$eval();
+ expect(select.find('option').length).toEqual(2);
+ expect(select.find('option')[0].selected).toEqual(true);
+ expect(select.find('option')[1].selected).toEqual(true);
+ });
+
+ it('should update model on change', function(){
+ createMultiSelect();
+ scope.values = [{name:'A'}, {name:'B'}];
+
+ scope.selected = [];
+ scope.$eval();
+ select.find('option')[0].selected = true;
+
+ browserTrigger(select, 'change');
+ expect(scope.selected).toEqual([scope.values[0]]);
+ });
+ });
+
+ });
+
describe('@ng:repeat', function() {
@@ -739,10 +902,10 @@ describe("widget", function(){
var scope = compile('<ul><li ng:repeat="i dont parse"></li></ul>');
expect(scope.$service('$log').error.logs.shift()[0]).
- toEqualError("Expected ng:repeat in form of 'item in collection' but got 'i dont parse'.");
+ toEqualError("Expected ng:repeat in form of '_item_ in _collection_' but got 'i dont parse'.");
expect(scope.$element.attr('ng-exception')).
- toMatch(/Expected ng:repeat in form of 'item in collection' but got 'i dont parse'/);
+ toMatch(/Expected ng:repeat in form of '_item_ in _collection_' but got 'i dont parse'/);
expect(scope.$element).toHaveClass('ng-exception');
dealoc(scope);