2222 * be nested into the `<select>` element. This element will then represent `null` or "not selected"
2323 * option. See example below for demonstration.
2424 *
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.
3529 *
3630 * @param {string } name assignable expression to data-bind to.
3731 * @param {string= } required The control is considered valid only if value is entered.
9286 <select ng-model="color" ng-options="c.name for c in colors"></select><br>
9387
9488 Color (null allowed):
95- <div class="nullable">
89+ <span class="nullable">
9690 <select ng-model="color" ng-options="c.name for c in colors">
9791 <option value="">-- chose color --</option>
9892 </select>
99- </div ><br/>
93+ </span ><br/>
10094
10195 Color grouped by shade:
10296 <select ng-model="color" ng-options="c.name group by c.shade for c in colors">
126120var ngOptionsDirective = valueFn ( { terminal : true } ) ;
127121var selectDirective = [ '$compile' , '$parse' , function ( $compile , $parse ) {
128122 //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 } ;
130125
131126 return {
132127 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+ } ;
136151
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 ) ;
139207
140208 // required validator
141209 if ( multiple && ( attr . required || attr . ngRequired ) ) {
142210 var requiredValidator = function ( value ) {
143- ctrl . $setValidity ( 'required' , ! attr . required || ( value && value . length ) ) ;
211+ ngModelCtrl . $setValidity ( 'required' , ! attr . required || ( value && value . length ) ) ;
144212 return value ;
145213 } ;
146214
147- ctrl . $parsers . push ( requiredValidator ) ;
148- ctrl . $formatters . unshift ( requiredValidator ) ;
215+ ngModelCtrl . $parsers . push ( requiredValidator ) ;
216+ ngModelCtrl . $formatters . unshift ( requiredValidator ) ;
149217
150218 attr . $observe ( 'required' , function ( ) {
151- requiredValidator ( ctrl . $viewValue ) ;
219+ requiredValidator ( ngModelCtrl . $viewValue ) ;
152220 } ) ;
153221 }
154222
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 ) ;
158226
159227
160228 ////////////////////////////
161229
162230
163231
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+ }
167247 } ;
168248
169249 selectElement . bind ( 'change' , function ( ) {
170250 scope . $apply ( function ( ) {
171- ctrl . $setViewValue ( selectElement . val ( ) ) ;
251+ if ( unknownOption . parent ( ) ) unknownOption . remove ( ) ;
252+ ngModelCtrl . $setViewValue ( selectElement . val ( ) ) ;
172253 } ) ;
173254 } ) ;
174255 }
@@ -219,26 +300,26 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
219300 groupByFn = $parse ( match [ 3 ] || '' ) ,
220301 valueFn = $parse ( match [ 2 ] ? match [ 1 ] : valueName ) ,
221302 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
227303 // This is an array of array of existing option groups in DOM. We try to reuse these if possible
228304 // optionGroupsCache[0] is the options with no option group
229305 // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
230306 optionGroupsCache = [ [ { element : selectElement , label :'' } ] ] ;
231307
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 ( '' ) ;
242323
243324 selectElement . bind ( 'change' , function ( ) {
244325 scope . $apply ( function ( ) {
@@ -250,8 +331,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
250331 if ( multiple ) {
251332 value = [ ] ;
252333 for ( groupIndex = 0 , groupLength = optionGroupsCache . length ;
253- groupIndex < groupLength ;
254- groupIndex ++ ) {
334+ groupIndex < groupLength ;
335+ groupIndex ++ ) {
255336 // list of options for that group. (first item has the parent)
256337 optionGroup = optionGroupsCache [ groupIndex ] ;
257338
@@ -365,7 +446,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
365446 }
366447 }
367448
368- lastElement = null ; // start at the begining
449+ lastElement = null ; // start at the beginning
369450 for ( index = 0 , length = optionGroup . length ; index < length ; index ++ ) {
370451 option = optionGroup [ index ] ;
371452 if ( ( existingOption = existingOptions [ index + 1 ] ) ) {
@@ -431,19 +512,34 @@ var optionDirective = ['$interpolate', function($interpolate) {
431512 return {
432513 restrict : 'E' ,
433514 priority : 100 ,
515+ require : '^select' ,
434516 compile : function ( element , attr ) {
435517 if ( isUndefined ( attr . value ) ) {
436518 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 ) {
444520 attr . $set ( 'value' , element . text ( ) ) ;
445521 }
446522 }
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+ } ;
447543 }
448544 }
449545} ] ;
0 commit comments