diff options
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | src/Angular.js | 5 | ||||
| -rw-r--r-- | src/directives.js | 6 | ||||
| -rw-r--r-- | src/formatters.js | 2 | ||||
| -rw-r--r-- | src/parser.js | 3 | ||||
| -rw-r--r-- | src/widgets.js | 288 | ||||
| -rw-r--r-- | test/BinderSpec.js | 6 | ||||
| -rw-r--r-- | test/testabilityPatch.js | 2 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 341 |
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}"> + + </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); |
