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 @@ +<!DOCTYPE html> +<html lang="en" ng-app="demo"> +<head> + <meta charset="utf-8"> + <title>AngularJS ui-select</title> + + <!-- + IE8 support, see AngularJS Internet Explorer Compatibility http://docs.angularjs.org/guide/ie + For Firefox 3.6, you will also need to include jQuery and ECMAScript 5 shim + --> + <!--[if lt IE 9]> + <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.js"></script> + <script src="http://cdnjs.cloudflare.com/ajax/libs/es5-shim/2.2.0/es5-shim.js"></script> + <script> + document.createElement('ui-select'); + document.createElement('ui-select-match'); + document.createElement('ui-select-choices'); + </script> + <![endif]--> + + <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular.js"></script> + <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.2.18/angular-sanitize.js"></script> + <link rel="stylesheet" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.css"> + + <!-- ui-select files --> + <script src="../dist/select.js"></script> + <link rel="stylesheet" href="../dist/select.css"> + + <script src="demo.js"></script> + + <!-- Select2 theme --> + <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/select2/3.4.5/select2.css"> + + <!-- + Selectize theme + Less versions are available at https://github.com/brianreavis/selectize.js/tree/master/dist/less + --> + <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.default.css"> + <!-- <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.bootstrap2.css"> --> + <!-- <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.8.5/css/selectize.bootstrap3.css"> --> + + <style> + body { + padding: 15px; + } + + .select2 > .select2-choice.ui-select-match { + /* Because of the inclusion of Bootstrap */ + height: 29px; + } + + .selectize-control > .selectize-dropdown { + top: 36px; + } + </style> +</head> + +<body ng-controller="DemoCtrl"> + + <button class="btn btn-default btn-xs" ng-click="enable()">Enable ui-select</button> + <button class="btn btn-default btn-xs" ng-click="disable()">Disable ui-select</button> + <button class="btn btn-default btn-xs" ng-click="clear()">Clear ng-model</button> + + <h1>(key, value) format</h1> + + <h2>Using value for binding</h2> + + <p>Selected: {{person.selectedValue}}</p> + <ui-select ng-model="person.selectedValue" theme="select2" ng-disabled="disabled" style="min-width: 300px;" title="Choose a person"> + <ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.value.name}}</ui-select-match> + <ui-select-choices repeat="person.value as (key, person) in peopleObj | filter: {'value':$select.search}"> + <div ng-bind-html="person.value.name | highlight: $select.search"></div> + <small> + email: {{person.value.email}} + age: <span ng-bind-html="''+person.value.age | highlight: $select.search"></span> + </small> + </ui-select-choices> + </ui-select> + + <h2>Using single property for binding</h2> + <p>Selected: {{person.selectedSingle}}</p> + <ui-select ng-model="person.selectedSingle" theme="select2" ng-disabled="disabled" style="min-width: 300px;" title="Choose a person"> + <ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.value.name}}</ui-select-match> + <ui-select-choices repeat="person.value.name as (key, person) in peopleObj | filter: {'value':$select.search}"> + <div ng-bind-html="person.value.name | highlight: $select.search"></div> + <small> + email: {{person.value.email}} + age: <span ng-bind-html="''+person.value.age | highlight: $select.search"></span> + </small> + </ui-select-choices> + </ui-select> + + <h2>Using key for binding</h2> + <p>Selected: {{person.selectedSingleKey}}</p> + <ui-select ng-model="person.selectedSingleKey" theme="select2" ng-disabled="disabled" style="min-width: 300px;" title="Choose a person"> + <ui-select-match placeholder="Select a person in the list or search his name/age...">{{$select.selected.value.name}}</ui-select-match> + <ui-select-choices repeat="person.key as (key, person) in peopleObj | filter: {'value':$select.search}"> + <div ng-bind-html="person.value.name | highlight: $select.search"></div> + <small> + email: {{person.value.email}} + age: <span ng-bind-html="''+person.value.age | highlight: $select.search"></span> + </small> + </ui-select-choices> + </ui-select> + +</body> +</html> 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( + '<ui-select ng-model="selection.selected"> \ + <ui-select-match placeholder="Pick one...">{{$select.selected.value.name}}</ui-select-match> \ + <ui-select-choices repeat="person.value as (key,person) in peopleObj | filter: $select.search"> \ + <div ng-bind-html="person.value.name | highlight: $select.search"></div> \ + <div ng-bind-html="person.value.email | highlight: $select.search"></div> \ + </ui-select-choices> \ + </ui-select>' + ); + // 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( + '<ui-select ng-model="selection.selected"> \ + <ui-select-match placeholder="Pick one...">{{$select.selected.value.name}}</ui-select-match> \ + <ui-select-choices repeat="person.value.name as (key,person) in peopleObj | filter: $select.search"> \ + <div ng-bind-html="person.value.name | highlight: $select.search"></div> \ + <div ng-bind-html="person.value.email | highlight: $select.search"></div> \ + </ui-select-choices> \ + </ui-select>' + ); + // 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( + '<ui-select ng-model="selection.selected"> \ + <ui-select-match placeholder="Pick one...">{{$select.selected.value.name}}</ui-select-match> \ + <ui-select-choices repeat="person.value.name as (key,person) in peopleObj | filter: $select.search"> \ + <div ng-bind-html="person.value.name | highlight: $select.search"></div> \ + <div ng-bind-html="person.value.email | highlight: $select.search"></div> \ + </ui-select-choices> \ + </ui-select>' + ); + + 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( + '<ui-select ng-model="selection.selected"> \ + <ui-select-match placeholder="Pick one...">{{$select.selected.value.name}}</ui-select-match> \ + <ui-select-choices repeat="person.key as (key,person) in peopleObj | filter: $select.search"> \ + <div ng-bind-html="person.value.name | highlight: $select.search"></div> \ + <div ng-bind-html="person.value.email | highlight: $select.search"></div> \ + </ui-select-choices> \ + </ui-select>' + ); + // 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 = '';