@@ -16,6 +16,8 @@ var SelectController =
16
16
var self = this ,
17
17
optionsMap = new HashMap ( ) ;
18
18
19
+ self . selectValueMap = { } ; // Keys are the hashed values, values the original values
20
+
19
21
// If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
20
22
self . ngModelCtrl = noopNgModelController ;
21
23
@@ -46,8 +48,15 @@ var SelectController =
46
48
// Read the value of the select control, the implementation of this changes depending
47
49
// upon whether the select can have multiple values and whether ngOptions is at work.
48
50
self . readValue = function readSingleValue ( ) {
49
- self . removeUnknownOption ( ) ;
50
- return $element . val ( ) ;
51
+ var val = $element . val ( ) ;
52
+ // ngValue added option values are stored in the selectValueMap, normal interpolations are not
53
+ var realVal = val in self . selectValueMap ? self . selectValueMap [ val ] : val ;
54
+
55
+ if ( self . hasOption ( realVal ) ) {
56
+ return realVal ;
57
+ }
58
+
59
+ return null ;
51
60
} ;
52
61
53
62
@@ -56,7 +65,9 @@ var SelectController =
56
65
self . writeValue = function writeSingleValue ( value ) {
57
66
if ( self . hasOption ( value ) ) {
58
67
self . removeUnknownOption ( ) ;
59
- $element . val ( value ) ;
68
+ var hashedVal = hashKey ( value ) ;
69
+ $element . val ( hashedVal in self . selectValueMap ? hashedVal : value ) ;
70
+
60
71
if ( value === '' ) self . emptyOption . prop ( 'selected' , true ) ; // to make IE9 happy
61
72
} else {
62
73
if ( value == null && self . emptyOption ) {
@@ -104,11 +115,53 @@ var SelectController =
104
115
} ;
105
116
106
117
118
+
119
+ var updateScheduled = false ;
120
+ function scheduleViewValueUpdate ( renderAfter ) {
121
+ if ( updateScheduled ) return ;
122
+
123
+ updateScheduled = true ;
124
+
125
+ $scope . $$postDigest ( function ( ) {
126
+ updateScheduled = false ;
127
+ self . ngModelCtrl . $setViewValue ( self . readValue ( ) ) ;
128
+ if ( renderAfter ) self . ngModelCtrl . $render ( ) ;
129
+ } ) ;
130
+ }
131
+
132
+
107
133
self . registerOption = function ( optionScope , optionElement , optionAttrs , interpolateValueFn , interpolateTextFn ) {
108
134
109
- if ( interpolateValueFn ) {
135
+ if ( optionAttrs . $attr . ngValue ) {
136
+ // The value attribute is set by ngValue
137
+ var oldVal , hashedVal = NaN ;
138
+ optionAttrs . $observe ( 'value' , function valueAttributeObserveAction ( newVal ) {
139
+
140
+ var removal ;
141
+ var previouslySelected = optionElement . prop ( 'selected' ) ;
142
+
143
+ if ( isDefined ( hashedVal ) ) {
144
+ self . removeOption ( oldVal ) ;
145
+ delete self . selectValueMap [ hashedVal ] ;
146
+ removal = true ;
147
+ }
148
+
149
+ hashedVal = hashKey ( newVal ) ;
150
+ oldVal = newVal ;
151
+ self . selectValueMap [ hashedVal ] = newVal ;
152
+ self . addOption ( newVal , optionElement ) ;
153
+ // Set the attribute directly instead of using optionAttrs.$set - this stops the observer
154
+ // from firing a second time. Other $observers on value will also get the result of the
155
+ // ngValue expression, not the hashed value
156
+ optionElement . attr ( 'value' , hashedVal ) ;
157
+
158
+ if ( removal && previouslySelected ) {
159
+ scheduleViewValueUpdate ( ) ;
160
+ }
161
+
162
+ } ) ;
163
+ } else if ( interpolateValueFn ) {
110
164
// The value attribute is interpolated
111
- var oldVal ;
112
165
optionAttrs . $observe ( 'value' , function valueAttributeObserveAction ( newVal ) {
113
166
if ( isDefined ( oldVal ) ) {
114
167
self . removeOption ( oldVal ) ;
@@ -143,7 +196,7 @@ var SelectController =
143
196
* @restrict E
144
197
*
145
198
* @description
146
- * HTML `SELECT ` element with angular data-binding.
199
+ * HTML `select ` element with angular data-binding.
147
200
*
148
201
* The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
149
202
* between the scope and the `<select>` control (including setting default values).
@@ -153,14 +206,24 @@ var SelectController =
153
206
* When an item in the `<select>` menu is selected, the value of the selected option will be bound
154
207
* to the model identified by the `ngModel` directive. With static or repeated options, this is
155
208
* the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing.
156
- * If you want dynamic value attributes, you can use interpolation inside the value attribute .
209
+ * Value and textContent can be interpolated .
157
210
*
158
- * <div class="alert alert-warning">
159
- * Note that the value of a `select` directive used without `ngOptions` is always a string.
160
- * When the model needs to be bound to a non-string value, you must either explicitly convert it
161
- * using a directive (see example below) or use `ngOptions` to specify the set of options.
162
- * This is because an option element can only be bound to string values at present.
163
- * </div>
211
+ * ## Matching model and option values
212
+ *
213
+ * In general, the match between the model and an option is evaluated by strictly comparing the model
214
+ * value against the value of the available options.
215
+ *
216
+ * If you are setting the option value with the option's `value` attribute, or textContent, the
217
+ * value will always be a `string` which means that the model value must also be a string.
218
+ * Otherwise the `select` directive cannot match them correctly.
219
+ *
220
+ * To bind the model to a non-string value, you can use one of the following strategies:
221
+ * - the {@link ng.ngOptions `ngOptions`} directive
222
+ * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value})
223
+ * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be
224
+ * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example})
225
+ * - model $parsers / $formatters to convert the string value
226
+ * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example})
164
227
*
165
228
* If the viewValue of `ngModel` does not match any of the options, then the control
166
229
* will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
@@ -169,13 +232,17 @@ var SelectController =
169
232
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
170
233
* option. See example below for demonstration.
171
234
*
172
- * <div class="alert alert-info">
235
+ * ## Choosing between `ngRepeat` and `ngOptions`
236
+ *
173
237
* In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
174
- * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as
175
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
176
- * comprehension expression, and additionally in reducing memory and increasing speed by not creating
177
- * a new scope for each repeated instance.
178
- * </div>
238
+ * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
239
+ * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
240
+ * comprehension expression
241
+ * - reduced memory consumption by not creating a new scope for each repeated instance
242
+ * - increased render speed by creating the options in a documentFragment instead of individually
243
+ *
244
+ * Specifically, select with repeated options slows down significantly starting at 2000 options in
245
+ * Chrome and Internet Explorer / Edge.
179
246
*
180
247
*
181
248
* @param {string } ngModel Assignable angular expression to data-bind to.
@@ -241,24 +308,24 @@ var SelectController =
241
308
*</example>
242
309
*
243
310
* ### Using `ngRepeat` to generate `select` options
244
- * <example name="ngrepeat- select" module="ngrepeatSelect">
311
+ * <example name="select-ngrepeat " module="ngrepeatSelect">
245
312
* <file name="index.html">
246
313
* <div ng-controller="ExampleController">
247
314
* <form name="myForm">
248
315
* <label for="repeatSelect"> Repeat select: </label>
249
- * <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect ">
316
+ * <select name="repeatSelect" id="repeatSelect" ng-model="data.model ">
250
317
* <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option>
251
318
* </select>
252
319
* </form>
253
320
* <hr>
254
- * <tt>repeatSelect = {{data.repeatSelect }}</tt><br/>
321
+ * <tt>model = {{data.model }}</tt><br/>
255
322
* </div>
256
323
* </file>
257
324
* <file name="app.js">
258
325
* angular.module('ngrepeatSelect', [])
259
326
* .controller('ExampleController', ['$scope', function($scope) {
260
327
* $scope.data = {
261
- * repeatSelect : null,
328
+ * model : null,
262
329
* availableOptions: [
263
330
* {id: '1', name: 'Option A'},
264
331
* {id: '2', name: 'Option B'},
@@ -269,6 +336,37 @@ var SelectController =
269
336
* </file>
270
337
*</example>
271
338
*
339
+ * ### Using `ngValue` to bind the model to an array of objects
340
+ * <example name="select-ngvalue" module="ngvalueSelect">
341
+ * <file name="index.html">
342
+ * <div ng-controller="ExampleController">
343
+ * <form name="myForm">
344
+ * <label for="ngvalueselect"> ngvalue select: </label>
345
+ * <select size="6" name="ngvalueselect" ng-model="data.model" multiple>
346
+ * <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option>
347
+ * </select>
348
+ * </form>
349
+ * <hr>
350
+ * <pre>model = {{data.model | json}}</pre><br/>
351
+ * </div>
352
+ * </file>
353
+ * <file name="app.js">
354
+ * angular.module('ngvalueSelect', [])
355
+ * .controller('ExampleController', ['$scope', function($scope) {
356
+ * $scope.data = {
357
+ * model: null,
358
+ * availableOptions: [
359
+ {value: 'myString', name: 'string'},
360
+ {value: 1, name: 'integer'},
361
+ {value: true, name: 'boolean'},
362
+ {value: null, name: 'null'},
363
+ {value: {prop: 'value'}, name: 'object'},
364
+ {value: ['a'], name: 'array'}
365
+ * ]
366
+ * };
367
+ * }]);
368
+ * </file>
369
+ *</example>
272
370
*
273
371
* ### Using `select` with `ngOptions` and setting a default value
274
372
* See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
@@ -368,6 +466,7 @@ var selectDirective = function() {
368
466
// to the `readValue` method, which can be changed if the select can have multiple
369
467
// selected values or if the options are being generated by `ngOptions`
370
468
element . on ( 'change' , function ( ) {
469
+ selectCtrl . removeUnknownOption ( ) ;
371
470
scope . $apply ( function ( ) {
372
471
ngModelCtrl . $setViewValue ( selectCtrl . readValue ( ) ) ;
373
472
} ) ;
@@ -384,7 +483,8 @@ var selectDirective = function() {
384
483
var array = [ ] ;
385
484
forEach ( element . find ( 'option' ) , function ( option ) {
386
485
if ( option . selected ) {
387
- array . push ( option . value ) ;
486
+ var val = option . value ;
487
+ array . push ( val in selectCtrl . selectValueMap ? selectCtrl . selectValueMap [ val ] : val ) ;
388
488
}
389
489
} ) ;
390
490
return array ;
@@ -394,7 +494,7 @@ var selectDirective = function() {
394
494
selectCtrl . writeValue = function writeMultipleValue ( value ) {
395
495
var items = new HashMap ( value ) ;
396
496
forEach ( element . find ( 'option' ) , function ( option ) {
397
- option . selected = isDefined ( items . get ( option . value ) ) ;
497
+ option . selected = isDefined ( items . get ( option . value ) ) || isDefined ( items . get ( selectCtrl . selectValueMap [ option . value ] ) ) ;
398
498
} ) ;
399
499
} ;
400
500
@@ -445,13 +545,18 @@ var optionDirective = ['$interpolate', function($interpolate) {
445
545
restrict : 'E' ,
446
546
priority : 100 ,
447
547
compile : function ( element , attr ) {
448
- if ( isDefined ( attr . value ) ) {
548
+ var interpolateValueFn , interpolateTextFn ;
549
+
550
+ if ( isDefined ( attr . ngValue ) ) {
551
+ // jshint noempty: false
552
+ // Will be handled by registerOption
553
+ } else if ( isDefined ( attr . value ) ) {
449
554
// If the value attribute is defined, check if it contains an interpolation
450
- var interpolateValueFn = $interpolate ( attr . value , true ) ;
555
+ interpolateValueFn = $interpolate ( attr . value , true ) ;
451
556
} else {
452
557
// If the value attribute is not defined then we fall back to the
453
558
// text content of the option element, which may be interpolated
454
- var interpolateTextFn = $interpolate ( element . text ( ) , true ) ;
559
+ interpolateTextFn = $interpolate ( element . text ( ) , true ) ;
455
560
if ( ! interpolateTextFn ) {
456
561
attr . $set ( 'value' , element . text ( ) ) ;
457
562
}
0 commit comments