Skip to content

Commit 992477a

Browse files
committed
Merge pull request #443 from antmt/select2_directive_bug_fix
Select2 Refactor
2 parents eea2cb1 + 8d64b8f commit 992477a

File tree

2 files changed

+171
-11
lines changed

2 files changed

+171
-11
lines changed

modules/directives/select2/select2.js

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,23 @@ angular.module('ui.directives').directive('uiSelect2', ['ui.config', '$timeout',
4545
// Watch the model for programmatic changes
4646
controller.$render = function () {
4747
if (isSelect) {
48-
elm.select2('val', controller.$modelValue);
48+
elm.select2('val', controller.$viewValue);
4949
} else {
5050
if (isMultiple) {
51-
if (!controller.$modelValue) {
51+
if (!controller.$viewValue) {
5252
elm.select2('data', []);
53-
} else if (angular.isArray(controller.$modelValue)) {
54-
elm.select2('data', controller.$modelValue);
53+
} else if (angular.isArray(controller.$viewValue)) {
54+
elm.select2('data', controller.$viewValue);
5555
} else {
56-
elm.select2('val', controller.$modelValue);
56+
elm.select2('val', controller.$viewValue);
5757
}
5858
} else {
59-
if (angular.isObject(controller.$modelValue)) {
60-
elm.select2('data', controller.$modelValue);
59+
if (angular.isObject(controller.$viewValue)) {
60+
elm.select2('data', controller.$viewValue);
61+
} else if (!controller.$viewValue) {
62+
elm.select2('data', null);
6163
} else {
62-
elm.select2('val', controller.$modelValue);
64+
elm.select2('val', controller.$viewValue);
6365
}
6466
}
6567
}
@@ -109,11 +111,17 @@ angular.module('ui.directives').directive('uiSelect2', ['ui.config', '$timeout',
109111
}
110112

111113
// Set initial value since Angular doesn't
112-
elm.val(scope.$eval(attrs.ngModel));
114+
//elm.val(scope.$eval(attrs.ngModel));
113115

114116
// Initialize the plugin late so that the injected DOM does not disrupt the template compiler
115117
$timeout(function () {
116118
elm.select2(opts);
119+
120+
// Set initial value - I'm not sure about this but it seems to need to be there
121+
elm.val(controller.$viewValue);
122+
// important!
123+
controller.$render();
124+
117125
// Not sure if I should just check for !isSelect OR if I should check for 'tags' key
118126
if (!opts.initSelection && !isSelect)
119127
controller.$setViewValue(elm.select2('data'));

modules/directives/select2/test/select2Spec.js

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
// a helper directive for injecting formatters and parsers
2+
angular.module('ui.directives').directive('injectTransformers', [ function () {
3+
return {
4+
restrict: 'A',
5+
require: 'ngModel',
6+
priority: -1,
7+
link: function (scope, element, attr, ngModel) {
8+
var local = scope.$eval(attr.injectTransformers);
9+
10+
if (!angular.isObject(local) || !angular.isFunction(local.fromModel) || !angular.isFunction(local.fromElement)) {
11+
throw "The injectTransformers directive must be bound to an object with two functions (`fromModel` and `fromElement`)";
12+
}
13+
14+
ngModel.$parsers.push(local.fromElement);
15+
ngModel.$formatters.push(local.fromModel);
16+
}
17+
};
18+
}]);
19+
120
/*global describe, beforeEach, module, inject, it, spyOn, expect, $ */
221
describe('uiSelect2', function () {
322
'use strict';
@@ -16,6 +35,54 @@ describe('uiSelect2', function () {
1635
query.callback(data);
1736
}
1837
};
38+
39+
scope.transformers = {
40+
fromModel: function (modelValue) {
41+
if (!modelValue) {
42+
return modelValue;
43+
}
44+
45+
if (angular.isArray(modelValue)) {
46+
return modelValue.map(function (val) {
47+
val.text += " - I've been formatted";
48+
return val;
49+
});
50+
}
51+
52+
if (angular.isObject(modelValue)) {
53+
modelValue.text += " - I've been formatted";
54+
return modelValue;
55+
}
56+
57+
return modelValue + " - I've been formatted";
58+
},
59+
fromElement: function (elementValue) {
60+
var suffix = " - I've been formatted";
61+
62+
if (!elementValue) {
63+
return elementValue;
64+
}
65+
66+
if (angular.isArray(elementValue)) {
67+
return elementValue.map(function (val) {
68+
val.text += val.text.slice(0, val.text.indexOf(" - I've been formatted"));
69+
return val;
70+
});
71+
}
72+
73+
if (angular.isObject(elementValue)) {
74+
75+
elementValue.text = elementValue.text.slice(0, elementValue.text.indexOf(suffix));
76+
return elementValue;
77+
}
78+
79+
if (elementValue) {
80+
return elementValue.slice(0, elementValue.indexOf(suffix));
81+
}
82+
83+
return undefined;
84+
}
85+
};
1986
}));
2087

2188
/**
@@ -94,7 +161,7 @@ describe('uiSelect2', function () {
94161
compile('<input ui-select2/>');
95162
}).toThrow();
96163
});
97-
it('should creae proper DOM structure', function () {
164+
it('should create proper DOM structure', function () {
98165
var element = compile('<input ui-select2="options" ng-model="foo"/>');
99166
expect(element.siblings().is('div.select2-container')).toBe(true);
100167
});
@@ -135,10 +202,95 @@ describe('uiSelect2', function () {
135202
});
136203
});
137204
});
205+
describe('consumers of ngModel should correctly use $viewValue', function() {
206+
it('should use any formatters if present (select - single select)', function(){
207+
scope.foo = 'First';
208+
var element = compile('<select ui-select2 ng-model="foo" inject-transformers="transformers"><option>First - I\'ve been formatted</option><option>Second - I\'ve been formatted</option></select>');
209+
expect(element.select2('val')).toBe('First - I\'ve been formatted');
210+
scope.$apply('foo = "Second"');
211+
expect(element.select2('val')).toBe('Second - I\'ve been formatted');
212+
});
213+
214+
// isMultiple && falsey
215+
it('should use any formatters if present (input multi select - falsey value)', function() {
216+
// need special function to hit this case
217+
// old code checked modelValue... can't just pass undefined to model value because view value will be the same
218+
scope.transformers.fromModel = function(modelValue) {
219+
if (modelValue === "magic") {
220+
return undefined;
221+
}
222+
223+
return modelValue;
224+
};
225+
226+
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
227+
spyOn($.fn, 'select2');
228+
scope.$apply('foo="magic"');
229+
expect(element.select2).toHaveBeenCalledWith('data', []);
230+
});
231+
// isMultiple && isArray
232+
it('should use any formatters if present (input multi select)', function() {
233+
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
234+
spyOn($.fn, 'select2');
235+
scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]');
236+
expect(element.select2).toHaveBeenCalledWith('data', [{ id: 1, text: "first - I've been formatted" },{ id: 2, text: "second - I've been formatted" }]);
237+
});
238+
// isMultiple...
239+
it('should use any formatters if present (input multi select - non array)', function() {
240+
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
241+
spyOn($.fn, 'select2');
242+
scope.$apply('foo={ id: 1, text: "first" }');
243+
expect(element.select2).toHaveBeenCalledWith('val', { id: 1, text: "first - I've been formatted" });
244+
});
245+
246+
// !isMultiple
247+
it('should use any formatters if present (input - single select - object)', function() {
248+
var element = compile('<input ng-model="foo" ui-select2="options" inject-transformers="transformers">');
249+
spyOn($.fn, 'select2');
250+
scope.$apply('foo={ id: 1, text: "first" }');
251+
expect(element.select2).toHaveBeenCalledWith('data', { id: 1, text: "first - I've been formatted" });
252+
});
253+
it('should use any formatters if present (input - single select - non object)', function() {
254+
var element = compile('<input ng-model="foo" ui-select2="options" inject-transformers="transformers">');
255+
spyOn($.fn, 'select2');
256+
scope.$apply('foo="first"');
257+
expect(element.select2).toHaveBeenCalledWith('val', "first - I've been formatted");
258+
});
259+
260+
it('should not set the default value using scope.$eval', function() {
261+
// testing directive instantiation - change order of test
262+
spyOn($.fn, 'select2');
263+
spyOn($.fn, 'val');
264+
scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]');
265+
266+
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
267+
expect(element.val).not.toHaveBeenCalledWith([{ id: 1, text: "first" },{ id: 2, text: "second" }]);
268+
});
269+
it('should expect a default value to be set with a call to the render method', function() {
270+
// this should monitor the events after init, when the timeout callback executes
271+
var opts = angular.copy(scope.options);
272+
opts.multiple = true;
273+
274+
scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]');
275+
276+
spyOn($.fn, 'select2');
277+
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
278+
279+
// select 2 init
280+
expect(element.select2).toHaveBeenCalledWith(opts);
281+
282+
// callback setting
283+
expect(element.select2).toHaveBeenCalledWith('data', [{ id: 1, text: "first - I've been formatted" },{ id: 2, text: "second - I've been formatted" }]);
284+
285+
// retieve data
286+
expect(element.select2).toHaveBeenCalledWith('data');
287+
});
288+
289+
});
138290
it('should set the model when the user selects an item', function(){
139291
var element = compile('<input ng-model="foo" multiple ui-select2="options">');
140292
// TODO: programmactically select an option
141-
// expect(scope.foo).toBe(/* selected val */);
293+
// expect(scope.foo).toBe(/* selected val */) ;
142294
});
143295
});
144296
});

0 commit comments

Comments
 (0)