diff --git a/examples/demo-object-as-source.html b/examples/demo-object-as-source.html new file mode 100644 index 000000000..a4095903f --- /dev/null +++ b/examples/demo-object-as-source.html @@ -0,0 +1,107 @@ + + + + + AngularJS ui-select + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

(key, value) format

+ +

Using value for binding

+ +

Selected: {{person.selectedValue}}

+ + {{$select.selected.value.name}} + +
+ + email: {{person.value.email}} + age: + +
+
+ +

Using single property for binding

+

Selected: {{person.selectedSingle}}

+ + {{$select.selected.value.name}} + +
+ + email: {{person.value.email}} + age: + +
+
+ +

Using key for binding

+

Selected: {{person.selectedSingleKey}}

+ + {{$select.selected.value.name}} + +
+ + email: {{person.value.email}} + age: + +
+
+ + + diff --git a/examples/demo.js b/examples/demo.js index 759917531..d53204914 100644 --- a/examples/demo.js +++ b/examples/demo.js @@ -46,7 +46,7 @@ app.controller('DemoCtrl', function($scope, $http, $timeout, $interval) { $scope.setInputFocus = function (){ $scope.$broadcast('UiSelectDemo1'); - } + }; $scope.enable = function() { $scope.disabled = false; @@ -58,11 +58,11 @@ app.controller('DemoCtrl', function($scope, $http, $timeout, $interval) { $scope.enableSearch = function() { $scope.searchEnabled = true; - } + }; $scope.disableSearch = function() { $scope.searchEnabled = false; - } + }; $scope.clear = function() { $scope.person.selected = undefined; @@ -130,7 +130,25 @@ app.controller('DemoCtrl', function($scope, $http, $timeout, $interval) { return item; }; + $scope.peopleObj = { + '1' : { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States' }, + '2' : { name: 'Amalie', email: 'amalie@email.com', age: 12, country: 'Argentina' }, + '3' : { name: 'Estefanía', email: 'estefania@email.com', age: 21, country: 'Argentina' }, + '4' : { name: 'Adrian', email: 'adrian@email.com', age: 21, country: 'Ecuador' }, + '5' : { name: 'Wladimir', email: 'wladimir@email.com', age: 30, country: 'Ecuador' }, + '6' : { name: 'Samantha', email: 'samantha@email.com', age: 30, country: 'United States' }, + '7' : { name: 'Nicole', email: 'nicole@email.com', age: 43, country: 'Colombia' }, + '8' : { name: 'Natasha', email: 'natasha@email.com', age: 54, country: 'Ecuador' }, + '9' : { name: 'Michael', email: 'michael@email.com', age: 15, country: 'Colombia' }, + '10' : { name: 'Nicolás', email: 'nicolas@email.com', age: 43, country: 'Colombia' } + }; + $scope.person = {}; + + $scope.person.selectedValue = $scope.peopleObj[3]; + $scope.person.selectedSingle = 'Samantha'; + $scope.person.selectedSingleKey = '5'; + $scope.people = [ { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States' }, { name: 'Amalie', email: 'amalie@email.com', age: 12, country: 'Argentina' }, diff --git a/src/uiSelectChoicesDirective.js b/src/uiSelectChoicesDirective.js index 69e29b6cc..0b4f8a562 100644 --- a/src/uiSelectChoicesDirective.js +++ b/src/uiSelectChoicesDirective.js @@ -39,7 +39,7 @@ uis.directive('uiSelectChoices', throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length); } - choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression($select.parserResult.itemName, '$select.items', $select.parserResult.trackByExp, groupByExp)) + choices.attr('ng-repeat', $select.parserResult.repeatExpression(groupByExp)) .attr('ng-if', '$select.open') //Prevent unnecessary watches when dropdown is closed .attr('ng-mouseenter', '$select.setActiveItem('+$select.parserResult.itemName +')') .attr('ng-click', '$select.select(' + $select.parserResult.itemName + ',false,$event)'); diff --git a/src/uiSelectController.js b/src/uiSelectController.js index 486c245a2..a1b786168 100644 --- a/src/uiSelectController.js +++ b/src/uiSelectController.js @@ -5,8 +5,8 @@ * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested. */ uis.controller('uiSelectCtrl', - ['$scope', '$element', '$timeout', '$filter', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig', - function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig) { + ['$scope', '$element', '$timeout', '$filter', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig', '$parse', + function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig, $parse) { var ctrl = this; @@ -92,6 +92,9 @@ uis.controller('uiSelectCtrl', $timeout(function() { ctrl.search = initSearchValue || ctrl.search; ctrl.searchInput[0].focus(); + if(!ctrl.tagging.isActivated && ctrl.items.length > 1) { + _ensureHighlightVisible(); + } }); } }; @@ -141,6 +144,28 @@ uis.controller('uiSelectCtrl', ctrl.isGrouped = !!groupByExp; ctrl.itemProperty = ctrl.parserResult.itemName; + //If collection is an Object, convert it to Array + + var originalSource = ctrl.parserResult.source; + + //When an object is used as source, we better create an array and use it as 'source' + var createArrayFromObject = function(){ + $scope.$uisSource = Object.keys(originalSource($scope)).map(function(v){ + var result = {}; + result[ctrl.parserResult.keyName] = v; + result.value = $scope.peopleObj[v]; + return result; + }); + }; + + if (ctrl.parserResult.keyName){ // Check for (key,value) syntax + createArrayFromObject(); + ctrl.parserResult.source = $parse('$uisSource' + ctrl.parserResult.filters); + $scope.$watch(originalSource, function(newVal, oldVal){ + if (newVal !== oldVal) createArrayFromObject(); + }, true); + } + ctrl.refreshItems = function (data){ data = data || ctrl.parserResult.source($scope); var selectedItems = ctrl.selected; @@ -164,7 +189,7 @@ uis.controller('uiSelectCtrl', ctrl.items = []; } else { if (!angular.isArray(items)) { - throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items); + throw uiSelectMinErr('items', "Expected an array but got '{0}'.", items); } else { //Remove already selected items (ex: while searching) //TODO Should add a test diff --git a/src/uisRepeatParserService.js b/src/uisRepeatParserService.js index 7ed9955f1..8d343a1e0 100644 --- a/src/uisRepeatParserService.js +++ b/src/uisRepeatParserService.js @@ -20,7 +20,9 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE */ self.parse = function(expression) { - var match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?([\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + //0000000000000000000000000000000000011111111100000000000000022222222222222003333333333333333333333000044444444444444444400000000000000005555500000666666666666600000000000000000000007777777770000000 + var match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\w]+)\s*(|\s*[\s\S]+?)?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); if (!match) { throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", @@ -28,10 +30,20 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE } return { - itemName: match[2], // (lhs) Left-hand side, - source: $parse(match[3]), - trackByExp: match[4], - modelMapper: $parse(match[1] || match[2]) + itemName: match[4] || match[2], // (lhs) Left-hand side, + keyName: match[3], //for (key, value) syntax + source: $parse(!match[3] ? match[5] + (match[6] || ''): match[5]), //concat source with filters if its an array + sourceName: match[5], + filters: match[6], + trackByExp: match[7], + modelMapper: $parse(match[1] || match[4] || match[2]), + repeatExpression: function (grouped) { + var expression = this.itemName + ' in ' + (grouped ? '$group.items' : '$select.items'); + if (this.trackByExp) { + expression += ' track by ' + this.trackByExp; + } + return expression; + } }; }; @@ -40,11 +52,4 @@ uis.service('uisRepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinE return '$group in $select.groups'; }; - self.getNgRepeatExpression = function(itemName, source, trackByExp, grouped) { - var expression = itemName + ' in ' + (grouped ? '$group.items' : source); - if (trackByExp) { - expression += ' track by ' + trackByExp; - } - return expression; - }; }]); diff --git a/test/select.spec.js b/test/select.spec.js index 80f953c46..aaeedfacc 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -1,7 +1,7 @@ 'use strict'; describe('ui-select tests', function() { - var scope, $rootScope, $compile, $timeout, $injector; + var scope, $rootScope, $compile, $timeout, $injector, uisRepeatParser; var Key = { Enter: 13, @@ -48,12 +48,13 @@ describe('ui-select tests', function() { }); }); - beforeEach(inject(function(_$rootScope_, _$compile_, _$timeout_, _$injector_) { + beforeEach(inject(function(_$rootScope_, _$compile_, _$timeout_, _$injector_, _uisRepeatParser_) { $rootScope = _$rootScope_; scope = $rootScope.$new(); $compile = _$compile_; $timeout = _$timeout_; $injector = _$injector_; + uisRepeatParser = _uisRepeatParser_; scope.selection = {}; scope.getGroupLabel = function(person) { @@ -77,6 +78,19 @@ describe('ui-select tests', function() { { name: 'Nicole', email: 'nicole@email.com', group: 'bar', age: 43 }, { name: 'Natasha', email: 'natasha@email.com', group: 'Baz', age: 54 } ]; + + scope.peopleObj = { + '1' : { name: 'Adam', email: 'adam@email.com', age: 12, country: 'United States' }, + '2' : { name: 'Amalie', email: 'amalie@email.com', age: 12, country: 'Argentina' }, + '3' : { name: 'Estefanía', email: 'estefania@email.com', age: 21, country: 'Argentina' }, + '4' : { name: 'Adrian', email: 'adrian@email.com', age: 21, country: 'Ecuador' }, + '5' : { name: 'Wladimir', email: 'wladimir@email.com', age: 30, country: 'Ecuador' }, + '6' : { name: 'Samantha', email: 'samantha@email.com', age: 30, country: 'United States' }, + '7' : { name: 'Nicole', email: 'nicole@email.com', age: 43, country: 'Colombia' }, + '8' : { name: 'Natasha', email: 'natasha@email.com', age: 54, country: 'Ecuador' }, + '9' : { name: 'Michael', email: 'michael@email.com', age: 15, country: 'Colombia' }, + '10' : { name: 'Nicolás', email: 'nicolas@email.com', age: 43, country: 'Colombia' } + }; scope.someObject = {}; scope.someObject.people = [ @@ -190,6 +204,86 @@ describe('ui-select tests', function() { // Tests + //uisRepeatParser + + it('should parse simple repeat syntax', function() { + + var locals = {}; + locals.people = [{name: 'Wladimir'}, {name: 'Samantha'}]; + locals.person = locals.people[0]; + + var parserResult = uisRepeatParser.parse('person in people'); + expect(parserResult.itemName).toBe('person'); + expect(parserResult.modelMapper(locals)).toBe(locals.person); + expect(parserResult.source(locals)).toBe(locals.people); + + var ngExp = parserResult.repeatExpression(false); + expect(ngExp).toBe('person in $select.items'); + + var ngExpGrouped = parserResult.repeatExpression(true); + expect(ngExpGrouped).toBe('person in $group.items'); + + }); + + it('should parse simple repeat syntax', function() { + + var locals = {}; + locals.people = [{name: 'Wladimir'}, {name: 'Samantha'}]; + locals.person = locals.people[0]; + + var parserResult = uisRepeatParser.parse('person.name as person in people'); + expect(parserResult.itemName).toBe('person'); + expect(parserResult.modelMapper(locals)).toBe(locals.person.name); + expect(parserResult.source(locals)).toBe(locals.people); + + }); + + it('should parse simple property binding repeat syntax', function() { + + var locals = {}; + locals.people = [{name: 'Wladimir'}, {name: 'Samantha'}]; + locals.person = locals.people[0]; + + var parserResult = uisRepeatParser.parse('person.name as person in people'); + expect(parserResult.itemName).toBe('person'); + expect(parserResult.modelMapper(locals)).toBe(locals.person.name); + expect(parserResult.source(locals)).toBe(locals.people); + + }); + + it('should parse (key, value) repeat syntax', function() { + + var locals = {}; + locals.people = { 'WC' : {name: 'Wladimir'}, 'SH' : {name: 'Samantha'}}; + locals.person = locals.people[0]; + + var parserResult = uisRepeatParser.parse('(key,person) in people'); + expect(parserResult.itemName).toBe('person'); + expect(parserResult.keyName).toBe('key'); + expect(parserResult.modelMapper(locals)).toBe(locals.person); + expect(parserResult.source(locals)).toBe(locals.people); + + var ngExp = parserResult.repeatExpression(false); + expect(ngExp).toBe('person in $select.items'); + + var ngExpGrouped = parserResult.repeatExpression(true); + expect(ngExpGrouped).toBe('person in $group.items'); + + }); + + it('should parse simple property binding with (key, value) repeat syntax', function() { + + var locals = {}; + locals.people = { 'WC' : {name: 'Wladimir'}, 'SH' : {name: 'Samantha'}}; + locals.person = locals.people['WC']; + + var parserResult = uisRepeatParser.parse('person.name as (key, person) in people'); + expect(parserResult.itemName).toBe('person'); + expect(parserResult.keyName).toBe('key'); + expect(parserResult.modelMapper(locals)).toBe(locals.person.name); + expect(parserResult.source(locals)).toBe(locals.people); + + }); it('should compile child directives', function() { var el = createUiSelect(); @@ -469,6 +563,88 @@ describe('ui-select tests', function() { el2.remove(); }); + it('should bind model correctly (with object as source)', function() { + var el = compileTemplate( + ' \ + {{$select.selected.value.name}} \ + \ +
\ +
\ +
\ +
' + ); + // scope.selection.selected = 'Samantha'; + + clickItem(el, 'Samantha'); + scope.$digest(); + expect(getMatchLabel(el)).toEqual('Samantha'); + expect(scope.selection.selected).toBe(scope.peopleObj[6]); + + }); + + it('should bind model correctly (with object as source) using a single property', function() { + var el = compileTemplate( + ' \ + {{$select.selected.value.name}} \ + \ +
\ +
\ +
\ +
' + ); + // scope.selection.selected = 'Samantha'; + + clickItem(el, 'Samantha'); + scope.$digest(); + expect(getMatchLabel(el)).toEqual('Samantha'); + expect(scope.selection.selected).toBe('Samantha'); + + }); + + it('should update choices when original source changes (with object as source)', function() { + var el = compileTemplate( + ' \ + {{$select.selected.value.name}} \ + \ +
\ +
\ +
\ +
' + ); + + scope.$digest(); + + openDropdown(el); + var choicesEls = $(el).find('.ui-select-choices-row'); + expect(choicesEls.length).toEqual(10); + + scope.peopleObj['11'] = { name: 'Camila', email: 'camila@email.com', age: 1, country: 'Ecuador' }; + scope.$digest(); + + choicesEls = $(el).find('.ui-select-choices-row'); + expect(choicesEls.length).toEqual(11); + + }); + + it('should bind model correctly (with object as source) using the key of collection', function() { + var el = compileTemplate( + ' \ + {{$select.selected.value.name}} \ + \ +
\ +
\ +
\ +
' + ); + // scope.selection.selected = 'Samantha'; + + clickItem(el, 'Samantha'); + scope.$digest(); + expect(getMatchLabel(el)).toEqual('Samantha'); + expect(scope.selection.selected).toBe('6'); + + }); + describe('disabled options', function() { function createUiSelect(attrs) { var attrsDisabled = '';