22
22
* be nested into the `<select>` element. This element will then represent `null` or "not selected"
23
23
* option. See example below for demonstration.
24
24
*
25
- * Note: `ngOptions` provides iterator facility for `<option>` element which must be used instead
26
- * of {@link angular.module.ng.$compileProvider.directive.ngRepeat ngRepeat}. `ngRepeat` is not
27
- * suitable for use with `<option>` element because of the following reasons:
28
- *
29
- * * value attribute of the option element that we need to bind to requires a string, but the
30
- * source of data for the iteration might be in a form of array containing objects instead of
31
- * strings
32
- * * {@link angular.module.ng.$compileProvider.directive.ngRepeat ngRepeat} unrolls after the
33
- * select binds causing incorect rendering on most browsers.
34
- * * binding to a value not in list confuses most browsers.
25
+ * Note: `ngOptions` provides iterator facility for `<option>` element which should be used instead
26
+ * of {@link angular.module.ng.$compileProvider.directive.ngRepeat ngRepeat} when you want the
27
+ * `select` model to be bound to a non-string value. This is because an option element can currently
28
+ * be bound to string values only.
35
29
*
36
30
* @param {string } name assignable expression to data-bind to.
37
31
* @param {string= } required The control is considered valid only if value is entered.
92
86
<select ng-model="color" ng-options="c.name for c in colors"></select><br>
93
87
94
88
Color (null allowed):
95
- <div class="nullable">
89
+ <span class="nullable">
96
90
<select ng-model="color" ng-options="c.name for c in colors">
97
91
<option value="">-- chose color --</option>
98
92
</select>
99
- </div ><br/>
93
+ </span ><br/>
100
94
101
95
Color grouped by shade:
102
96
<select ng-model="color" ng-options="c.name group by c.shade for c in colors">
126
120
var ngOptionsDirective = valueFn ( { terminal : true } ) ;
127
121
var selectDirective = [ '$compile' , '$parse' , function ( $compile , $parse ) {
128
122
//00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
129
- var NG_OPTIONS_REGEXP = / ^ \s * ( .* ?) (?: \s + a s \s + ( .* ?) ) ? (?: \s + g r o u p \s + b y \s + ( .* ) ) ? \s + f o r \s + (?: ( [ \$ \w ] [ \$ \w \d ] * ) | (?: \( \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * , \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * \) ) ) \s + i n \s + ( .* ) $ / ;
123
+ var NG_OPTIONS_REGEXP = / ^ \s * ( .* ?) (?: \s + a s \s + ( .* ?) ) ? (?: \s + g r o u p \s + b y \s + ( .* ) ) ? \s + f o r \s + (?: ( [ \$ \w ] [ \$ \w \d ] * ) | (?: \( \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * , \s * ( [ \$ \w ] [ \$ \w \d ] * ) \s * \) ) ) \s + i n \s + ( .* ) $ / ,
124
+ nullModelCtrl = { $setViewValue : noop } ;
130
125
131
126
return {
132
127
restrict : 'E' ,
133
- require : '?ngModel' ,
134
- link : function ( scope , element , attr , ctrl ) {
135
- if ( ! ctrl ) return ;
128
+ require : [ 'select' , '?ngModel' ] ,
129
+ controller : [ '$element' , '$scope' , function ( $element , $scope ) {
130
+ var self = this ,
131
+ optionsMap = { } ,
132
+ ngModelCtrl = nullModelCtrl ,
133
+ nullOption ,
134
+ unknownOption ;
135
+
136
+ self . init = function ( ngModelCtrl_ , nullOption_ , unknownOption_ ) {
137
+ ngModelCtrl = ngModelCtrl_ ;
138
+ nullOption = nullOption_ ;
139
+ unknownOption = unknownOption_ ;
140
+ }
141
+
142
+
143
+ self . addOption = function ( value ) {
144
+ optionsMap [ value ] = true ;
145
+
146
+ if ( ngModelCtrl . $viewValue == value ) {
147
+ $element . val ( value ) ;
148
+ if ( unknownOption . parent ( ) ) unknownOption . remove ( ) ;
149
+ }
150
+ } ;
136
151
137
- var multiple = attr . multiple ,
138
- optionsExp = attr . ngOptions ;
152
+
153
+ self . removeOption = function ( value ) {
154
+ if ( this . hasOption ( value ) ) {
155
+ delete optionsMap [ value ] ;
156
+ if ( ngModelCtrl . $viewValue == value ) {
157
+ this . renderUnknownOption ( value ) ;
158
+ }
159
+ }
160
+ } ;
161
+
162
+
163
+ self . renderUnknownOption = function ( val ) {
164
+ var unknownVal = '? ' + hashKey ( val ) + ' ?' ;
165
+ unknownOption . val ( unknownVal ) ;
166
+ $element . prepend ( unknownOption ) ;
167
+ $element . val ( unknownVal ) ;
168
+ unknownOption . prop ( 'selected' , true ) ; // needed for IE
169
+ }
170
+
171
+
172
+ self . hasOption = function ( value ) {
173
+ return optionsMap . hasOwnProperty ( value ) ;
174
+ }
175
+
176
+ $scope . $on ( '$destroy' , function ( ) {
177
+ // disable unknown option so that we don't do work when the whole select is being destroyed
178
+ self . renderUnknownOption = noop ;
179
+ } ) ;
180
+ } ] ,
181
+
182
+ link : function ( scope , element , attr , ctrls ) {
183
+ // if ngModel is not defined, we don't need to do anything
184
+ if ( ! ctrls [ 1 ] ) return ;
185
+
186
+ var selectCtrl = ctrls [ 0 ] ,
187
+ ngModelCtrl = ctrls [ 1 ] ,
188
+ multiple = attr . multiple ,
189
+ optionsExp = attr . ngOptions ,
190
+ nullOption = false , // if false, user will not be able to select it (used by ngOptions)
191
+ emptyOption ,
192
+ // we can't just jqLite('<option>') since jqLite is not smart enough
193
+ // to create it in <select> and IE barfs otherwise.
194
+ optionTemplate = jqLite ( document . createElement ( 'option' ) ) ,
195
+ optGroupTemplate = jqLite ( document . createElement ( 'optgroup' ) ) ,
196
+ unknownOption = optionTemplate . clone ( ) ;
197
+
198
+ // find "null" option
199
+ for ( var i = 0 , children = element . children ( ) , ii = children . length ; i < ii ; i ++ ) {
200
+ if ( children [ i ] . value == '' ) {
201
+ emptyOption = nullOption = children . eq ( i ) ;
202
+ break ;
203
+ }
204
+ }
205
+
206
+ selectCtrl . init ( ngModelCtrl , nullOption , unknownOption ) ;
139
207
140
208
// required validator
141
209
if ( multiple && ( attr . required || attr . ngRequired ) ) {
142
210
var requiredValidator = function ( value ) {
143
- ctrl . $setValidity ( 'required' , ! attr . required || ( value && value . length ) ) ;
211
+ ngModelCtrl . $setValidity ( 'required' , ! attr . required || ( value && value . length ) ) ;
144
212
return value ;
145
213
} ;
146
214
147
- ctrl . $parsers . push ( requiredValidator ) ;
148
- ctrl . $formatters . unshift ( requiredValidator ) ;
215
+ ngModelCtrl . $parsers . push ( requiredValidator ) ;
216
+ ngModelCtrl . $formatters . unshift ( requiredValidator ) ;
149
217
150
218
attr . $observe ( 'required' , function ( ) {
151
- requiredValidator ( ctrl . $viewValue ) ;
219
+ requiredValidator ( ngModelCtrl . $viewValue ) ;
152
220
} ) ;
153
221
}
154
222
155
- if ( optionsExp ) Options ( scope , element , ctrl ) ;
156
- else if ( multiple ) Multiple ( scope , element , ctrl ) ;
157
- else Single ( scope , element , ctrl ) ;
223
+ if ( optionsExp ) Options ( scope , element , ngModelCtrl ) ;
224
+ else if ( multiple ) Multiple ( scope , element , ngModelCtrl ) ;
225
+ else Single ( scope , element , ngModelCtrl , selectCtrl ) ;
158
226
159
227
160
228
////////////////////////////
161
229
162
230
163
231
164
- function Single ( scope , selectElement , ctrl ) {
165
- ctrl . $render = function ( ) {
166
- selectElement . val ( ctrl . $viewValue ) ;
232
+ function Single ( scope , selectElement , ngModelCtrl , selectCtrl ) {
233
+ ngModelCtrl . $render = function ( ) {
234
+ var viewValue = ngModelCtrl . $viewValue ;
235
+
236
+ if ( selectCtrl . hasOption ( viewValue ) ) {
237
+ if ( unknownOption . parent ( ) ) unknownOption . remove ( ) ;
238
+ selectElement . val ( viewValue ) ;
239
+ if ( viewValue === '' ) emptyOption . prop ( 'selected' , true ) ; // to make IE9 happy
240
+ } else {
241
+ if ( isUndefined ( viewValue ) && emptyOption ) {
242
+ selectElement . val ( '' ) ;
243
+ } else {
244
+ selectCtrl . renderUnknownOption ( viewValue ) ;
245
+ }
246
+ }
167
247
} ;
168
248
169
249
selectElement . bind ( 'change' , function ( ) {
170
250
scope . $apply ( function ( ) {
171
- ctrl . $setViewValue ( selectElement . val ( ) ) ;
251
+ if ( unknownOption . parent ( ) ) unknownOption . remove ( ) ;
252
+ ngModelCtrl . $setViewValue ( selectElement . val ( ) ) ;
172
253
} ) ;
173
254
} ) ;
174
255
}
@@ -219,26 +300,26 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
219
300
groupByFn = $parse ( match [ 3 ] || '' ) ,
220
301
valueFn = $parse ( match [ 2 ] ? match [ 1 ] : valueName ) ,
221
302
valuesFn = $parse ( match [ 7 ] ) ,
222
- // we can't just jqLite('<option>') since jqLite is not smart enough
223
- // to create it in <select> and IE barfs otherwise.
224
- optionTemplate = jqLite ( document . createElement ( 'option' ) ) ,
225
- optGroupTemplate = jqLite ( document . createElement ( 'optgroup' ) ) ,
226
- nullOption = false , // if false then user will not be able to select it
227
303
// This is an array of array of existing option groups in DOM. We try to reuse these if possible
228
304
// optionGroupsCache[0] is the options with no option group
229
305
// optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
230
306
optionGroupsCache = [ [ { element : selectElement , label :'' } ] ] ;
231
307
232
- // find existing special options
233
- forEach ( selectElement . children ( ) , function ( option ) {
234
- if ( option . value == '' ) {
235
- // developer declared null option, so user should be able to select it
236
- nullOption = jqLite ( option ) . remove ( ) ;
237
- // compile the element since there might be bindings in it
238
- $compile ( nullOption ) ( scope ) ;
239
- }
240
- } ) ;
241
- selectElement . html ( '' ) ; // clear contents
308
+ if ( nullOption ) {
309
+ // compile the element since there might be bindings in it
310
+ $compile ( nullOption ) ( scope ) ;
311
+
312
+ // remove the class, which is added automatically because we recompile the element and it
313
+ // becomes the compilation root
314
+ nullOption . removeClass ( 'ng-scope' ) ;
315
+
316
+ // we need to remove it before calling selectElement.html('') because otherwise IE will
317
+ // remove the label from the element. wtf?
318
+ nullOption . remove ( ) ;
319
+ }
320
+
321
+ // clear contents, we'll add what's needed based on the model
322
+ selectElement . html ( '' ) ;
242
323
243
324
selectElement . bind ( 'change' , function ( ) {
244
325
scope . $apply ( function ( ) {
@@ -250,8 +331,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
250
331
if ( multiple ) {
251
332
value = [ ] ;
252
333
for ( groupIndex = 0 , groupLength = optionGroupsCache . length ;
253
- groupIndex < groupLength ;
254
- groupIndex ++ ) {
334
+ groupIndex < groupLength ;
335
+ groupIndex ++ ) {
255
336
// list of options for that group. (first item has the parent)
256
337
optionGroup = optionGroupsCache [ groupIndex ] ;
257
338
@@ -365,7 +446,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
365
446
}
366
447
}
367
448
368
- lastElement = null ; // start at the begining
449
+ lastElement = null ; // start at the beginning
369
450
for ( index = 0 , length = optionGroup . length ; index < length ; index ++ ) {
370
451
option = optionGroup [ index ] ;
371
452
if ( ( existingOption = existingOptions [ index + 1 ] ) ) {
@@ -431,19 +512,34 @@ var optionDirective = ['$interpolate', function($interpolate) {
431
512
return {
432
513
restrict : 'E' ,
433
514
priority : 100 ,
515
+ require : '^select' ,
434
516
compile : function ( element , attr ) {
435
517
if ( isUndefined ( attr . value ) ) {
436
518
var interpolateFn = $interpolate ( element . text ( ) , true ) ;
437
- if ( interpolateFn ) {
438
- return function ( scope , element , attr ) {
439
- scope . $watch ( interpolateFn , function ( value ) {
440
- attr . $set ( 'value' , value ) ;
441
- } ) ;
442
- }
443
- } else {
519
+ if ( ! interpolateFn ) {
444
520
attr . $set ( 'value' , element . text ( ) ) ;
445
521
}
446
522
}
523
+
524
+ // For some reason Opera defaults to true and if not overridden this messes up the repeater.
525
+ // We don't want the view to drive the initialization of the model anyway.
526
+ element . prop ( 'selected' , false ) ;
527
+
528
+ return function ( scope , element , attr , selectCtrl ) {
529
+ if ( interpolateFn ) {
530
+ scope . $watch ( interpolateFn , function ( newVal , oldVal ) {
531
+ attr . $set ( 'value' , newVal ) ;
532
+ if ( newVal !== oldVal ) selectCtrl . removeOption ( oldVal ) ;
533
+ selectCtrl . addOption ( newVal ) ;
534
+ } ) ;
535
+ } else {
536
+ selectCtrl . addOption ( attr . value ) ;
537
+ }
538
+
539
+ element . bind ( '$destroy' , function ( ) {
540
+ selectCtrl . removeOption ( attr . value ) ;
541
+ } ) ;
542
+ } ;
447
543
}
448
544
}
449
545
} ] ;
0 commit comments