diff --git a/src/select.js b/src/select.js index 3b81924b1..0f9c76253 100644 --- a/src/select.js +++ b/src/select.js @@ -109,13 +109,22 @@ expression); } - return { + var result = { itemName: match[2], // (lhs) Left-hand side, source: $parse(match[3]), trackByExp: match[4], modelMapper: $parse(match[1] || match[2]) }; + if (result.trackByExp) { + var propMatch = result.trackByExp.match(/\S+\.(\S+)/); + + if (propMatch) { + result.trackByProp = propMatch[1]; + } + } + + return result; }; self.getGroupNgRepeatExpression = function() { @@ -657,49 +666,93 @@ }); //From model --> view - ngModel.$formatters.unshift(function (inputValue) { - var data = $select.parserResult.source (scope, { $select : {search:''}}), //Overwrite $search - locals = {}, - result; - if (data){ - if ($select.multiple){ - var resultMultiple = []; - var checkFnMultiple = function(list, value){ - if (!list || !list.length) return; - for (var p = list.length - 1; p >= 0; p--) { - locals[$select.parserResult.itemName] = list[p]; - result = $select.parserResult.modelMapper(scope, locals); - if (result == value){ - resultMultiple.unshift(list[p]); - return true; - } + ngModel.$formatters.unshift(function() { + var matchesOptionMultiple = function(options, inputValue, resultMultiple) { + var locals, result; + var match = false; + + if (options && options.length) { + for (var i = options.length - 1; i >= 0 && !match; i--) { + locals = {}; + locals[$select.parserResult.itemName] = options[i]; + result = $select.parserResult.modelMapper(scope, locals); + + if ($select.parserResult.trackByProp && inputValue) { + match = angular.equals(result[$select.parserResult.trackByProp], inputValue[$select.parserResult.trackByProp]); + } else { + match = angular.equals(result, inputValue); } - return false; - }; - if (!inputValue) return resultMultiple; //If ngModel was undefined + + if (match) { + resultMultiple.unshift(options[i]); + } + } + } + + return match; + }; + + var matchesOption = function(option, inputValue) { + var result, locals = {}; + locals[$select.parserResult.itemName] = option; + result = $select.parserResult.modelMapper(scope, locals); + + if ($select.parserResult.trackByProp && inputValue) { + return angular.equals(result[$select.parserResult.trackByProp], inputValue[$select.parserResult.trackByProp]); + } + + return angular.equals(result, inputValue); + }; + + var handleMultiple = function(data, inputValue) { + var resultMultiple = []; + + if (inputValue) { for (var k = inputValue.length - 1; k >= 0; k--) { - if (!checkFnMultiple($select.selected, inputValue[k])){ - checkFnMultiple(data, inputValue[k]); + if (!matchesOptionMultiple($select.selected, inputValue[k], resultMultiple)) { + matchesOptionMultiple(data, inputValue[k], resultMultiple); } } - return resultMultiple; - }else{ - var checkFnSingle = function(d){ - locals[$select.parserResult.itemName] = d; - result = $select.parserResult.modelMapper(scope, locals); - return result == inputValue; - }; - //If possible pass same object stored in $select.selected - if ($select.selected && checkFnSingle($select.selected)) { - return $select.selected; + } + + return resultMultiple; + }; + + var handleSingle = function(data, inputValue) { + var result = inputValue; + + if (inputValue) { + // If possible pass same object stored in $select.selected + if ($select.selected && matchesOption($select.selected, inputValue)) { + result = $select.selected; + } else { + for (var i = data.length - 1; i >= 0; i--) { + if (matchesOption(data[i], inputValue)) { + result = data[i]; + break; + } + } } - for (var i = data.length - 1; i >= 0; i--) { - if (checkFnSingle(data[i])) return data[i]; + } + + return result; + }; + + return function(inputValue) { + var data = $select.parserResult.source(scope, {$select: {search: ''}}); //Overwrite $search + var result = inputValue; + + if (data) { + if ($select.multiple) { + result = handleMultiple(data, inputValue); + } else { + result = handleSingle(data, inputValue); } } - } - return inputValue; - }); + + return result; + }; + }()); //Set reference to ngModel from uiSelectCtrl $select.ngModel = ngModel; diff --git a/test/select.spec.js b/test/select.spec.js index 461ec3c13..bff5f971d 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -124,7 +124,7 @@ describe('ui-select tests', function() { var $select = el.scope().$select; $select.open = true; scope.$digest(); - }; + } // Tests @@ -157,6 +157,38 @@ describe('ui-select tests', function() { expect(getMatchLabel(el)).toEqual('Adam'); }); + it('should correctly render initial state with track by feature', function() { + scope.selection.selected = { name: 'Samantha', email: 'something different than array source', group: 'foobar', age: 31 }; + + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + expect(getMatchLabel(el)).toEqual('Samantha'); + }); + + it('should not error when using track by feature and ng-model reference is set to null', function() { + scope.selection.selected = null; + + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + expect(getMatchLabel(el)).toEqual(''); + }); + it('should display the choices when activated', function() { var el = createUiSelect(); @@ -383,7 +415,7 @@ describe('ui-select tests', function() { beforeEach(function() { disablePerson({ disableAttr : 'active', - disableBool : false, + disableBool : false }); this.el = createUiSelect({ disabled: '!person.active' @@ -727,7 +759,7 @@ describe('ui-select tests', function() { }); - it('should invoke hover callback', function(){ + it('should invoke hover callback', function() { var highlighted; scope.onHighlightFn = function ($item) { @@ -955,7 +987,7 @@ describe('ui-select tests', function() { scope.fetchFromServer = function(searching){ if (searching == 's') - return scope.people + return scope.people; if (searching == 'o'){ scope.people = []; //To simulate cases were previously selected item isnt in the list anymore @@ -963,11 +995,11 @@ describe('ui-select tests', function() { }; - setSearchText(el, 'r') + setSearchText(el, 'r'); clickItem(el, 'Samantha'); expect(getMatchLabel(el)).toBe('Samantha'); - setSearchText(el, 'o') + setSearchText(el, 'o'); expect(getMatchLabel(el)).toBe('Samantha'); }); @@ -1080,6 +1112,26 @@ describe('ui-select tests', function() { expect(el.find('.ui-select-match-item').length).toBe(2); }); + it('should render initial selected items with track by feature', function() { + scope.selection.selectedMultiple = [ + { name: 'Samantha', email: 'something different than array source', group: 'foobar', age: 31 }, + { name: 'Wladimir', email: 'something different than array source', group: 'FooBar', age: 31 }, + ]; + + var el = compileTemplate( + ' \ + {{$item.name}} <{{$item.email}}> \ + \ +
\ +
\ +
\ +
' + ); + + expect(el.scope().$select.selected.length).toBe(2); + expect(el.find('.ui-select-match-item').length).toBe(2); + }); + it('should remove item by pressing X icon', function() { scope.selection.selectedMultiple = [scope.people[4], scope.people[5]]; //Wladimir & Samantha var el = createUiSelectMultiple(); @@ -1191,13 +1243,13 @@ describe('ui-select tests', function() { var searchInput = el.find('.ui-select-search'); expect(isDropdownOpened(el)).toEqual(false); - triggerKeydown(searchInput, Key.Left) - triggerKeydown(searchInput, Key.Left) + triggerKeydown(searchInput, Key.Left); + triggerKeydown(searchInput, Key.Left); expect(isDropdownOpened(el)).toEqual(false); expect(el.scope().$select.activeMatchIndex).toBe(el.scope().$select.selected.length - 2); - triggerKeydown(searchInput, Key.Left) - triggerKeydown(searchInput, Key.Left) - triggerKeydown(searchInput, Key.Left) + triggerKeydown(searchInput, Key.Left); + triggerKeydown(searchInput, Key.Left); + triggerKeydown(searchInput, Key.Left); expect(el.scope().$select.activeMatchIndex).toBe(0); }); @@ -1208,9 +1260,9 @@ describe('ui-select tests', function() { var el = createUiSelectMultiple(); var searchInput = el.find('.ui-select-search'); - el.scope().$select.activeMatchIndex = 3 - triggerKeydown(searchInput, Key.Left) - triggerKeydown(searchInput, Key.Left) + el.scope().$select.activeMatchIndex = 3; + triggerKeydown(searchInput, Key.Left); + triggerKeydown(searchInput, Key.Left); expect(el.scope().$select.activeMatchIndex).toBe(1); }); @@ -1221,9 +1273,9 @@ describe('ui-select tests', function() { var el = createUiSelectMultiple(); var searchInput = el.find('.ui-select-search'); - el.scope().$select.activeMatchIndex = 0 - triggerKeydown(searchInput, Key.Right) - triggerKeydown(searchInput, Key.Right) + el.scope().$select.activeMatchIndex = 0; + triggerKeydown(searchInput, Key.Right); + triggerKeydown(searchInput, Key.Right); expect(el.scope().$select.activeMatchIndex).toBe(2); }); @@ -1235,7 +1287,7 @@ describe('ui-select tests', function() { var searchInput = el.find('.ui-select-search'); expect(isDropdownOpened(el)).toEqual(false); - triggerKeydown(searchInput, Key.Down) + triggerKeydown(searchInput, Key.Down); expect(isDropdownOpened(el)).toEqual(true); }); @@ -1270,7 +1322,7 @@ describe('ui-select tests', function() { var searchInput = el.find('.ui-select-search'); expect(isDropdownOpened(el)).toEqual(false); - triggerKeydown(searchInput, Key.Down) + triggerKeydown(searchInput, Key.Down); expect(isDropdownOpened(el)).toEqual(true); clickItem(el, 'Wladimir'); @@ -1286,7 +1338,7 @@ describe('ui-select tests', function() { var searchInput = el.find('.ui-select-search'); expect(isDropdownOpened(el)).toEqual(false); - triggerKeydown(searchInput, Key.Down) + triggerKeydown(searchInput, Key.Down); expect(isDropdownOpened(el)).toEqual(true); clickItem(el, 'Wladimir'); @@ -1302,9 +1354,9 @@ describe('ui-select tests', function() { var searchInput = el.find('.ui-select-search'); expect(isDropdownOpened(el)).toEqual(false); - triggerKeydown(searchInput, Key.Down) + triggerKeydown(searchInput, Key.Down); expect(isDropdownOpened(el)).toEqual(true); - triggerKeydown(searchInput, Key.Escape) + triggerKeydown(searchInput, Key.Escape); expect(isDropdownOpened(el)).toEqual(false); }); @@ -1315,8 +1367,8 @@ describe('ui-select tests', function() { var el = createUiSelectMultiple(); var searchInput = el.find('.ui-select-search'); - triggerKeydown(searchInput, Key.Down) - triggerKeydown(searchInput, Key.Enter) + triggerKeydown(searchInput, Key.Down); + triggerKeydown(searchInput, Key.Enter); expect(scope.selection.selectedMultiple.length).toEqual(2); }); @@ -1328,9 +1380,9 @@ describe('ui-select tests', function() { triggerKeydown(searchInput, Key.Down); //Open dropdown - el.scope().$select.activeIndex = 0 - triggerKeydown(searchInput, Key.Down) - triggerKeydown(searchInput, Key.Down) + el.scope().$select.activeIndex = 0; + triggerKeydown(searchInput, Key.Down); + triggerKeydown(searchInput, Key.Down); expect(el.scope().$select.activeIndex).toBe(2); }); @@ -1342,20 +1394,13 @@ describe('ui-select tests', function() { triggerKeydown(searchInput, Key.Down); //Open dropdown - el.scope().$select.activeIndex = 5 - triggerKeydown(searchInput, Key.Up) - triggerKeydown(searchInput, Key.Up) + el.scope().$select.activeIndex = 5; + triggerKeydown(searchInput, Key.Up); + triggerKeydown(searchInput, Key.Up); expect(el.scope().$select.activeIndex).toBe(3); }); - it('should render initial selected items', function() { - scope.selection.selectedMultiple = [scope.people[4], scope.people[5]]; //Wladimir & Samantha - var el = createUiSelectMultiple(); - expect(el.scope().$select.selected.length).toBe(2); - expect(el.find('.ui-select-match-item').length).toBe(2); - }); - it('should parse the items correctly using single property binding', function() { scope.selection.selectedMultiple = ['wladimir@email.com', 'samantha@email.com']; @@ -1397,7 +1442,7 @@ describe('ui-select tests', function() { }); - it('should format view value correctly when using single property binding and refresh funcion', function () { + it('should format view value correctly when using single property binding and refresh function', function () { scope.selection.selectedMultiple = ['wladimir@email.com', 'samantha@email.com']; @@ -1417,7 +1462,7 @@ describe('ui-select tests', function() { scope.fetchFromServer = function(searching){ if (searching == 'n') - return scope.people + return scope.people; if (searching == 'o'){ scope.people = []; //To simulate cases were previously selected item isnt in the list anymore @@ -1425,13 +1470,13 @@ describe('ui-select tests', function() { }; - setSearchText(el, 'n') + setSearchText(el, 'n'); clickItem(el, 'Nicole'); expect(el.find('.ui-select-match-item [uis-transclude-append]:not(.ng-hide)').text()) .toBe("Wladimir Samantha Nicole "); - setSearchText(el, 'o') + setSearchText(el, 'o'); expect(el.find('.ui-select-match-item [uis-transclude-append]:not(.ng-hide)').text()) .toBe("Wladimir Samantha Nicole ");