diff options
Diffstat (limited to 'src/widgets.js')
| -rw-r--r-- | src/widgets.js | 288 |
1 files changed, 233 insertions, 55 deletions
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]; |
