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

Commit 331f819

Browse files
meyerdswesleycho
authored andcommitted
fix(tagging): do not remove selected items when invalid
- Prevent accidental removal of a tagged item that subsequently becomes invalid Closes #1359
1 parent 349b96e commit 331f819

File tree

3 files changed

+118
-3
lines changed

3 files changed

+118
-3
lines changed

src/uiSelectController.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,11 @@ uis.controller('uiSelectCtrl',
265265
//Remove already selected items (ex: while searching)
266266
//TODO Should add a test
267267
ctrl.refreshItems(items);
268-
ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
268+
269+
//update the view value with fresh data from items, if there is a valid model value
270+
if(angular.isDefined(ctrl.ngModel.$modelValue)) {
271+
ctrl.ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
272+
}
269273
}
270274
}
271275
});

src/uiSelectMultipleDirective.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,10 @@ uis.directive('uiSelectMultiple', ['uiSelectMinErr','$timeout', function(uiSelec
146146
//Watch for external model changes
147147
scope.$watchCollection(function(){ return ngModel.$modelValue; }, function(newValue, oldValue) {
148148
if (oldValue != newValue){
149-
ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
149+
//update the view value with fresh data from items, if there is a valid model value
150+
if(angular.isDefined(ngModel.$modelValue)) {
151+
ngModel.$modelValue = null; //Force scope model value and ngModel value to be out of sync to re-run formatters
152+
}
150153
$selectMultiple.refreshComponent();
151154
}
152155
});

test/select.spec.js

+109-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,37 @@ describe('ui-select tests', function() {
3636

3737
});
3838

39-
beforeEach(module('ngSanitize', 'ui.select', 'wrapperDirective'));
39+
/* Create a directive that can be applied to the ui-select instance to test
40+
* the effects of Angular's validation process on the control.
41+
*
42+
* Does not currently work with single property binding. Looks at the
43+
* selected object or objects for a "valid" property. If all selected objects
44+
* have a "valid" property that is truthy, the validator passes.
45+
*/
46+
angular.module('testValidator', []);
47+
angular.module('testValidator').directive('testValidator', function() {
48+
return {
49+
restrict: 'A',
50+
require: 'ngModel',
51+
link: function(scope, element, attrs, ngModel) {
52+
ngModel.$validators.testValidator = function(modelValue, viewValue) {
53+
if(angular.isUndefined(modelValue) || modelValue === null) {
54+
return true;
55+
} else if(angular.isArray(modelValue)) {
56+
var allValid = true, idx = modelValue.length;
57+
while(idx-- > 0 && allValid) {
58+
allValid = allValid && modelValue[idx].valid;
59+
}
60+
return allValid;
61+
} else {
62+
return !!modelValue.valid;
63+
}
64+
};
65+
}
66+
}
67+
});
68+
69+
beforeEach(module('ngSanitize', 'ui.select', 'wrapperDirective', 'testValidator'));
4070

4171
beforeEach(function() {
4272
module(function($provide) {
@@ -1510,6 +1540,45 @@ describe('ui-select tests', function() {
15101540

15111541
});
15121542

1543+
it('should retain an invalid view value after refreshing items', function() {
1544+
scope.taggingFunc = function (name) {
1545+
return {
1546+
name: name,
1547+
email: name + '@email.com',
1548+
valid: name === "iamvalid"
1549+
};
1550+
};
1551+
1552+
var el = compileTemplate(
1553+
'<ui-select ng-model="selection.selected" tagging="taggingFunc" tagging-label="false" test-validator> \
1554+
<ui-select-match placeholder="Pick one...">{{$select.selected.email}}</ui-select-match> \
1555+
<ui-select-choices repeat="person in people | filter: $select.search"> \
1556+
<div ng-bind-html="person.name" | highlight: $select.search"></div> \
1557+
<div ng-bind-html="person.email | highlight: $select.search"></div> \
1558+
</ui-select-choices> \
1559+
</ui-select>'
1560+
);
1561+
1562+
clickMatch(el);
1563+
var searchInput = el.find('.ui-select-search');
1564+
1565+
setSearchText(el, 'iamvalid');
1566+
triggerKeydown(searchInput, Key.Tab);
1567+
1568+
//model value defined because it's valid, view value defined as expected
1569+
var validTag = scope.taggingFunc("iamvalid");
1570+
expect(scope.selection.selected).toEqual(validTag);
1571+
expect($(el).scope().$select.selected).toEqual(validTag);
1572+
1573+
clickMatch(el);
1574+
setSearchText(el, 'notvalid');
1575+
triggerKeydown(searchInput, Key.Tab);
1576+
1577+
//model value undefined because it's invalid, view value STILL defined as expected
1578+
expect(scope.selection.selected).toEqual(undefined);
1579+
expect($(el).scope().$select.selected).toEqual(scope.taggingFunc("notvalid"));
1580+
});
1581+
15131582
describe('search-enabled option', function() {
15141583

15151584
var el;
@@ -2159,6 +2228,45 @@ describe('ui-select tests', function() {
21592228

21602229
});
21612230

2231+
it('should retain an invalid view value after refreshing items', function() {
2232+
scope.taggingFunc = function (name) {
2233+
return {
2234+
name: name,
2235+
email: name + '@email.com',
2236+
valid: name === "iamvalid"
2237+
};
2238+
};
2239+
2240+
var el = compileTemplate(
2241+
'<ui-select multiple ng-model="selection.selectedMultiple" tagging="taggingFunc" tagging-label="false" test-validator> \
2242+
<ui-select-match placeholder="Pick one...">{{$select.selected.email}}</ui-select-match> \
2243+
<ui-select-choices repeat="person in people | filter: $select.search"> \
2244+
<div ng-bind-html="person.name" | highlight: $select.search"></div> \
2245+
<div ng-bind-html="person.email | highlight: $select.search"></div> \
2246+
</ui-select-choices> \
2247+
</ui-select>'
2248+
);
2249+
2250+
clickMatch(el);
2251+
var searchInput = el.find('.ui-select-search');
2252+
2253+
setSearchText(el, 'iamvalid');
2254+
triggerKeydown(searchInput, Key.Tab);
2255+
2256+
//model value defined because it's valid, view value defined as expected
2257+
var validTag = scope.taggingFunc("iamvalid");
2258+
expect(scope.selection.selectedMultiple).toEqual([jasmine.objectContaining(validTag)]);
2259+
expect($(el).scope().$select.selected).toEqual([jasmine.objectContaining(validTag)]);
2260+
2261+
clickMatch(el);
2262+
setSearchText(el, 'notvalid');
2263+
triggerKeydown(searchInput, Key.Tab);
2264+
2265+
//model value undefined because it's invalid, view value STILL defined as expected
2266+
var invalidTag = scope.taggingFunc("notvalid");
2267+
expect(scope.selection.selected).toEqual(undefined);
2268+
expect($(el).scope().$select.selected).toEqual([jasmine.objectContaining(validTag), jasmine.objectContaining(invalidTag)]);
2269+
});
21622270

21632271
it('should run $formatters when changing model directly', function () {
21642272

0 commit comments

Comments
 (0)