feat(select): support values of any type added with ngValue
select elements with ngModel will now set ngModel to option values added by ngValue. This allows setting values of any type (not only strings) without the use of ngOptions. Interpolations inside attributes can only be strings, but the ngValue directive uses attrs.$set, which does not have any type restriction. Any $observe on the value attribute will therefore receive the original value (result of ngValue expression). However, when a user selects an option, the browser sets the select value to the actual option's value attribute, which is still always a string. For that reason, when options are added by ngValue, we set the hashed value of the original value in the value attribute and store the actual value in an extra map. When the select value changes, we read access the actual value via the hashed select value. Since we only use a hashed value for ngValue, we will have extra checks for the hashed values: - when options are read, for both single and multiple select - when options are written, for multiple select I don't expect this to have a performance impact, but it should be kept in mind. Closes #9842 Closes #6297 BREAKING CHANGE: `<option>` elements added to `<select ng-model>` via `ngValue` now add their values in hash form, i.e. `<option ng-value="myString">` becomes `<option ng-value="myString" value="string:myString">`. This is done to support binding options with values of any type to selects. This should rarely affect applications, as the values of options are usually not relevant to the application logic, but it's possible that option values are checked in tests.
This commit is contained in:
committed by
Martin Staffa
parent
47fbbabe0b
commit
f02b707b5e
@@ -1743,10 +1743,8 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
|
||||
* `ngValue` is useful when dynamically generating lists of radio buttons using
|
||||
* {@link ngRepeat `ngRepeat`}, as shown below.
|
||||
*
|
||||
* Likewise, `ngValue` can be used to generate `<option>` elements for
|
||||
* the {@link select `select`} element. In that case however, only strings are supported
|
||||
* for the `value `attribute, so the resulting `ngModel` will always be a string.
|
||||
* Support for `select` models with non-string values is available via `ngOptions`.
|
||||
* Likewise, `ngValue` can be used to set the value of `<option>` elements for
|
||||
* the {@link select `select`} element.
|
||||
*
|
||||
* @element input
|
||||
* @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
|
||||
|
||||
@@ -15,13 +15,12 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* elements for the `<select>` element using the array or object obtained by evaluating the
|
||||
* `ngOptions` comprehension expression.
|
||||
*
|
||||
* In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a
|
||||
* similar result. However, `ngOptions` provides some benefits such as reducing memory and
|
||||
* increasing speed by not creating a new scope for each repeated instance, as well as providing
|
||||
* more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
|
||||
* comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound
|
||||
* to a non-string value. This is because an option element can only be bound to string values at
|
||||
* present.
|
||||
* In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
|
||||
* ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
|
||||
* - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
|
||||
* comprehension expression
|
||||
* - reduced memory consumption by not creating a new scope for each repeated instance
|
||||
* - increased render speed by creating the options in a documentFragment instead of individually
|
||||
*
|
||||
* When an item in the `<select>` menu is selected, the array element or object property
|
||||
* represented by the selected option will be bound to the model identified by the `ngModel`
|
||||
|
||||
+133
-28
@@ -16,6 +16,8 @@ var SelectController =
|
||||
var self = this,
|
||||
optionsMap = new HashMap();
|
||||
|
||||
self.selectValueMap = {}; // Keys are the hashed values, values the original values
|
||||
|
||||
// If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
|
||||
self.ngModelCtrl = noopNgModelController;
|
||||
|
||||
@@ -46,8 +48,15 @@ var SelectController =
|
||||
// Read the value of the select control, the implementation of this changes depending
|
||||
// upon whether the select can have multiple values and whether ngOptions is at work.
|
||||
self.readValue = function readSingleValue() {
|
||||
self.removeUnknownOption();
|
||||
return $element.val();
|
||||
var val = $element.val();
|
||||
// ngValue added option values are stored in the selectValueMap, normal interpolations are not
|
||||
var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val;
|
||||
|
||||
if (self.hasOption(realVal)) {
|
||||
return realVal;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -56,7 +65,9 @@ var SelectController =
|
||||
self.writeValue = function writeSingleValue(value) {
|
||||
if (self.hasOption(value)) {
|
||||
self.removeUnknownOption();
|
||||
$element.val(value);
|
||||
var hashedVal = hashKey(value);
|
||||
$element.val(hashedVal in self.selectValueMap ? hashedVal : value);
|
||||
|
||||
if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
|
||||
} else {
|
||||
if (value == null && self.emptyOption) {
|
||||
@@ -104,11 +115,53 @@ var SelectController =
|
||||
};
|
||||
|
||||
|
||||
|
||||
var updateScheduled = false;
|
||||
function scheduleViewValueUpdate(renderAfter) {
|
||||
if (updateScheduled) return;
|
||||
|
||||
updateScheduled = true;
|
||||
|
||||
$scope.$$postDigest(function() {
|
||||
updateScheduled = false;
|
||||
self.ngModelCtrl.$setViewValue(self.readValue());
|
||||
if (renderAfter) self.ngModelCtrl.$render();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
|
||||
|
||||
if (interpolateValueFn) {
|
||||
if (optionAttrs.$attr.ngValue) {
|
||||
// The value attribute is set by ngValue
|
||||
var oldVal, hashedVal = NaN;
|
||||
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
|
||||
|
||||
var removal;
|
||||
var previouslySelected = optionElement.prop('selected');
|
||||
|
||||
if (isDefined(hashedVal)) {
|
||||
self.removeOption(oldVal);
|
||||
delete self.selectValueMap[hashedVal];
|
||||
removal = true;
|
||||
}
|
||||
|
||||
hashedVal = hashKey(newVal);
|
||||
oldVal = newVal;
|
||||
self.selectValueMap[hashedVal] = newVal;
|
||||
self.addOption(newVal, optionElement);
|
||||
// Set the attribute directly instead of using optionAttrs.$set - this stops the observer
|
||||
// from firing a second time. Other $observers on value will also get the result of the
|
||||
// ngValue expression, not the hashed value
|
||||
optionElement.attr('value', hashedVal);
|
||||
|
||||
if (removal && previouslySelected) {
|
||||
scheduleViewValueUpdate();
|
||||
}
|
||||
|
||||
});
|
||||
} else if (interpolateValueFn) {
|
||||
// The value attribute is interpolated
|
||||
var oldVal;
|
||||
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
|
||||
if (isDefined(oldVal)) {
|
||||
self.removeOption(oldVal);
|
||||
@@ -143,7 +196,7 @@ var SelectController =
|
||||
* @restrict E
|
||||
*
|
||||
* @description
|
||||
* HTML `SELECT` element with angular data-binding.
|
||||
* HTML `select` element with angular data-binding.
|
||||
*
|
||||
* The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
|
||||
* between the scope and the `<select>` control (including setting default values).
|
||||
@@ -153,14 +206,24 @@ var SelectController =
|
||||
* When an item in the `<select>` menu is selected, the value of the selected option will be bound
|
||||
* to the model identified by the `ngModel` directive. With static or repeated options, this is
|
||||
* the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing.
|
||||
* If you want dynamic value attributes, you can use interpolation inside the value attribute.
|
||||
* Value and textContent can be interpolated.
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* Note that the value of a `select` directive used without `ngOptions` is always a string.
|
||||
* When the model needs to be bound to a non-string value, you must either explicitly convert it
|
||||
* using a directive (see example below) or use `ngOptions` to specify the set of options.
|
||||
* This is because an option element can only be bound to string values at present.
|
||||
* </div>
|
||||
* ## Matching model and option values
|
||||
*
|
||||
* In general, the match between the model and an option is evaluated by strictly comparing the model
|
||||
* value against the value of the available options.
|
||||
*
|
||||
* If you are setting the option value with the option's `value` attribute, or textContent, the
|
||||
* value will always be a `string` which means that the model value must also be a string.
|
||||
* Otherwise the `select` directive cannot match them correctly.
|
||||
*
|
||||
* To bind the model to a non-string value, you can use one of the following strategies:
|
||||
* - the {@link ng.ngOptions `ngOptions`} directive
|
||||
* ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value})
|
||||
* - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be
|
||||
* option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example})
|
||||
* - model $parsers / $formatters to convert the string value
|
||||
* ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example})
|
||||
*
|
||||
* If the viewValue of `ngModel` does not match any of the options, then the control
|
||||
* will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
|
||||
@@ -169,13 +232,17 @@ var SelectController =
|
||||
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
|
||||
* option. See example below for demonstration.
|
||||
*
|
||||
* <div class="alert alert-info">
|
||||
* ## Choosing between `ngRepeat` and `ngOptions`
|
||||
*
|
||||
* In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
|
||||
* ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as
|
||||
* more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
|
||||
* comprehension expression, and additionally in reducing memory and increasing speed by not creating
|
||||
* a new scope for each repeated instance.
|
||||
* </div>
|
||||
* ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
|
||||
* - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
|
||||
* comprehension expression
|
||||
* - reduced memory consumption by not creating a new scope for each repeated instance
|
||||
* - increased render speed by creating the options in a documentFragment instead of individually
|
||||
*
|
||||
* Specifically, select with repeated options slows down significantly starting at 2000 options in
|
||||
* Chrome and Internet Explorer / Edge.
|
||||
*
|
||||
*
|
||||
* @param {string} ngModel Assignable angular expression to data-bind to.
|
||||
@@ -241,24 +308,24 @@ var SelectController =
|
||||
*</example>
|
||||
*
|
||||
* ### Using `ngRepeat` to generate `select` options
|
||||
* <example name="ngrepeat-select" module="ngrepeatSelect">
|
||||
* <example name="select-ngrepeat" module="ngrepeatSelect">
|
||||
* <file name="index.html">
|
||||
* <div ng-controller="ExampleController">
|
||||
* <form name="myForm">
|
||||
* <label for="repeatSelect"> Repeat select: </label>
|
||||
* <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect">
|
||||
* <select name="repeatSelect" id="repeatSelect" ng-model="data.model">
|
||||
* <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option>
|
||||
* </select>
|
||||
* </form>
|
||||
* <hr>
|
||||
* <tt>repeatSelect = {{data.repeatSelect}}</tt><br/>
|
||||
* <tt>model = {{data.model}}</tt><br/>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="app.js">
|
||||
* angular.module('ngrepeatSelect', [])
|
||||
* .controller('ExampleController', ['$scope', function($scope) {
|
||||
* $scope.data = {
|
||||
* repeatSelect: null,
|
||||
* model: null,
|
||||
* availableOptions: [
|
||||
* {id: '1', name: 'Option A'},
|
||||
* {id: '2', name: 'Option B'},
|
||||
@@ -269,6 +336,37 @@ var SelectController =
|
||||
* </file>
|
||||
*</example>
|
||||
*
|
||||
* ### Using `ngValue` to bind the model to an array of objects
|
||||
* <example name="select-ngvalue" module="ngvalueSelect">
|
||||
* <file name="index.html">
|
||||
* <div ng-controller="ExampleController">
|
||||
* <form name="myForm">
|
||||
* <label for="ngvalueselect"> ngvalue select: </label>
|
||||
* <select size="6" name="ngvalueselect" ng-model="data.model" multiple>
|
||||
* <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option>
|
||||
* </select>
|
||||
* </form>
|
||||
* <hr>
|
||||
* <pre>model = {{data.model | json}}</pre><br/>
|
||||
* </div>
|
||||
* </file>
|
||||
* <file name="app.js">
|
||||
* angular.module('ngvalueSelect', [])
|
||||
* .controller('ExampleController', ['$scope', function($scope) {
|
||||
* $scope.data = {
|
||||
* model: null,
|
||||
* availableOptions: [
|
||||
{value: 'myString', name: 'string'},
|
||||
{value: 1, name: 'integer'},
|
||||
{value: true, name: 'boolean'},
|
||||
{value: null, name: 'null'},
|
||||
{value: {prop: 'value'}, name: 'object'},
|
||||
{value: ['a'], name: 'array'}
|
||||
* ]
|
||||
* };
|
||||
* }]);
|
||||
* </file>
|
||||
*</example>
|
||||
*
|
||||
* ### Using `select` with `ngOptions` and setting a default value
|
||||
* See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
|
||||
@@ -368,6 +466,7 @@ var selectDirective = function() {
|
||||
// to the `readValue` method, which can be changed if the select can have multiple
|
||||
// selected values or if the options are being generated by `ngOptions`
|
||||
element.on('change', function() {
|
||||
selectCtrl.removeUnknownOption();
|
||||
scope.$apply(function() {
|
||||
ngModelCtrl.$setViewValue(selectCtrl.readValue());
|
||||
});
|
||||
@@ -384,7 +483,8 @@ var selectDirective = function() {
|
||||
var array = [];
|
||||
forEach(element.find('option'), function(option) {
|
||||
if (option.selected) {
|
||||
array.push(option.value);
|
||||
var val = option.value;
|
||||
array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val);
|
||||
}
|
||||
});
|
||||
return array;
|
||||
@@ -394,7 +494,7 @@ var selectDirective = function() {
|
||||
selectCtrl.writeValue = function writeMultipleValue(value) {
|
||||
var items = new HashMap(value);
|
||||
forEach(element.find('option'), function(option) {
|
||||
option.selected = isDefined(items.get(option.value));
|
||||
option.selected = isDefined(items.get(option.value)) || isDefined(items.get(selectCtrl.selectValueMap[option.value]));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -445,13 +545,18 @@ var optionDirective = ['$interpolate', function($interpolate) {
|
||||
restrict: 'E',
|
||||
priority: 100,
|
||||
compile: function(element, attr) {
|
||||
if (isDefined(attr.value)) {
|
||||
var interpolateValueFn, interpolateTextFn;
|
||||
|
||||
if (isDefined(attr.ngValue)) {
|
||||
// jshint noempty: false
|
||||
// Will be handled by registerOption
|
||||
} else if (isDefined(attr.value)) {
|
||||
// If the value attribute is defined, check if it contains an interpolation
|
||||
var interpolateValueFn = $interpolate(attr.value, true);
|
||||
interpolateValueFn = $interpolate(attr.value, true);
|
||||
} else {
|
||||
// If the value attribute is not defined then we fall back to the
|
||||
// text content of the option element, which may be interpolated
|
||||
var interpolateTextFn = $interpolate(element.text(), true);
|
||||
interpolateTextFn = $interpolate(element.text(), true);
|
||||
if (!interpolateTextFn) {
|
||||
attr.$set('value', element.text());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
describe('select', function() {
|
||||
var scope, formElement, element, $compile, ngModelCtrl, selectCtrl, renderSpy;
|
||||
var scope, formElement, element, $compile, ngModelCtrl, selectCtrl, renderSpy, optionAttributesList = [];
|
||||
|
||||
function compile(html) {
|
||||
formElement = jqLite('<form name="form">' + html + '</form>');
|
||||
@@ -55,6 +55,18 @@ describe('select', function() {
|
||||
'</option>'
|
||||
};
|
||||
});
|
||||
|
||||
$compileProvider.directive('exposeAttributes', function() {
|
||||
return {
|
||||
require: '^^select',
|
||||
link: {
|
||||
pre: function(scope, element, attrs, ctrl) {
|
||||
optionAttributesList.push(attrs);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($rootScope, _$compile_) {
|
||||
@@ -297,7 +309,7 @@ describe('select', function() {
|
||||
expect(selectCtrl.writeValue).not.toHaveBeenCalled();
|
||||
|
||||
scope.$digest();
|
||||
expect(selectCtrl.writeValue).toHaveBeenCalledOnce();
|
||||
expect(selectCtrl.writeValue).toHaveBeenCalled();
|
||||
dealoc(select);
|
||||
});
|
||||
|
||||
@@ -1224,5 +1236,224 @@ describe('select', function() {
|
||||
}).toThrowMinErr('ng','badname', 'hasOwnProperty is not a valid "option value" name');
|
||||
});
|
||||
|
||||
describe('with ngValue (and non-primitive values)', function() {
|
||||
|
||||
they('should set the option attribute and select it for value $prop', [
|
||||
'string',
|
||||
undefined,
|
||||
1,
|
||||
true,
|
||||
null,
|
||||
{prop: 'value'},
|
||||
['a'],
|
||||
NaN
|
||||
], function(prop) {
|
||||
scope.option1 = prop;
|
||||
scope.selected = 'NOMATCH';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option1">{{option1}}</option>' +
|
||||
'</select>');
|
||||
|
||||
scope.$digest();
|
||||
expect(element.find('option').eq(0).val()).toBe('? string:NOMATCH ?');
|
||||
|
||||
scope.selected = prop;
|
||||
scope.$digest();
|
||||
|
||||
expect(element.find('option').eq(0).val()).toBe(hashKey(prop));
|
||||
|
||||
// Reset
|
||||
scope.selected = false;
|
||||
scope.$digest();
|
||||
|
||||
expect(element.find('option').eq(0).val()).toBe('? boolean:false ?');
|
||||
|
||||
browserTrigger(element.find('option').eq(0));
|
||||
if (typeof prop === 'number' && isNaN(prop)) {
|
||||
expect(scope.selected).toBeNaN();
|
||||
} else {
|
||||
expect(scope.selected).toBe(prop);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
they('should update the option attribute and select it for value $prop', [
|
||||
'string',
|
||||
undefined,
|
||||
1,
|
||||
true,
|
||||
null,
|
||||
{prop: 'value'},
|
||||
['a'],
|
||||
NaN
|
||||
], function(prop) {
|
||||
scope.option = prop;
|
||||
scope.selected = 'NOMATCH';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option">{{option}}</option>' +
|
||||
'</select>');
|
||||
|
||||
var selectController = element.controller('select');
|
||||
spyOn(selectController, 'removeOption').and.callThrough();
|
||||
|
||||
scope.$digest();
|
||||
expect(selectController.removeOption).not.toHaveBeenCalled();
|
||||
expect(element.find('option').eq(0).val()).toBe('? string:NOMATCH ?');
|
||||
|
||||
scope.selected = prop;
|
||||
scope.$digest();
|
||||
|
||||
expect(element.find('option').eq(0).val()).toBe(hashKey(prop));
|
||||
expect(element[0].selectedIndex).toBe(0);
|
||||
|
||||
scope.option = 'UPDATEDVALUE';
|
||||
scope.$digest();
|
||||
|
||||
expect(selectController.removeOption.calls.count()).toBe(1);
|
||||
|
||||
// Updating the option value currently does not update the select model
|
||||
if (typeof prop === 'number' && isNaN(prop)) {
|
||||
expect(selectController.removeOption.calls.argsFor(0)[0]).toBeNaN();
|
||||
} else {
|
||||
expect(selectController.removeOption.calls.argsFor(0)[0]).toBe(prop);
|
||||
}
|
||||
|
||||
expect(scope.selected).toBe(null);
|
||||
expect(element[0].selectedIndex).toBe(0);
|
||||
expect(element.find('option').length).toBe(2);
|
||||
expect(element.find('option').eq(0).prop('selected')).toBe(true);
|
||||
expect(element.find('option').eq(0).val()).toBe(unknownValue(prop));
|
||||
expect(element.find('option').eq(1).prop('selected')).toBe(false);
|
||||
expect(element.find('option').eq(1).val()).toBe('string:UPDATEDVALUE');
|
||||
|
||||
scope.selected = 'UPDATEDVALUE';
|
||||
scope.$digest();
|
||||
|
||||
expect(element[0].selectedIndex).toBe(0);
|
||||
expect(element.find('option').eq(0).val()).toBe('string:UPDATEDVALUE');
|
||||
});
|
||||
|
||||
it('should interact with custom attribute $observe and $set calls', function() {
|
||||
var log = [], optionAttr;
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option expose-attributes ng-value="option">{{option}}</option>' +
|
||||
'</select>');
|
||||
|
||||
optionAttr = optionAttributesList[0];
|
||||
optionAttr.$observe('value', function(newVal) {
|
||||
log.push(newVal);
|
||||
});
|
||||
|
||||
scope.option = 'init';
|
||||
scope.$digest();
|
||||
|
||||
expect(log[0]).toBe('init');
|
||||
expect(element.find('option').eq(1).val()).toBe('string:init');
|
||||
|
||||
optionAttr.$set('value', 'update');
|
||||
expect(log[1]).toBe('update');
|
||||
expect(element.find('option').eq(1).val()).toBe('string:update');
|
||||
|
||||
});
|
||||
|
||||
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.value = 'def';
|
||||
scope.textvalue = 'ghi';
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.textvalue = 'def';
|
||||
scope.textvalue2 = 'ghi';
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
describe('and select[multiple]', function() {
|
||||
|
||||
it('should allow multiple selection', function() {
|
||||
scope.options = {
|
||||
a: 'string',
|
||||
b: undefined,
|
||||
c: 1,
|
||||
d: true,
|
||||
e: null,
|
||||
f: {prop: 'value'},
|
||||
g: ['a'],
|
||||
h: NaN
|
||||
};
|
||||
scope.selected = [];
|
||||
|
||||
compile('<select multiple ng-model="selected">' +
|
||||
'<option ng-value="options.a">{{options.a}}</option>' +
|
||||
'<option ng-value="options.b">{{options.b}}</option>' +
|
||||
'<option ng-value="options.c">{{options.c}}</option>' +
|
||||
'<option ng-value="options.d">{{options.d}}</option>' +
|
||||
'<option ng-value="options.e">{{options.e}}</option>' +
|
||||
'<option ng-value="options.f">{{options.f}}</option>' +
|
||||
'<option ng-value="options.g">{{options.g}}</option>' +
|
||||
'<option ng-value="options.h">{{options.h}}</option>' +
|
||||
'</select>');
|
||||
|
||||
scope.$digest();
|
||||
expect(element).toEqualSelect(
|
||||
'string:string',
|
||||
'undefined:undefined',
|
||||
'number:1',
|
||||
'boolean:true',
|
||||
'object:null',
|
||||
'object:4',
|
||||
'object:5',
|
||||
'number:NaN'
|
||||
);
|
||||
|
||||
scope.selected = ['string', 1];
|
||||
scope.$digest();
|
||||
|
||||
expect(element.find('option').eq(0).prop('selected')).toBe(true);
|
||||
expect(element.find('option').eq(2).prop('selected')).toBe(true);
|
||||
|
||||
browserTrigger(element.find('option').eq(1));
|
||||
expect(scope.selected).toEqual([undefined]);
|
||||
|
||||
//reset
|
||||
scope.selected = [];
|
||||
scope.$digest();
|
||||
|
||||
forEach(element.find('option'), function(option) {
|
||||
// browserTrigger can't produce click + ctrl, so set selection manually
|
||||
jqLite(option).prop('selected', true);
|
||||
});
|
||||
|
||||
browserTrigger(element, 'change');
|
||||
|
||||
var arrayVal = ['a'];
|
||||
arrayVal.$$hashKey = 'object:5';
|
||||
|
||||
expect(scope.selected).toEqual([
|
||||
'string',
|
||||
undefined,
|
||||
1,
|
||||
true,
|
||||
null,
|
||||
{prop: 'value', $$hashKey: 'object:4'},
|
||||
arrayVal,
|
||||
NaN
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user