diff --git a/package.json b/package.json index 78cd771b6..dda889ca1 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,9 @@ "karma-ng-html2js-preprocessor": "^0.1.0", "karma-phantomjs-launcher": "~0.1.4" }, + "scripts": { + "postinstall": "bower install", + "test": "gulp test" + }, "license": "MIT" } diff --git a/src/select.js b/src/select.js index fb31b71a2..233c3e4a6 100644 --- a/src/select.js +++ b/src/select.js @@ -43,57 +43,40 @@ * Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat: * https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697 */ - .service('RepeatParser', ['uiSelectMinErr', function(uiSelectMinErr) { + .service('RepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) { var self = this; /** * Example: * expression = "address in addresses | filter: {street: $select.search} track by $index" - * lhs = "address", - * rhs = "addresses | filter: {street: $select.search}", + * itemName = "address", + * source = "addresses | filter: {street: $select.search}", * trackByExp = "$index", - * valueIdentifier = "address", - * keyIdentifier = undefined */ self.parse = function(expression) { - if (!expression) { - throw uiSelectMinErr('repeat', "Expected 'repeat' expression."); - } - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + var match = expression.match(/^\s*(?:([\s\S]+?)\s+as\s+)?([\s\S]+?)\s+in\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}'.", - expression); - } - - var lhs = match[1]; // Left-hand side - var rhs = match[2]; // Right-hand side - var trackByExp = match[3]; - - match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/); - if (!match) { - throw uiSelectMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", - lhs); + expression); } - // Unused for now - // var valueIdentifier = match[3] || match[1]; - // var keyIdentifier = match[2]; - return { - lhs: lhs, - rhs: rhs, - trackByExp: trackByExp + itemName: match[2], // (lhs) Left-hand side, + source: match[3], // (rhs) Right-hand side, + trackByExp: match[4], + modelMapper: $parse(match[1] || match[2]) }; + }; self.getGroupNgRepeatExpression = function() { return '($group, $items) in $select.groups'; }; - self.getNgRepeatExpression = function(lhs, rhs, trackByExp, grouped) { - var expression = lhs + ' in ' + (grouped ? '$items' : rhs); + self.getNgRepeatExpression = function(itemName, source, trackByExp, grouped) { + var expression = itemName + ' in ' + (grouped ? '$items' : source); if (trackByExp) { expression += ' track by ' + trackByExp; } @@ -180,14 +163,15 @@ ctrl.items = items; } - var repeat = RepeatParser.parse(repeatAttr), - setItemsFn = groupByExp ? updateGroups : setPlainItems; + var setItemsFn = groupByExp ? updateGroups : setPlainItems; + + ctrl.parserResult = RepeatParser.parse(repeatAttr); ctrl.isGrouped = !!groupByExp; - ctrl.itemProperty = repeat.lhs; + ctrl.itemProperty = ctrl.parserResult.itemName; // See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259 - $scope.$watchCollection(repeat.rhs, function(items) { + $scope.$watchCollection(ctrl.parserResult.source, function(items) { if (items === undefined || items === null) { // If the user specifies undefined or null => reset the collection @@ -204,6 +188,7 @@ } }); + }; var _refreshDelayPromise; @@ -355,6 +340,32 @@ var $select = ctrls[0]; var ngModel = ctrls[1]; + //From view --> model + ngModel.$parsers.unshift(function (inputValue) { + var locals = {}; + locals[$select.parserResult.itemName] = inputValue; + var result = $select.parserResult.modelMapper(scope, locals); + return result; + }); + + //From model --> view + ngModel.$formatters.unshift(function (inputValue) { + var match = $select.parserResult.source.match(/^\s*([\S]+).*$/); + var data = scope[match[1]]; + if (data){ + for (var i = data.length - 1; i >= 0; i--) { + var locals = {}; + locals[$select.parserResult.itemName] = data[i]; + var result = $select.parserResult.modelMapper(scope, locals); + if (result == inputValue){ + return data[i]; + } + } + } + return inputValue; + }); + + //Idea from: https://github.com/ivaynberg/select2/blob/79b5bf6db918d7560bdd959109b7bcfb47edaf43/select2.js#L1954 var focusser = angular.element(""); $compile(focusser)(scope); @@ -533,9 +544,15 @@ }, compile: function(tElement, tAttrs) { - var repeat = RepeatParser.parse(tAttrs.repeat); - var groupByExp = tAttrs.groupBy; + + if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression."); + return function link(scope, element, attrs, $select, transcludeFn) { + + // var repeat = RepeatParser.parse(attrs.repeat); + var groupByExp = attrs.groupBy; + + $select.parseRepeatAttr(attrs.repeat, groupByExp); //Result ready at $select.parserResult if(groupByExp) { var groups = element.querySelectorAll('.ui-select-choices-group'); @@ -548,10 +565,9 @@ throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length); } - choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression(repeat.lhs, '$select.items', repeat.trackByExp, groupByExp)) - .attr('ng-mouseenter', '$select.setActiveItem('+repeat.lhs+')') - .attr('ng-click', '$select.select(' + repeat.lhs + ')'); - + choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression($select.parserResult.itemName, '$select.items', $select.parserResult.trackByExp, groupByExp)) + .attr('ng-mouseenter', '$select.setActiveItem('+$select.parserResult.itemName +')') + .attr('ng-click', '$select.select(' + $select.parserResult.itemName + ')'); transcludeFn(function(clone) { var rowsInner = element.querySelectorAll('.ui-select-choices-row-inner'); @@ -562,8 +578,6 @@ $compile(element)(scope); }); - $select.parseRepeatAttr(attrs.repeat, groupByExp); - scope.$watch('$select.search', function() { $select.activeIndex = 0; $select.refresh(attrs.refresh); diff --git a/test/select.spec.js b/test/select.spec.js index a9709ab0c..79486df60 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -8,7 +8,7 @@ describe('ui-select tests', function() { $rootScope = _$rootScope_; scope = $rootScope.$new(); $compile = _$compile_; - + scope.selection = {} scope.getGroupLabel = function(person) { return person.age % 2 ? 'even' : 'odd'; }; @@ -42,7 +42,7 @@ describe('ui-select tests', function() { } return compileTemplate( - ' \ + ' \ {{$select.selected.name}} \ \
\ @@ -102,7 +102,7 @@ describe('ui-select tests', function() { }); it('should correctly render initial state', function() { - scope.selection = scope.people[0]; + scope.selection.selected = scope.people[0]; var el = createUiSelect(); @@ -178,7 +178,7 @@ describe('ui-select tests', function() { scope.items = ['false']; var el = compileTemplate( - ' \ + ' \ {{$select.selected}} \ \
\ @@ -199,7 +199,7 @@ describe('ui-select tests', function() { } function createUiSelect() { return compileTemplate( - ' \ + ' \ {{$select.selected.name}} \ \
\ @@ -249,7 +249,7 @@ describe('ui-select tests', function() { describe('choices group by function', function() { function createUiSelect() { return compileTemplate( - ' \ + ' \ {{$select.selected.name}} \ \
\ @@ -268,7 +268,7 @@ describe('ui-select tests', function() { it('should throw when no ui-select-choices found', function() { expect(function() { compileTemplate( - ' \ + ' \ \ ' ); @@ -278,7 +278,7 @@ describe('ui-select tests', function() { it('should throw when no repeat attribute is provided to ui-select-choices', function() { expect(function() { compileTemplate( - ' \ + ' \ \ ' ); @@ -288,9 +288,96 @@ describe('ui-select tests', function() { it('should throw when no ui-select-match found', function() { expect(function() { compileTemplate( - ' \ + ' \ \ ' ); }).toThrow(new Error('[ui.select:transcluded] Expected 1 .ui-select-match but got \'0\'.')); - });}); + }); + + it('should format the model correctly using alias', function() { + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + clickItem(el, 'Samantha'); + expect(scope.selection.selected).toBe(scope.people[5]); + }); + + it('should parse the model correctly using alias', function() { + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + scope.selection.selected = scope.people[5]; + scope.$digest(); + expect(getMatchLabel(el)).toEqual('Samantha'); + }); + + it('should format the model correctly using property of alias', function() { + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + clickItem(el, 'Samantha'); + expect(scope.selection.selected).toBe('Samantha'); + }); + + it('should parse the model correctly using property of alias', function() { + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + scope.selection.selected = 'Samantha'; + scope.$digest(); + expect(getMatchLabel(el)).toEqual('Samantha'); + }); + + it('should parse the model correctly using property of alias but passed whole object', function() { + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + scope.selection.selected = scope.people[5]; + scope.$digest(); + expect(getMatchLabel(el)).toEqual('Samantha'); + }); + + it('should format the model correctly without alias', function() { + var el = createUiSelect(); + clickItem(el, 'Samantha'); + expect(scope.selection.selected).toBe(scope.people[5]); + }); + + it('should parse the model correctly without alias', function() { + var el = createUiSelect(); + scope.selection.selected = scope.people[5]; + scope.$digest(); + expect(getMatchLabel(el)).toEqual('Samantha'); + }); +});