diff options
| author | Misko Hevery | 2011-09-08 13:56:29 -0700 |
|---|---|---|
| committer | Igor Minar | 2011-10-11 11:01:45 -0700 |
| commit | 4f78fd692c0ec51241476e6be9a4df06cd62fdd6 (patch) | |
| tree | 91f70bb89b9c095126fbc093f51cedbac5cb0c78 /src/widgets.js | |
| parent | df6d2ba3266de405ad6c2f270f24569355706e76 (diff) | |
| download | angular.js-4f78fd692c0ec51241476e6be9a4df06cd62fdd6.tar.bz2 | |
feat(forms): new and improved forms
Diffstat (limited to 'src/widgets.js')
| -rw-r--r-- | src/widgets.js | 1033 |
1 files changed, 71 insertions, 962 deletions
diff --git a/src/widgets.js b/src/widgets.js index 1047c3ce..e3c6906f 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -14,14 +14,11 @@ * * Following is the list of built-in angular widgets: * - * * {@link angular.widget.@ng:format ng:format} - Formats data for display to user and for storage. * * {@link angular.widget.@ng:non-bindable ng:non-bindable} - Blocks angular from processing an * HTML element. * * {@link angular.widget.@ng:repeat ng:repeat} - Creates and manages a collection of cloned HTML * elements. - * * {@link angular.widget.@ng:required ng:required} - Verifies presence of user input. - * * {@link angular.widget.@ng:validate ng:validate} - Validates content of user input. - * * {@link angular.widget.HTML HTML input elements} - Standard HTML input elements data-bound by + * * {@link angular.inputType HTML input elements} - Standard HTML input elements data-bound by * angular. * * {@link angular.widget.ng:view ng:view} - Works with $route to "include" partial templates * * {@link angular.widget.ng:switch ng:switch} - Conditionally changes DOM structure @@ -34,915 +31,6 @@ /** * @workInProgress * @ngdoc widget - * @name angular.widget.HTML - * - * @description - * The most common widgets you will use will be in the form of the - * standard HTML set. These widgets are bound using the `name` attribute - * to an expression. In addition, they can have `ng:validate`, `ng:required`, - * `ng:format`, `ng:change` attribute to further control their behavior. - * - * @usageContent - * see example below for usage - * - * <input type="text|checkbox|..." ... /> - * <textarea ... /> - * <select ...> - * <option>...</option> - * </select> - * - * @example - <doc:example> - <doc:source> - <table style="font-size:.9em;"> - <tr> - <th>Name</th> - <th>Format</th> - <th>HTML</th> - <th>UI</th> - <th ng:non-bindable>{{input#}}</th> - </tr> - <tr> - <th>text</th> - <td>String</td> - <td><tt><input type="text" name="input1"></tt></td> - <td><input type="text" name="input1" size="4"></td> - <td><tt>{{input1|json}}</tt></td> - </tr> - <tr> - <th>textarea</th> - <td>String</td> - <td><tt><textarea name="input2"></textarea></tt></td> - <td><textarea name="input2" cols='6'></textarea></td> - <td><tt>{{input2|json}}</tt></td> - </tr> - <tr> - <th>radio</th> - <td>String</td> - <td><tt> - <input type="radio" name="input3" value="A"><br> - <input type="radio" name="input3" value="B"> - </tt></td> - <td> - <input type="radio" name="input3" value="A"> - <input type="radio" name="input3" value="B"> - </td> - <td><tt>{{input3|json}}</tt></td> - </tr> - <tr> - <th>checkbox</th> - <td>Boolean</td> - <td><tt><input type="checkbox" name="input4" value="checked"></tt></td> - <td><input type="checkbox" name="input4" value="checked"></td> - <td><tt>{{input4|json}}</tt></td> - </tr> - <tr> - <th>pulldown</th> - <td>String</td> - <td><tt> - <select name="input5"><br> - <option value="c">C</option><br> - <option value="d">D</option><br> - </select><br> - </tt></td> - <td> - <select name="input5"> - <option value="c">C</option> - <option value="d">D</option> - </select> - </td> - <td><tt>{{input5|json}}</tt></td> - </tr> - <tr> - <th>multiselect</th> - <td>Array</td> - <td><tt> - <select name="input6" multiple size="4"><br> - <option value="e">E</option><br> - <option value="f">F</option><br> - </select><br> - </tt></td> - <td> - <select name="input6" multiple size="4"> - <option value="e">E</option> - <option value="f">F</option> - </select> - </td> - <td><tt>{{input6|json}}</tt></td> - </tr> - </table> - </doc:source> - <doc:scenario> - - it('should exercise text', function(){ - input('input1').enter('Carlos'); - expect(binding('input1')).toEqual('"Carlos"'); - }); - it('should exercise textarea', function(){ - input('input2').enter('Carlos'); - expect(binding('input2')).toEqual('"Carlos"'); - }); - it('should exercise radio', function(){ - expect(binding('input3')).toEqual('null'); - input('input3').select('A'); - expect(binding('input3')).toEqual('"A"'); - input('input3').select('B'); - expect(binding('input3')).toEqual('"B"'); - }); - it('should exercise checkbox', function(){ - expect(binding('input4')).toEqual('false'); - input('input4').check(); - expect(binding('input4')).toEqual('true'); - }); - it('should exercise pulldown', function(){ - expect(binding('input5')).toEqual('"c"'); - select('input5').option('d'); - expect(binding('input5')).toEqual('"d"'); - }); - it('should exercise multiselect', function(){ - expect(binding('input6')).toEqual('[]'); - select('input6').options('e'); - expect(binding('input6')).toEqual('["e"]'); - select('input6').options('e', 'f'); - expect(binding('input6')).toEqual('["e","f"]'); - }); - </doc:scenario> - </doc:example> - */ - -function modelAccessor(scope, element) { - var expr = element.attr('name'); - var exprFn, assignFn; - if (expr) { - exprFn = parser(expr).assignable(); - assignFn = exprFn.assign; - if (!assignFn) throw new Error("Expression '" + expr + "' is not assignable."); - return { - get: function() { - return exprFn(scope); - }, - set: function(value) { - if (value !== undefined) { - assignFn(scope, value); - } - } - }; - } -} - -function modelFormattedAccessor(scope, element) { - var accessor = modelAccessor(scope, element), - formatterName = element.attr('ng:format') || NOOP, - formatter = compileFormatter(formatterName); - if (accessor) { - return { - get: function() { - return formatter.format(scope, accessor.get()); - }, - set: function(value) { - return accessor.set(formatter.parse(scope, value)); - } - }; - } -} - -function compileValidator(expr) { - return parser(expr).validator()(); -} - -function compileFormatter(expr) { - return parser(expr).formatter()(); -} - -/** - * @workInProgress - * @ngdoc widget - * @name angular.widget.@ng:validate - * - * @description - * The `ng:validate` attribute widget validates the user input. If the input does not pass - * validation, the `ng-validation-error` CSS class and the `ng:error` attribute are set on the input - * element. Check out {@link angular.validator validators} to find out more. - * - * @param {string} validator The name of a built-in or custom {@link angular.validator validator} to - * to be used. - * - * @element INPUT - * @css ng-validation-error - * - * @example - * This example shows how the input element becomes red when it contains invalid input. Correct - * the input to make the error disappear. - * - <doc:example> - <doc:source> - I don't validate: - <input type="text" name="value" value="NotANumber"><br/> - - I need an integer or nothing: - <input type="text" name="value" ng:validate="integer"><br/> - </doc:source> - <doc:scenario> - it('should check ng:validate', function(){ - expect(element('.doc-example-live :input:last').prop('className')). - toMatch(/ng-validation-error/); - - input('value').enter('123'); - expect(element('.doc-example-live :input:last').prop('className')). - not().toMatch(/ng-validation-error/); - }); - </doc:scenario> - </doc:example> - */ -/** - * @workInProgress - * @ngdoc widget - * @name angular.widget.@ng:required - * - * @description - * The `ng:required` attribute widget validates that the user input is present. It is a special case - * of the {@link angular.widget.@ng:validate ng:validate} attribute widget. - * - * @element INPUT - * @css ng-validation-error - * - * @example - * This example shows how the input element becomes red when it contains invalid input. Correct - * the input to make the error disappear. - * - <doc:example> - <doc:source> - I cannot be blank: <input type="text" name="value" ng:required><br/> - </doc:source> - <doc:scenario> - it('should check ng:required', function(){ - expect(element('.doc-example-live :input').prop('className')). - toMatch(/ng-validation-error/); - input('value').enter('123'); - expect(element('.doc-example-live :input').prop('className')). - not().toMatch(/ng-validation-error/); - }); - </doc:scenario> - </doc:example> - */ -/** - * @workInProgress - * @ngdoc widget - * @name angular.widget.@ng:format - * - * @description - * The `ng:format` attribute widget formats stored data to user-readable text and parses the text - * back to the stored form. You might find this useful, for example, if you collect user input in a - * text field but need to store the data in the model as a list. Check out - * {@link angular.formatter formatters} to learn more. - * - * @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter} - * to be used. - * - * @element INPUT - * - * @example - * This example shows how the user input is converted from a string and internally represented as an - * array. - * - <doc:example> - <doc:source> - Enter a comma separated list of items: - <input type="text" name="list" ng:format="list" value="table, chairs, plate"> - <pre>list={{list}}</pre> - </doc:source> - <doc:scenario> - it('should check ng:format', function(){ - expect(binding('list')).toBe('list=["table","chairs","plate"]'); - input('list').enter(',,, a ,,,'); - expect(binding('list')).toBe('list=["a"]'); - }); - </doc:scenario> - </doc:example> - */ -function valueAccessor(scope, element) { - var validatorName = element.attr('ng:validate') || NOOP, - validator = compileValidator(validatorName), - requiredExpr = element.attr('ng:required'), - formatterName = element.attr('ng:format') || NOOP, - formatter = compileFormatter(formatterName), - format, parse, lastError, required, - invalidWidgets = scope.$service('$invalidWidgets') || {markValid:noop, markInvalid:noop}; - if (!validator) throw "Validator named '" + validatorName + "' not found."; - format = formatter.format; - parse = formatter.parse; - if (requiredExpr) { - scope.$watch(requiredExpr, function(scope, newValue) { - required = newValue; - validate(); - }); - } else { - required = requiredExpr === ''; - } - - element.data($$validate, validate); - return { - get: function(){ - if (lastError) - elementError(element, NG_VALIDATION_ERROR, null); - try { - var value = parse(scope, element.val()); - validate(); - return value; - } catch (e) { - lastError = e; - elementError(element, NG_VALIDATION_ERROR, e); - } - }, - set: function(value) { - var oldValue = element.val(), - newValue = format(scope, value); - if (oldValue != newValue) { - element.val(newValue || ''); // needed for ie - } - validate(); - } - }; - - function validate() { - var value = trim(element.val()); - if (element[0].disabled || element[0].readOnly) { - elementError(element, NG_VALIDATION_ERROR, null); - invalidWidgets.markValid(element); - } else { - var error, validateScope = inherit(scope, {$element:element}); - error = required && !value - ? 'Required' - : (value ? validator(validateScope, value) : null); - elementError(element, NG_VALIDATION_ERROR, error); - lastError = error; - if (error) { - invalidWidgets.markInvalid(element); - } else { - invalidWidgets.markValid(element); - } - } - } -} - -function checkedAccessor(scope, element) { - var domElement = element[0], elementValue = domElement.value; - return { - get: function(){ - return !!domElement.checked; - }, - set: function(value){ - domElement.checked = toBoolean(value); - } - }; -} - -function radioAccessor(scope, element) { - var domElement = element[0]; - return { - get: function(){ - return domElement.checked ? domElement.value : null; - }, - set: function(value){ - domElement.checked = value == domElement.value; - } - }; -} - -function optionsAccessor(scope, element) { - var formatterName = element.attr('ng:format') || NOOP, - formatter = compileFormatter(formatterName); - return { - get: function(){ - var values = []; - forEach(element[0].options, function(option){ - if (option.selected) values.push(formatter.parse(scope, option.value)); - }); - return values; - }, - set: function(values){ - var keys = {}; - forEach(values, function(value){ - keys[formatter.format(scope, value)] = true; - }); - forEach(element[0].options, function(option){ - option.selected = keys[option.value]; - }); - } - }; -} - -function noopAccessor() { return { get: noop, set: noop }; } - -/* - * TODO: refactor - * - * The table below is not quite right. In some cases the formatter is on the model side - * and in some cases it is on the view side. This is a historical artifact - * - * The concept of model/view accessor is useful for anyone who is trying to develop UI, and - * so it should be exposed to others. There should be a form object which keeps track of the - * accessors and also acts as their factory. It should expose it as an object and allow - * the validator to publish errors to it, so that the the error messages can be bound to it. - * - */ -var textWidget = inputWidget('keydown change', modelAccessor, valueAccessor, initWidgetValue(), true), - INPUT_TYPE = { - 'text': textWidget, - 'textarea': textWidget, - 'hidden': textWidget, - 'password': textWidget, - 'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)), - 'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), - 'select-one': inputWidget('change', modelAccessor, valueAccessor, initWidgetValue(null)), - 'select-multiple': inputWidget('change', modelAccessor, optionsAccessor, initWidgetValue([])) -// 'file': fileWidget??? - }; - - -function initWidgetValue(initValue) { - return function (model, view) { - var value = view.get(); - if (!value && isDefined(initValue)) { - value = copy(initValue); - } - if (isUndefined(model.get()) && isDefined(value)) { - model.set(value); - } - }; -} - -function radioInit(model, view, element) { - var modelValue = model.get(), viewValue = view.get(), input = element[0]; - input.checked = false; - input.name = this.$id + '@' + input.name; - if (isUndefined(modelValue)) { - model.set(modelValue = null); - } - if (modelValue == null && viewValue !== null) { - model.set(viewValue); - } - view.set(modelValue); -} - -/** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:change - * - * @description - * The directive executes an expression whenever the input widget changes. - * - * @element INPUT - * @param {expression} expression to execute. - * - * @example - * @example - <doc:example> - <doc:source> - <div ng:init="checkboxCount=0; textCount=0"></div> - <input type="text" name="text" ng:change="textCount = 1 + textCount"> - changeCount {{textCount}}<br/> - <input type="checkbox" name="checkbox" ng:change="checkboxCount = 1 + checkboxCount"> - changeCount {{checkboxCount}}<br/> - </doc:source> - <doc:scenario> - it('should check ng:change', function(){ - expect(binding('textCount')).toBe('0'); - expect(binding('checkboxCount')).toBe('0'); - - using('.doc-example-live').input('text').enter('abc'); - expect(binding('textCount')).toBe('1'); - expect(binding('checkboxCount')).toBe('0'); - - - using('.doc-example-live').input('checkbox').check(); - expect(binding('textCount')).toBe('1'); - expect(binding('checkboxCount')).toBe('1'); - }); - </doc:scenario> - </doc:example> - */ -function inputWidget(events, modelAccessor, viewAccessor, initFn, textBox) { - return annotate('$defer', function($defer, element) { - var scope = this, - model = modelAccessor(scope, element), - view = viewAccessor(scope, element), - ngChange = element.attr('ng:change') || noop, - lastValue; - if (model) { - initFn.call(scope, model, view, element); - scope.$eval(element.attr('ng:init') || noop); - element.bind(events, function(event){ - function handler(){ - var value = view.get(); - if (!textBox || value != lastValue) { - model.set(value); - lastValue = model.get(); - scope.$eval(ngChange); - } - } - event.type == 'keydown' ? $defer(handler) : scope.$apply(handler); - }); - scope.$watch(model.get, function(scope, value) { - if (!equals(lastValue, value)) { - view.set(lastValue = value); - } - }); - } - }); -} - -function inputWidgetSelector(element){ - this.directives(true); - this.descend(true); - return INPUT_TYPE[lowercase(element[0].type)] || noop; -} - -angularWidget('input', inputWidgetSelector); -angularWidget('textarea', inputWidgetSelector); - - -/** - * @workInProgress - * @ngdoc directive - * @name angular.directive.ng:options - * - * @description - * Dynamically generate a list of `<option>` elements for a `<select>` element using an array or - * an object obtained by evaluating the `ng:options` expression. - * - * When an item in the select menu is select, the value of array element or object property - * 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 in one of the following forms: - * - * * for array data sources: - * * `label` **`for`** `value` **`in`** `array` - * * `select` **`as`** `label` **`for`** `value` **`in`** `array` - * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` - * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array` - * * for object data sources: - * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object` - * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object` - * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object` - * * `select` **`as`** `label` **`group by`** `group` - * **`for` `(`**`key`**`,`** `value`**`) in`** `object` - * - * Where: - * - * * `array` / `object`: an expression which evaluates to an array / object to iterate over. - * * `value`: local variable which will refer to each item in the `array` or each property value - * of `object` during iteration. - * * `key`: local variable which will refer to a property name in `object` during iteration. - * * `label`: The result of this expression will be the label for `<option>` element. The - * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`). - * * `select`: The result of this expression will be bound to the model of the parent `<select>` - * element. If not specified, `select` expression will default to `value`. - * * `group`: The result of this expression will be used to group options using the `<optgroup>` - * DOM element. - * - * @example - <doc:example> - <doc:source> - <script> - function MyCntrl(){ - this.colors = [ - {name:'black', shade:'dark'}, - {name:'white', shade:'light'}, - {name:'red', shade:'dark'}, - {name:'blue', shade:'dark'}, - {name:'yellow', shade:'light'} - ]; - 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): - <div class="nullable"> - <select name="color" ng:options="c.name for c in colors"> - <option value="">-- chose color --</option> - </select> - </div><br/> - - Color grouped by shade: - <select name="color" ng:options="c.name group by c.shade for c in colors"> - </select><br/> - - - 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; height:20px" - 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'); - using('.nullable').select('color').option(''); - expect(binding('color')).toMatch('null'); - }); - </doc:scenario> - </doc:example> - */ -// 00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777 -var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/; -angularWidget('select', function(element){ - this.descend(true); - this.directives(true); - - var isMultiselect = element.attr('multiple'), - expression = element.attr('ng:options'), - onChange = expressionCompile(element.attr('ng:change') || ""), - match; - - if (!expression) { - return inputWidgetSelector.call(this, element); - } - if (! (match = expression.match(NG_OPTIONS_REGEXP))) { - throw Error( - "Expected ng:options in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" + - " but got '" + expression + "'."); - } - - var displayFn = expressionCompile(match[2] || match[1]), - valueName = match[4] || match[6], - keyName = match[5], - groupByFn = expressionCompile(match[3] || ''), - valueFn = expressionCompile(match[2] ? match[1] : valueName), - valuesFn = expressionCompile(match[7]), - // we can't just jqLite('<option>') since jqLite is not smart enough - // to create it in <select> and IE barfs otherwise. - optionTemplate = jqLite(document.createElement('option')), - optGroupTemplate = jqLite(document.createElement('optgroup')), - nullOption = false; // if false then user will not be able to select it - - return function(selectElement){ - - // This is an array of array of existing option groups in DOM. We try to reuse these if possible - // optionGroupsCache[0] is the options with no option group - // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element - var optionGroupsCache = [[{element: selectElement, label:''}]], - scope = this, - model = modelAccessor(scope, element), - inChangeEvent; - - // find existing special options - forEach(selectElement.children(), function(option){ - if (option.value == '') - // User is allowed to select the null. - nullOption = {label:jqLite(option).text(), id:''}; - }); - selectElement.html(''); // clear contents - - selectElement.bind('change', function(){ - var optionGroup, - collection = valuesFn(scope) || [], - key = selectElement.val(), - tempScope = scope.$new(), - value, optionElement, index, groupIndex, length, groupLength; - - // let's set a flag that the current model change is due to a change event. - // the default action of option selection will cause the appropriate option element to be - // deselected and another one to be selected - there is no need for us to be updating the DOM - // in this case. - inChangeEvent = true; - - try { - if (isMultiselect) { - value = []; - for (groupIndex = 0, groupLength = optionGroupsCache.length; - groupIndex < groupLength; - groupIndex++) { - // list of options for that group. (first item has the parent) - optionGroup = optionGroupsCache[groupIndex]; - - for(index = 1, length = optionGroup.length; index < length; index++) { - if ((optionElement = optionGroup[index].element)[0].selected) { - if (keyName) tempScope[keyName] = key; - tempScope[valueName] = collection[optionElement.val()]; - value.push(valueFn(tempScope)); - } - } - } - } else { - if (key == '?') { - value = undefined; - } else if (key == ''){ - value = null; - } else { - tempScope[valueName] = collection[key]; - if (keyName) tempScope[keyName] = key; - value = valueFn(tempScope); - } - } - if (isDefined(value) && model.get() !== value) { - model.set(value); - onChange(scope); - } - scope.$root.$apply(); - } finally { - tempScope = null; // TODO(misko): needs to be $destroy - inChangeEvent = false; - } - }); - - scope.$watch(function(scope) { - var optionGroups = {'':[]}, // Temporary location for the option groups before we render them - optionGroupNames = [''], - optionGroupName, - optionGroup, - option, - existingParent, existingOptions, existingOption, - values = valuesFn(scope) || [], - keys = values, - key, - groupLength, length, - fragment, - groupIndex, index, - optionElement, - optionScope = scope.$new(), - modelValue = model.get(), - selected, - selectedSet = false, // nothing is selected yet - isMulti = isMultiselect, - lastElement, - element; - - try { - if (isMulti) { - selectedSet = new HashMap(); - if (modelValue && isNumber(length = modelValue.length)) { - for (index = 0; index < length; index++) { - selectedSet.put(modelValue[index], true); - } - } - } else if (modelValue === null || nullOption) { - // if we are not multiselect, and we are null then we have to add the nullOption - optionGroups[''].push(extend({selected:modelValue === null, id:'', label:''}, nullOption)); - selectedSet = true; - } - - // If we have a keyName then we are iterating over on object. Grab the keys and sort them. - if(keyName) { - keys = []; - for (key in values) { - if (values.hasOwnProperty(key)) - keys.push(key); - } - keys.sort(); - } - - // We now build up the list of options we need (we merge later) - for (index = 0; length = keys.length, index < length; index++) { - optionScope[valueName] = values[keyName ? optionScope[keyName]=keys[index]:index]; - optionGroupName = groupByFn(optionScope) || ''; - if (!(optionGroup = optionGroups[optionGroupName])) { - optionGroup = optionGroups[optionGroupName] = []; - optionGroupNames.push(optionGroupName); - } - if (isMulti) { - selected = !!selectedSet.remove(valueFn(optionScope)); - } else { - selected = modelValue === valueFn(optionScope); - selectedSet = selectedSet || selected; // see if at least one item is selected - } - optionGroup.push({ - id: keyName ? keys[index] : index, // either the index into array or key from object - label: displayFn(optionScope) || '', // what will be seen by the user - selected: selected // determine if we should be selected - }); - } - optionGroupNames.sort(); - if (!isMulti && !selectedSet) { - // nothing was selected, we have to insert the undefined item - optionGroups[''].unshift({id:'?', label:'', selected:true}); - } - - // Now we need to update the list of DOM nodes to match the optionGroups we computed above - for (groupIndex = 0, groupLength = optionGroupNames.length; - groupIndex < groupLength; - groupIndex++) { - // current option group name or '' if no group - optionGroupName = optionGroupNames[groupIndex]; - - // list of options for that group. (first item has the parent) - optionGroup = optionGroups[optionGroupName]; - - if (optionGroupsCache.length <= groupIndex) { - // we need to grow the optionGroups - optionGroupsCache.push( - existingOptions = [ - existingParent = { - element: optGroupTemplate.clone().attr('label', optionGroupName), - label: optionGroup.label - } - ] - ); - selectElement.append(existingParent.element); - } else { - existingOptions = optionGroupsCache[groupIndex]; - existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element - - // update the OPTGROUP label if not the same. - if (existingParent.label != optionGroupName) { - existingParent.element.attr('label', existingParent.label = optionGroupName); - } - } - - lastElement = null; // start at the begining - for(index = 0, length = optionGroup.length; index < length; index++) { - option = optionGroup[index]; - if ((existingOption = existingOptions[index+1])) { - // reuse elements - lastElement = existingOption.element; - if (existingOption.label !== option.label) { - lastElement.text(existingOption.label = option.label); - } - if (existingOption.id !== option.id) { - lastElement.val(existingOption.id = option.id); - } - if (!inChangeEvent && existingOption.selected !== option.selected) { - lastElement.prop('selected', (existingOption.selected = option.selected)); - } - } else { - // grow elements - // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but - // in this version of jQuery on some browser the .text() returns a string - // rather then the element. - (element = optionTemplate.clone()) - .val(option.id) - .attr('selected', option.selected) - .text(option.label); - existingOptions.push(existingOption = { - element: element, - label: option.label, - id: option.id, - selected: option.selected - }); - if (lastElement) { - lastElement.after(element); - } else { - existingParent.element.append(element); - } - lastElement = element; - } - } - // remove any excessive OPTIONs in a group - index++; // increment since the existingOptions[0] is parent element not OPTION - while(existingOptions.length > index) { - existingOptions.pop().element.remove(); - } - } - // remove any excessive OPTGROUPs from select - while(optionGroupsCache.length > groupIndex) { - optionGroupsCache.pop()[0].element.remove(); - } - } finally { - optionScope.$destroy(); - } - }); - }; -}); - - -/** - * @workInProgress - * @ngdoc widget * @name angular.widget.ng:include * * @description @@ -960,28 +48,36 @@ angularWidget('select', function(element){ * @example <doc:example> <doc:source jsfiddle="false"> - <select name="url"> - <option value="examples/ng-include/template1.html">template1.html</option> - <option value="examples/ng-include/template2.html">template2.html</option> - <option value="">(blank)</option> - </select> - url of the template: <tt><a href="{{url}}">{{url}}</a></tt> - <hr/> - <ng:include src="url"></ng:include> + <script> + function Ctrl(){ + this.templates = + [ { name: 'template1.html', url: 'examples/ng-include/template1.html'} + , { name: 'template2.html', url: 'examples/ng-include/template2.html'} ]; + this.template = this.templates[0]; + } + </script> + <div ng:controller="Ctrl"> + <select ng:model="template" ng:options="t.name for t in templates"> + <option value="">(blank)</option> + </select> + url of the template: <tt><a href="{{template.url}}">{{template.url}}</a></tt> + <hr/> + <ng:include src="template.url"></ng:include> + </div> </doc:source> <doc:scenario> it('should load template1.html', function(){ - expect(element('.doc-example-live ng\\:include').text()). + expect(element('.doc-example-live .ng-include').text()). toBe('Content of template1.html\n'); }); it('should load template2.html', function(){ - select('url').option('examples/ng-include/template2.html'); - expect(element('.doc-example-live ng\\:include').text()). + select('template').option('1'); + expect(element('.doc-example-live .ng-include').text()). toBe('Content of template2.html\n'); }); it('should change to blank', function(){ - select('url').option(''); - expect(element('.doc-example-live ng\\:include').text()).toEqual(''); + select('template').option(''); + expect(element('.doc-example-live .ng-include').text()).toEqual(''); }); </doc:scenario> </doc:example> @@ -1064,30 +160,34 @@ angularWidget('ng:include', function(element){ * @example <doc:example> <doc:source> - <select name="switch"> - <option>settings</option> - <option>home</option> - <option>other</option> - </select> - <tt>switch={{switch}}</tt> - </hr> - <ng:switch on="switch" > - <div ng:switch-when="settings">Settings Div</div> - <span ng:switch-when="home">Home Span</span> - <span ng:switch-default>default</span> - </ng:switch> - </code> + <script> + function Ctrl(){ + this.items = ['settings', 'home', 'other']; + this.selection = this.items[0]; + } + </script> + <div ng:controller="Ctrl"> + <select ng:model="selection" ng:options="item for item in items"> + </select> + <tt>selection={{selection}}</tt> + <hr/> + <ng:switch on="selection" > + <div ng:switch-when="settings">Settings Div</div> + <span ng:switch-when="home">Home Span</span> + <span ng:switch-default>default</span> + </ng:switch> + </div> </doc:source> <doc:scenario> it('should start in settings', function(){ expect(element('.doc-example-live ng\\:switch').text()).toEqual('Settings Div'); }); it('should change to home', function(){ - select('switch').option('home'); + select('selection').option('home'); expect(element('.doc-example-live ng\\:switch').text()).toEqual('Home Span'); }); it('should select deafault', function(){ - select('switch').option('other'); + select('selection').option('other'); expect(element('.doc-example-live ng\\:switch').text()).toEqual('default'); }); </doc:scenario> @@ -1568,27 +668,36 @@ angularWidget('ng:view', function(element) { * @example <doc:example> <doc:source> - Person 1:<input type="text" name="person1" value="Igor" /><br/> - Person 2:<input type="text" name="person2" value="Misko" /><br/> - Number of People:<input type="text" name="personCount" value="1" /><br/> - - <!--- Example with simple pluralization rules for en locale ---> - Without Offset: - <ng:pluralize count="personCount" - when="{'0': 'Nobody is viewing.', - 'one': '1 person is viewing.', - 'other': '{} people are viewing.'}"> - </ng:pluralize><br> - - <!--- Example with offset ---> - With Offset(2): - <ng:pluralize count="personCount" offset=2 - when="{'0': 'Nobody is viewing.', - '1': '{{person1}} is viewing.', - '2': '{{person1}} and {{person2}} are viewing.', - 'one': '{{person1}}, {{person2}} and one other person are viewing.', - 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}"> - </ng:pluralize> + <script> + function Ctrl(){ + this.person1 = 'Igor'; + this.person2 = 'Misko'; + this.personCount = 1; + } + </script> + <div ng:controller="Ctrl"> + Person 1:<input type="text" ng:model="person1" value="Igor" /><br/> + Person 2:<input type="text" ng:model="person2" value="Misko" /><br/> + Number of People:<input type="text" ng:model="personCount" value="1" /><br/> + + <!--- Example with simple pluralization rules for en locale ---> + Without Offset: + <ng:pluralize count="personCount" + when="{'0': 'Nobody is viewing.', + 'one': '1 person is viewing.', + 'other': '{} people are viewing.'}"> + </ng:pluralize><br> + + <!--- Example with offset ---> + With Offset(2): + <ng:pluralize count="personCount" offset=2 + when="{'0': 'Nobody is viewing.', + '1': '{{person1}} is viewing.', + '2': '{{person1}} and {{person2}} are viewing.', + 'one': '{{person1}}, {{person2}} and one other person are viewing.', + 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}"> + </ng:pluralize> + </div> </doc:source> <doc:scenario> it('should show correct pluralized string', function(){ |
