Skip to content
This repository was archived by the owner on Oct 2, 2019. It is now read-only.

Commit 0fc1f35

Browse files
committed
Merge pull request #107 from angular-ui/feat-modelmapper
feat(ngModel): bind to single property instead of entire object
2 parents 0603827 + c2183c2 commit 0fc1f35

File tree

3 files changed

+156
-51
lines changed

3 files changed

+156
-51
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,9 @@
2929
"karma-ng-html2js-preprocessor": "^0.1.0",
3030
"karma-phantomjs-launcher": "~0.1.4"
3131
},
32+
"scripts": {
33+
"postinstall": "bower install",
34+
"test": "gulp test"
35+
},
3236
"license": "MIT"
3337
}

src/select.js

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,57 +43,40 @@
4343
* Original discussion about parsing "repeat" attribute instead of fully relying on ng-repeat:
4444
* https://github.com/angular-ui/ui-select/commit/5dd63ad#commitcomment-5504697
4545
*/
46-
.service('RepeatParser', ['uiSelectMinErr', function(uiSelectMinErr) {
46+
.service('RepeatParser', ['uiSelectMinErr','$parse', function(uiSelectMinErr, $parse) {
4747
var self = this;
4848

4949
/**
5050
* Example:
5151
* expression = "address in addresses | filter: {street: $select.search} track by $index"
52-
* lhs = "address",
53-
* rhs = "addresses | filter: {street: $select.search}",
52+
* itemName = "address",
53+
* source = "addresses | filter: {street: $select.search}",
5454
* trackByExp = "$index",
55-
* valueIdentifier = "address",
56-
* keyIdentifier = undefined
5755
*/
5856
self.parse = function(expression) {
59-
if (!expression) {
60-
throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");
61-
}
6257

63-
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
58+
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*$/);
6459

6560
if (!match) {
6661
throw uiSelectMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
67-
expression);
68-
}
69-
70-
var lhs = match[1]; // Left-hand side
71-
var rhs = match[2]; // Right-hand side
72-
var trackByExp = match[3];
73-
74-
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
75-
if (!match) {
76-
throw uiSelectMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
77-
lhs);
62+
expression);
7863
}
7964

80-
// Unused for now
81-
// var valueIdentifier = match[3] || match[1];
82-
// var keyIdentifier = match[2];
83-
8465
return {
85-
lhs: lhs,
86-
rhs: rhs,
87-
trackByExp: trackByExp
66+
itemName: match[2], // (lhs) Left-hand side,
67+
source: match[3], // (rhs) Right-hand side,
68+
trackByExp: match[4],
69+
modelMapper: $parse(match[1] || match[2])
8870
};
71+
8972
};
9073

9174
self.getGroupNgRepeatExpression = function() {
9275
return '($group, $items) in $select.groups';
9376
};
9477

95-
self.getNgRepeatExpression = function(lhs, rhs, trackByExp, grouped) {
96-
var expression = lhs + ' in ' + (grouped ? '$items' : rhs);
78+
self.getNgRepeatExpression = function(itemName, source, trackByExp, grouped) {
79+
var expression = itemName + ' in ' + (grouped ? '$items' : source);
9780
if (trackByExp) {
9881
expression += ' track by ' + trackByExp;
9982
}
@@ -180,14 +163,15 @@
180163
ctrl.items = items;
181164
}
182165

183-
var repeat = RepeatParser.parse(repeatAttr),
184-
setItemsFn = groupByExp ? updateGroups : setPlainItems;
166+
var setItemsFn = groupByExp ? updateGroups : setPlainItems;
167+
168+
ctrl.parserResult = RepeatParser.parse(repeatAttr);
185169

186170
ctrl.isGrouped = !!groupByExp;
187-
ctrl.itemProperty = repeat.lhs;
171+
ctrl.itemProperty = ctrl.parserResult.itemName;
188172

189173
// See https://github.com/angular/angular.js/blob/v1.2.15/src/ng/directive/ngRepeat.js#L259
190-
$scope.$watchCollection(repeat.rhs, function(items) {
174+
$scope.$watchCollection(ctrl.parserResult.source, function(items) {
191175

192176
if (items === undefined || items === null) {
193177
// If the user specifies undefined or null => reset the collection
@@ -204,6 +188,7 @@
204188
}
205189

206190
});
191+
207192
};
208193

209194
var _refreshDelayPromise;
@@ -355,6 +340,32 @@
355340
var $select = ctrls[0];
356341
var ngModel = ctrls[1];
357342

343+
//From view --> model
344+
ngModel.$parsers.unshift(function (inputValue) {
345+
var locals = {};
346+
locals[$select.parserResult.itemName] = inputValue;
347+
var result = $select.parserResult.modelMapper(scope, locals);
348+
return result;
349+
});
350+
351+
//From model --> view
352+
ngModel.$formatters.unshift(function (inputValue) {
353+
var match = $select.parserResult.source.match(/^\s*([\S]+).*$/);
354+
var data = scope[match[1]];
355+
if (data){
356+
for (var i = data.length - 1; i >= 0; i--) {
357+
var locals = {};
358+
locals[$select.parserResult.itemName] = data[i];
359+
var result = $select.parserResult.modelMapper(scope, locals);
360+
if (result == inputValue){
361+
return data[i];
362+
}
363+
}
364+
}
365+
return inputValue;
366+
});
367+
368+
358369
//Idea from: https://github.com/ivaynberg/select2/blob/79b5bf6db918d7560bdd959109b7bcfb47edaf43/select2.js#L1954
359370
var focusser = angular.element("<input ng-disabled='$select.disabled' class='ui-select-focusser ui-select-offscreen' type='text' aria-haspopup='true' role='button' />");
360371
$compile(focusser)(scope);
@@ -543,9 +554,15 @@
543554
},
544555

545556
compile: function(tElement, tAttrs) {
546-
var repeat = RepeatParser.parse(tAttrs.repeat);
547-
var groupByExp = tAttrs.groupBy;
557+
558+
if (!tAttrs.repeat) throw uiSelectMinErr('repeat', "Expected 'repeat' expression.");
559+
548560
return function link(scope, element, attrs, $select, transcludeFn) {
561+
562+
// var repeat = RepeatParser.parse(attrs.repeat);
563+
var groupByExp = attrs.groupBy;
564+
565+
$select.parseRepeatAttr(attrs.repeat, groupByExp); //Result ready at $select.parserResult
549566

550567
if(groupByExp) {
551568
var groups = element.querySelectorAll('.ui-select-choices-group');
@@ -558,10 +575,9 @@
558575
throw uiSelectMinErr('rows', "Expected 1 .ui-select-choices-row but got '{0}'.", choices.length);
559576
}
560577

561-
choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression(repeat.lhs, '$select.items', repeat.trackByExp, groupByExp))
562-
.attr('ng-mouseenter', '$select.setActiveItem('+repeat.lhs+')')
563-
.attr('ng-click', '$select.select(' + repeat.lhs + ')');
564-
578+
choices.attr('ng-repeat', RepeatParser.getNgRepeatExpression($select.parserResult.itemName, '$select.items', $select.parserResult.trackByExp, groupByExp))
579+
.attr('ng-mouseenter', '$select.setActiveItem('+$select.parserResult.itemName +')')
580+
.attr('ng-click', '$select.select(' + $select.parserResult.itemName + ')');
565581

566582
transcludeFn(function(clone) {
567583
var rowsInner = element.querySelectorAll('.ui-select-choices-row-inner');
@@ -572,8 +588,6 @@
572588
$compile(element)(scope);
573589
});
574590

575-
$select.parseRepeatAttr(attrs.repeat, groupByExp);
576-
577591
scope.$watch('$select.search', function() {
578592
$select.activeIndex = 0;
579593
$select.refresh(attrs.refresh);

test/select.spec.js

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('ui-select tests', function() {
88
$rootScope = _$rootScope_;
99
scope = $rootScope.$new();
1010
$compile = _$compile_;
11-
11+
scope.selection = {}
1212
scope.getGroupLabel = function(person) {
1313
return person.age % 2 ? 'even' : 'odd';
1414
};
@@ -42,7 +42,7 @@ describe('ui-select tests', function() {
4242
}
4343

4444
return compileTemplate(
45-
'<ui-select ng-model="selection"' + attrsHtml + '> \
45+
'<ui-select ng-model="selection.selected"' + attrsHtml + '> \
4646
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
4747
<ui-select-choices repeat="person in people | filter: $select.search"> \
4848
<div ng-bind-html="person.name | highlight: $select.search"></div> \
@@ -102,7 +102,7 @@ describe('ui-select tests', function() {
102102
});
103103

104104
it('should correctly render initial state', function() {
105-
scope.selection = scope.people[0];
105+
scope.selection.selected = scope.people[0];
106106

107107
var el = createUiSelect();
108108

@@ -178,7 +178,7 @@ describe('ui-select tests', function() {
178178
scope.items = ['false'];
179179

180180
var el = compileTemplate(
181-
'<ui-select ng-model="selection"> \
181+
'<ui-select ng-model="selection.selected"> \
182182
<ui-select-match>{{$select.selected}}</ui-select-match> \
183183
<ui-select-choices repeat="item in items | filter: $select.search"> \
184184
<div ng-bind-html="item | highlight: $select.search"></div> \
@@ -199,7 +199,7 @@ describe('ui-select tests', function() {
199199
}
200200
function createUiSelect() {
201201
return compileTemplate(
202-
'<ui-select ng-model="selection"> \
202+
'<ui-select ng-model="selection.selected"> \
203203
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
204204
<ui-select-choices group-by="\'group\'" repeat="person in people | filter: $select.search"> \
205205
<div ng-bind-html="person.name | highlight: $select.search"></div> \
@@ -249,7 +249,7 @@ describe('ui-select tests', function() {
249249
describe('choices group by function', function() {
250250
function createUiSelect() {
251251
return compileTemplate(
252-
'<ui-select ng-model="selection"> \
252+
'<ui-select ng-model="selection.selected"> \
253253
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
254254
<ui-select-choices group-by="getGroupLabel" repeat="person in people | filter: $select.search"> \
255255
<div ng-bind-html="person.name | highlight: $select.search"></div> \
@@ -268,7 +268,7 @@ describe('ui-select tests', function() {
268268
it('should throw when no ui-select-choices found', function() {
269269
expect(function() {
270270
compileTemplate(
271-
'<ui-select ng-model="selection"> \
271+
'<ui-select ng-model="selection.selected"> \
272272
<ui-select-match></ui-select-match> \
273273
</ui-select>'
274274
);
@@ -278,7 +278,7 @@ describe('ui-select tests', function() {
278278
it('should throw when no repeat attribute is provided to ui-select-choices', function() {
279279
expect(function() {
280280
compileTemplate(
281-
'<ui-select ng-model="selection"> \
281+
'<ui-select ng-model="selection.selected"> \
282282
<ui-select-choices></ui-select-choices> \
283283
</ui-select>'
284284
);
@@ -288,9 +288,96 @@ describe('ui-select tests', function() {
288288
it('should throw when no ui-select-match found', function() {
289289
expect(function() {
290290
compileTemplate(
291-
'<ui-select ng-model="selection"> \
291+
'<ui-select ng-model="selection.selected"> \
292292
<ui-select-choices repeat="item in items"></ui-select-choices> \
293293
</ui-select>'
294294
);
295295
}).toThrow(new Error('[ui.select:transcluded] Expected 1 .ui-select-match but got \'0\'.'));
296-
});});
296+
});
297+
298+
it('should format the model correctly using alias', function() {
299+
var el = compileTemplate(
300+
'<ui-select ng-model="selection.selected"> \
301+
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
302+
<ui-select-choices repeat="person as person in people | filter: $select.search"> \
303+
<div ng-bind-html="person.name | highlight: $select.search"></div> \
304+
<div ng-bind-html="person.email | highlight: $select.search"></div> \
305+
</ui-select-choices> \
306+
</ui-select>'
307+
);
308+
clickItem(el, 'Samantha');
309+
expect(scope.selection.selected).toBe(scope.people[5]);
310+
});
311+
312+
it('should parse the model correctly using alias', function() {
313+
var el = compileTemplate(
314+
'<ui-select ng-model="selection.selected"> \
315+
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
316+
<ui-select-choices repeat="person as person in people | filter: $select.search"> \
317+
<div ng-bind-html="person.name | highlight: $select.search"></div> \
318+
<div ng-bind-html="person.email | highlight: $select.search"></div> \
319+
</ui-select-choices> \
320+
</ui-select>'
321+
);
322+
scope.selection.selected = scope.people[5];
323+
scope.$digest();
324+
expect(getMatchLabel(el)).toEqual('Samantha');
325+
});
326+
327+
it('should format the model correctly using property of alias', function() {
328+
var el = compileTemplate(
329+
'<ui-select ng-model="selection.selected"> \
330+
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
331+
<ui-select-choices repeat="person.name as person in people | filter: $select.search"> \
332+
<div ng-bind-html="person.name | highlight: $select.search"></div> \
333+
<div ng-bind-html="person.email | highlight: $select.search"></div> \
334+
</ui-select-choices> \
335+
</ui-select>'
336+
);
337+
clickItem(el, 'Samantha');
338+
expect(scope.selection.selected).toBe('Samantha');
339+
});
340+
341+
it('should parse the model correctly using property of alias', function() {
342+
var el = compileTemplate(
343+
'<ui-select ng-model="selection.selected"> \
344+
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
345+
<ui-select-choices repeat="person.name as person in people | filter: $select.search"> \
346+
<div ng-bind-html="person.name | highlight: $select.search"></div> \
347+
<div ng-bind-html="person.email | highlight: $select.search"></div> \
348+
</ui-select-choices> \
349+
</ui-select>'
350+
);
351+
scope.selection.selected = 'Samantha';
352+
scope.$digest();
353+
expect(getMatchLabel(el)).toEqual('Samantha');
354+
});
355+
356+
it('should parse the model correctly using property of alias but passed whole object', function() {
357+
var el = compileTemplate(
358+
'<ui-select ng-model="selection.selected"> \
359+
<ui-select-match placeholder="Pick one...">{{$select.selected.name}}</ui-select-match> \
360+
<ui-select-choices repeat="person.name as person in people | filter: $select.search"> \
361+
<div ng-bind-html="person.name | highlight: $select.search"></div> \
362+
<div ng-bind-html="person.email | highlight: $select.search"></div> \
363+
</ui-select-choices> \
364+
</ui-select>'
365+
);
366+
scope.selection.selected = scope.people[5];
367+
scope.$digest();
368+
expect(getMatchLabel(el)).toEqual('Samantha');
369+
});
370+
371+
it('should format the model correctly without alias', function() {
372+
var el = createUiSelect();
373+
clickItem(el, 'Samantha');
374+
expect(scope.selection.selected).toBe(scope.people[5]);
375+
});
376+
377+
it('should parse the model correctly without alias', function() {
378+
var el = createUiSelect();
379+
scope.selection.selected = scope.people[5];
380+
scope.$digest();
381+
expect(getMatchLabel(el)).toEqual('Samantha');
382+
});
383+
});

0 commit comments

Comments
 (0)