@@ -77,6 +77,24 @@ var ngOptionsMinErr = minErr('ngOptions');
77
77
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
78
78
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
79
79
* even when the options are recreated (e.g. reloaded from the server).
80
+
81
+ * <div class="alert alert-info">
82
+ * **Note:** Using `selectAs` together with `trackexpr` is not possible (and will throw).
83
+ * TODO: Add some nice reasoning here, add a minErr and a nice error page.
84
+ * reasoning:
85
+ * - Example: <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
86
+ * values: [{id: 1, label: 'aLabel', subItem: {name: 'aSubItem'}}, {id: 2, label: 'bLabel', subItem: {name: 'bSubItemß'}}],
87
+ * $scope.selected = {name: 'aSubItem'};
88
+ * - trackBy is always applied to `value`, with purpose to preserve the selection,
89
+ * (to `item` in this case)
90
+ * - to calculate whether an item is selected we do the following:
91
+ * 1. apply `trackBy` to the values in the array, e.g.
92
+ * In the example: [1,2]
93
+ * 2. apply `trackBy` to the already selected value in `ngModel`:
94
+ * In the example: this is not possible, as `trackBy` refers to `item.id`, but the selected
95
+ * value from `ngModel` is `{name: aSubItem}`.
96
+ *
97
+ * </div>
80
98
*
81
99
* @example
82
100
<example module="selectExample">
@@ -345,7 +363,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
345
363
// We try to reuse these if possible
346
364
// - optionGroupsCache[0] is the options with no option group
347
365
// - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
348
- optionGroupsCache = [ [ { element : selectElement , label :'' } ] ] ;
366
+ optionGroupsCache = [ [ { element : selectElement , label :'' } ] ] ,
367
+ //re-usable object to represent option's locals
368
+ locals = { } ;
349
369
350
370
if ( nullOption ) {
351
371
// compile the element since there might be bindings in it
@@ -363,146 +383,109 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
363
383
// clear contents, we'll add what's needed based on the model
364
384
selectElement . empty ( ) ;
365
385
366
- selectElement . on ( 'change' , function ( ) {
386
+ selectElement . on ( 'change' , selectionChanged ) ;
387
+
388
+ ctrl . $render = render ;
389
+
390
+ scope . $watchCollection ( valuesFn , scheduleRendering ) ;
391
+ scope . $watchCollection ( getLabels , scheduleRendering ) ;
392
+
393
+ if ( multiple ) {
394
+ scope . $watchCollection ( function ( ) { return ctrl . $modelValue ; } , scheduleRendering ) ;
395
+ }
396
+
397
+ // ------------------------------------------------------------------ //
398
+
399
+ function callExpression ( exprFn , key , value ) {
400
+ locals [ valueName ] = value ;
401
+ if ( keyName ) locals [ keyName ] = key ;
402
+ return exprFn ( scope , locals ) ;
403
+ }
404
+
405
+ function selectionChanged ( ) {
367
406
scope . $apply ( function ( ) {
368
407
var optionGroup ,
369
408
collection = valuesFn ( scope ) || [ ] ,
370
- locals = { } ,
371
409
key , value , optionElement , index , groupIndex , length , groupLength , trackIndex ;
372
-
410
+ var viewValue ;
373
411
if ( multiple ) {
374
- value = [ ] ;
375
- for ( groupIndex = 0 , groupLength = optionGroupsCache . length ;
376
- groupIndex < groupLength ;
377
- groupIndex ++ ) {
378
- // list of options for that group. (first item has the parent)
379
- optionGroup = optionGroupsCache [ groupIndex ] ;
380
-
381
- for ( index = 1 , length = optionGroup . length ; index < length ; index ++ ) {
382
- if ( ( optionElement = optionGroup [ index ] . element ) [ 0 ] . selected ) {
383
- value . push ( getViewValue ( optionElement , locals , collection ) ) ;
384
- }
385
- }
386
- }
412
+ viewValue = [ ] ;
413
+ forEach ( selectElement . val ( ) , function ( selectedKey ) {
414
+ viewValue . push ( getViewValue ( selectedKey , collection [ selectedKey ] ) ) ;
415
+ } ) ;
387
416
} else {
388
- value = getViewValue ( selectElement , locals , collection ) ;
417
+ var selectedKey = selectElement . val ( ) ;
418
+ viewValue = getViewValue ( selectedKey , collection [ selectedKey ] ) ;
389
419
}
390
- ctrl . $setViewValue ( value ) ;
420
+ ctrl . $setViewValue ( viewValue ) ;
391
421
render ( ) ;
392
422
} ) ;
393
- } ) ;
423
+ }
394
424
395
- ctrl . $render = render ;
425
+ function getViewValue ( key , value ) {
426
+ if ( key === '?' ) {
427
+ return undefined ;
428
+ } else if ( key === '' ) {
429
+ return null ;
430
+ } else {
431
+ var viewValueFn = selectAsFn ? selectAsFn : valueFn ;
432
+ return callExpression ( viewValueFn , key , value ) ;
433
+ }
434
+ }
396
435
397
- scope . $watchCollection ( valuesFn , scheduleRendering ) ;
398
- scope . $watchCollection ( function ( ) {
399
- var locals = { } ,
400
- values = valuesFn ( scope ) ;
401
- if ( values ) {
402
- var toDisplay = new Array ( values . length ) ;
436
+ function getLabels ( ) {
437
+ var values = valuesFn ( scope ) ;
438
+ var toDisplay ;
439
+ if ( values && isArray ( values ) ) {
440
+ toDisplay = new Array ( values . length ) ;
403
441
for ( var i = 0 , ii = values . length ; i < ii ; i ++ ) {
404
- locals [ valueName ] = values [ i ] ;
405
- toDisplay [ i ] = displayFn ( scope , locals ) ;
442
+ toDisplay [ i ] = callExpression ( displayFn , i , values [ i ] ) ;
406
443
}
407
444
return toDisplay ;
445
+ } else if ( values ) {
446
+ // TODO: Add a test for this case
447
+ toDisplay = { } ;
448
+ for ( var prop in values ) {
449
+ if ( values . hasOwnProperty ( prop ) ) {
450
+ toDisplay [ prop ] = callExpression ( displayFn , prop , values [ prop ] ) ;
451
+ }
452
+ }
408
453
}
409
- } , scheduleRendering ) ;
410
-
411
- if ( multiple ) {
412
- scope . $watchCollection ( function ( ) { return ctrl . $modelValue ; } , scheduleRendering ) ;
454
+ return toDisplay ;
413
455
}
414
456
415
-
416
- function getSelectedSet ( ) {
417
- var selectedSet = false ;
457
+ function createIsSelectedFn ( viewValue ) {
458
+ var selectedSet ;
418
459
if ( multiple ) {
419
- var viewValue = ctrl . $ viewValue;
420
- if ( trackFn && isArray ( viewValue ) && ! selectAs ) {
460
+ if ( ! selectAs && trackFn && isArray ( viewValue ) ) {
461
+
421
462
selectedSet = new HashMap ( [ ] ) ;
422
- var locals = { } ;
423
463
for ( var trackIndex = 0 ; trackIndex < viewValue . length ; trackIndex ++ ) {
424
- locals [ valueName ] = viewValue [ trackIndex ] ;
425
- selectedSet . put ( trackFn ( scope , locals ) , viewValue [ trackIndex ] ) ;
464
+ // tracking by key
465
+ selectedSet . put ( callExpression ( trackFn , null , viewValue [ trackIndex ] ) , true ) ;
426
466
}
427
467
} else {
428
468
selectedSet = new HashMap ( viewValue ) ;
429
469
}
470
+ } else if ( ! selectAsFn && trackFn ) {
471
+ viewValue = callExpression ( trackFn , null , viewValue ) ;
430
472
}
431
- return selectedSet ;
432
- }
433
-
434
- function getViewValue ( el , locals , collection ) {
435
- var key = el . val ( ) ;
436
- var value ;
437
- var calculateViewValue ;
438
-
439
- if ( selectAsFn || trackFn ) {
440
- calculateViewValue = function ( ) {
441
- var getterFn = selectAsFn || trackFn ;
442
- var wrappedCollectionValue = { } ;
443
- wrappedCollectionValue [ valueName ] = collection [ key ] ;
444
- wrappedCollectionValue [ keyName ] = key ;
445
-
446
- for ( var i in collection ) {
447
- if ( collection . hasOwnProperty ( i ) ) {
448
- locals [ valueName ] = collection [ i ] ;
449
- if ( keyName ) locals [ keyName ] = i ;
450
- if ( getterFn ( scope , locals ) ==
451
- getterFn ( scope , wrappedCollectionValue ) ) {
452
- /*
453
- * trackBy should not be used for final calculation, because it doesn't
454
- * necessarily return the expected value.
455
- */
456
- return ( selectAsFn || valueFn ) ( scope , locals ) ;
457
- }
458
- }
459
- }
460
- } ;
461
- } else {
462
- calculateViewValue = function ( ) {
463
- locals [ valueName ] = collection [ key ] ;
464
- if ( keyName ) locals [ keyName ] = key ;
465
- return valueFn ( scope , locals ) ;
466
- } ;
467
- }
468
-
469
- if ( multiple ) {
470
- if ( keyName ) locals [ keyName ] = key ;
471
- calculateViewValue ( ) ;
472
- return ( selectAsFn || valueFn ) ( scope , locals ) ;
473
- }
474
-
475
- if ( key == '?' ) {
476
- value = undefined ;
477
- } else if ( key === '' ) {
478
- value = null ;
479
- } else {
480
- value = calculateViewValue ( ) ;
481
- }
482
-
483
- return value ;
484
- }
485
-
486
- function isSelected ( viewValue , locals , selectedSet ) {
487
- var compareValueFn ;
488
- if ( selectAsFn ) {
489
- compareValueFn = selectAsFn ;
490
- } else if ( trackFn ) {
491
- var withValueName = { } ;
492
- withValueName [ valueName ] = viewValue ;
493
- compareValueFn = trackFn ;
494
-
495
- viewValue = trackFn ( withValueName ) ;
496
- } else {
497
- compareValueFn = valueFn ;
498
- }
499
-
500
- var optionValue = compareValueFn ( scope , locals ) ;
473
+ return function isSelected ( key , value ) {
474
+ var compareValueFn ;
475
+ if ( selectAsFn ) {
476
+ compareValueFn = selectAsFn ;
477
+ } else if ( trackFn ) {
478
+ compareValueFn = trackFn ;
479
+ } else {
480
+ compareValueFn = valueFn ;
481
+ }
501
482
502
- if ( multiple ) {
503
- return isDefined ( selectedSet . remove ( optionValue ) ) ;
483
+ if ( multiple ) {
484
+ return isDefined ( selectedSet . remove ( callExpression ( compareValueFn , key , value ) ) ) ;
485
+ } else {
486
+ return viewValue == callExpression ( compareValueFn , key , value ) ;
487
+ }
504
488
}
505
- return viewValue === optionValue ;
506
489
}
507
490
508
491
function scheduleRendering ( ) {
@@ -512,11 +495,10 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
512
495
}
513
496
}
514
497
515
-
516
498
function render ( ) {
517
499
renderScheduled = false ;
518
500
519
- // Temporary location for the option groups before we render them
501
+ // Temporary location for the option groups before we render them
520
502
var optionGroups = { '' :[ ] } ,
521
503
optionGroupNames = [ '' ] ,
522
504
optionGroupName ,
@@ -527,11 +509,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
527
509
values = valuesFn ( scope ) || [ ] ,
528
510
keys = keyName ? sortedKeys ( values ) : values ,
529
511
key ,
512
+ value ,
530
513
groupLength , length ,
531
514
groupIndex , index ,
532
- locals = { } ,
533
515
selected ,
534
- selectedSet = getSelectedSet ( ) ,
516
+ isSelected = createIsSelectedFn ( viewValue ) ,
517
+ anySelected = false ,
535
518
lastElement ,
536
519
element ,
537
520
label ;
@@ -542,24 +525,19 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
542
525
if ( keyName ) {
543
526
key = keys [ index ] ;
544
527
if ( key . charAt ( 0 ) === '$' ) continue ;
545
- locals [ keyName ] = key ;
546
528
}
529
+ value = values [ key ] ;
547
530
548
- locals [ valueName ] = values [ key ] ;
549
-
550
- optionGroupName = groupByFn ( scope , locals ) || '' ;
531
+ optionGroupName = callExpression ( groupByFn , key , value ) || '' ;
551
532
if ( ! ( optionGroup = optionGroups [ optionGroupName ] ) ) {
552
533
optionGroup = optionGroups [ optionGroupName ] = [ ] ;
553
534
optionGroupNames . push ( optionGroupName ) ;
554
535
}
555
536
556
- selected = isSelected (
557
- viewValue ,
558
- locals ,
559
- selectedSet ) ;
560
- selectedSet = selectedSet || selected ;
537
+ selected = isSelected ( key , value ) ;
538
+ anySelected = anySelected || selected ;
561
539
562
- label = displayFn ( scope , locals ) ; // what will be seen by the user
540
+ label = callExpression ( displayFn , key , value ) ; // what will be seen by the user
563
541
564
542
// doing displayFn(scope, locals) || '' overwrites zero values
565
543
label = isDefined ( label ) ? label : '' ;
@@ -573,8 +551,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
573
551
if ( ! multiple ) {
574
552
if ( nullOption || viewValue === null ) {
575
553
// insert null option if we have a placeholder, or the model is null
576
- optionGroups [ '' ] . unshift ( { id :'' , label :'' , selected :! selectedSet } ) ;
577
- } else if ( ! selectedSet ) {
554
+ optionGroups [ '' ] . unshift ( { id :'' , label :'' , selected :! anySelected } ) ;
555
+ } else if ( ! anySelected ) {
578
556
// option could not be found, we have to insert the undefined item
579
557
optionGroups [ '' ] . unshift ( { id :'?' , label :'' , selected :true } ) ;
580
558
}
0 commit comments