Skip to content
This repository was archived by the owner on Jan 27, 2018. It is now read-only.

Commit 9133f40

Browse files
committed
Two-way bind to model and options without destroying selectize
Initializes a <select> as a selectize component a single time. Batches updates to model and options once per timeout. Fixes EvanOxfeld#3
1 parent 1a695bc commit 9133f40

File tree

3 files changed

+187
-40
lines changed

3 files changed

+187
-40
lines changed

angular-selectize.js

Lines changed: 81 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
angular.module('selectize', [])
2727

28-
.directive('selectize', ['$parse', function($parse) {
28+
.directive('selectize', ['$parse', '$timeout', function($parse, $timeout) {
2929
var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
3030

3131
return {
@@ -39,45 +39,75 @@
3939
var match = attrs.ngOptions.match(NG_OPTIONS_REGEXP);
4040
var valueName = match[4] || match[6];
4141
var optionsProperty = match[7];
42+
var displayFn = $parse(match[2] || match[1]);
4243
var valueFn = $parse(match[2] ? match[1] : valueName);
43-
var selectize;
44+
var selectize, newSelections, newOptions, updateTimer;
4445

45-
scope.$parent.$watch(function() {
46+
scope.$watchCollection(function() {
4647
return ngModelCtrl.$modelValue;
47-
}, function(newValues, oldValues) {
48-
if (selectize && !angular.equals(newValues, oldValues) && !modelMatchesView()) {
49-
refreshSelectize();
48+
}, function(modelValue) {
49+
if (!selectize) {
50+
return;
51+
}
52+
if (!newSelections) {
53+
newSelections = getSelectedItems(modelValue);
54+
}
55+
if (!updateTimer) {
56+
scheduleUpdate();
57+
}
58+
});
59+
60+
scope.$parent.$watchCollection(optionsProperty, function(options) {
61+
if (!selectize) {
62+
return initSelectize();
63+
}
64+
newOptions = newOptions ? newOptions: options;
65+
if (!updateTimer) {
66+
scheduleUpdate();
5067
}
5168
});
5269

53-
function modelMatchesView() {
54-
var model = ngModelCtrl.$modelValue;
55-
model = model && !angular.isArray(model) ? [model] : model || [];
56-
var view = selectize.items.map(function(item) {
57-
return getOptionValue(scope.$parent[optionsProperty][item]);
70+
function scheduleUpdate() {
71+
updateTimer = $timeout(function() {
72+
var selected = newSelections || selectize.items;
73+
selectize.clear();
74+
if (newOptions) {
75+
selectize.clearOptions();
76+
selectize.load(function(cb) {
77+
cb(newOptions.map(function(option, index) {
78+
return {
79+
text: getOptionLabel(option),
80+
value: index
81+
};
82+
}));
83+
});
84+
}
85+
selected.forEach(function(item) {
86+
selectize.addItem(item);
87+
});
88+
newSelections = null;
89+
newOptions = null;
90+
updateTimer = null;
5891
});
59-
return angular.equals(model, view);
6092
}
6193

62-
scope.$parent.$watchCollection(optionsProperty, refreshSelectize);
63-
64-
function refreshSelectize(newOptions, oldOptions) {
65-
if (selectize) {
66-
selectize.destroy();
67-
}
94+
function initSelectize() {
6895
scope.$evalAsync(function() {
69-
$(element).selectize(opts);
70-
selectize = $(element)[0].selectize;
96+
element.selectize(opts);
97+
selectize = element[0].selectize;
7198
if (scope.multiple) {
7299
selectize.on('item_add', function(value, $item) {
73100
var model = ngModelCtrl.$viewValue;
74101
var option = scope.$parent[optionsProperty][value];
75102
value = option ? getOptionValue(option) : value;
76103

77-
model.push(value);
78-
scope.$evalAsync(function() {
79-
ngModelCtrl.$setViewValue(model);
80-
});
104+
if (model.indexOf(value) === -1) {
105+
model.push(value);
106+
scope.$parent[optionsProperty].push(value);
107+
scope.$evalAsync(function() {
108+
ngModelCtrl.$setViewValue(model);
109+
});
110+
}
81111
});
82112
selectize.on('item_remove', function(value) {
83113
var model = ngModelCtrl.$viewValue;
@@ -96,11 +126,38 @@
96126
});
97127
}
98128

129+
function getSelectedItems(model) {
130+
model = typeof model === 'string' ? [model] : model || [];
131+
var selections = scope.$parent[optionsProperty].reduce(function(selected, option, index) {
132+
var optionValue = getOptionValue(option);
133+
if (model.indexOf(optionValue) >= 0) {
134+
selected[optionValue] = index;
135+
}
136+
return selected;
137+
}, {});
138+
return Object.keys(selections)
139+
.map(function(key) {
140+
return selections[key];
141+
});
142+
}
143+
99144
function getOptionValue(option) {
100145
var optionContext = {};
101146
optionContext[valueName] = option;
102147
return valueFn(optionContext);
103148
}
149+
150+
function getOptionLabel(option) {
151+
var optionContext = {};
152+
optionContext[valueName] = option;
153+
return displayFn(optionContext);
154+
}
155+
156+
scope.$on('$destroy', function() {
157+
if (updateTimer) {
158+
$timeout.cancel(updateTimer);
159+
}
160+
});
104161
}
105162
};
106163
}]);

test/multiselect.spec.js

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
describe('<select multiple selectize>', function() {
44
beforeEach(module('selectize'));
55

6-
var selectElement, selectize, scope, compile;
6+
var selectElement, selectize, scope, compile, timeout;
77

88
var stringOptions = ['foo', 'bar', 'baz'];
99
var objectOptions = [{
@@ -17,9 +17,10 @@ describe('<select multiple selectize>', function() {
1717
text: 'third'
1818
}];
1919

20-
beforeEach(inject(function ($rootScope, $compile) {
20+
beforeEach(inject(function ($rootScope, $compile, $timeout) {
2121
scope = $rootScope.$new();
2222
compile = $compile;
23+
timeout = $timeout;
2324
}));
2425

2526
afterEach(function() {
@@ -108,6 +109,10 @@ describe('<select multiple selectize>', function() {
108109
selectize.removeItem('foobar');
109110
assert.deepEqual(scope.selection, ['foo']);
110111
});
112+
113+
it('should update the options on scope', function() {
114+
assert.deepEqual(scope.options, ['foo', 'bar', 'baz', 'foobar']);
115+
})
111116
});
112117

113118
describe('when a selected option is unselected', function() {
@@ -129,18 +134,21 @@ describe('<select multiple selectize>', function() {
129134
it('should clear the selection when the model is empty', function() {
130135
scope.selection = [];
131136
scope.$apply();
137+
timeout.flush();
132138
assert.strictEqual(selectElement.find('option').length, 0);
133139
});
134140

135141
it('should update the selection when the model contains a single item', function() {
136142
scope.selection = ['bar'];
137143
scope.$apply();
144+
timeout.flush();
138145
testSelectedOptions(1);
139146
});
140147

141148
it('should update the selection when the model contains two items', function() {
142149
scope.selection = ['bar', 'baz'];
143150
scope.$apply();
151+
timeout.flush();
144152
testSelectedOptions([1,2]);
145153
});
146154
});
@@ -150,10 +158,30 @@ describe('<select multiple selectize>', function() {
150158
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length - scope.selection.length);
151159
scope.options.push('qux');
152160
scope.$apply();
153-
selectize = $(selectElement)[0].selectize;
154-
selectize.refreshOptions();
161+
timeout.flush();
162+
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length - scope.selection.length);
163+
});
164+
});
165+
166+
describe('when both the model and the options are updated', function() {
167+
it('should have the same number of options in the dropdown menu as scope.options', function() {
168+
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length - scope.selection.length);
169+
170+
scope.selection = ['bar', 'baz'];
171+
scope.options.push('qux');
172+
scope.$apply();
173+
timeout.flush();
174+
155175
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length - scope.selection.length);
156176
});
177+
178+
it('should update the selection', function() {
179+
scope.selection = ['bar', 'baz'];
180+
scope.options.push('qux');
181+
scope.$apply();
182+
timeout.flush();
183+
testSelectedOptions([1,2]);
184+
});
157185
});
158186
});
159187

@@ -227,18 +255,21 @@ describe('<select multiple selectize>', function() {
227255
it('should clear the selection when the model is empty', function() {
228256
scope.selection = [];
229257
scope.$apply();
258+
timeout.flush();
230259
assert.strictEqual(selectElement.find('option').length, 0);
231260
});
232261

233262
it('should update the selection when the model contains a single item', function() {
234263
scope.selection = ['guid2'];
235264
scope.$apply();
265+
timeout.flush();
236266
testSelectedOptions(1);
237267
});
238268

239269
it('should update the selection when the model contains two items', function() {
240270
scope.selection = ['guid2', 'guid3'];
241271
scope.$apply();
272+
timeout.flush();
242273
testSelectedOptions([1,2]);
243274
});
244275
});
@@ -251,8 +282,7 @@ describe('<select multiple selectize>', function() {
251282
text: 'fourth'
252283
});
253284
scope.$apply();
254-
selectize = $(selectElement)[0].selectize;
255-
selectize.refreshOptions();
285+
timeout.flush();
256286
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length - scope.selection.length);
257287
});
258288
});

test/select.spec.js

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
describe('<select selectize>', function() {
44
beforeEach(module('selectize'));
55

6-
var selectElement, selectize, scope, compile;
6+
var selectElement, selectize, scope, compile, timeout;
77

88
var stringOptions = ['foo', 'bar', 'baz'];
99
var objectOptions = [{
@@ -17,9 +17,10 @@ describe('<select selectize>', function() {
1717
text: 'third'
1818
}];
1919

20-
beforeEach(inject(function ($rootScope, $compile) {
20+
beforeEach(inject(function ($rootScope, $compile, $timeout) {
2121
scope = $rootScope.$new();
2222
compile = $compile;
23+
timeout = $timeout;
2324
}));
2425

2526
afterEach(function() {
@@ -79,10 +80,11 @@ describe('<select selectize>', function() {
7980

8081
describe('when the model is updated', function() {
8182
it('should update the selection', function() {
82-
testSelectedOption('foo');
83-
scope.selection = 'bar';
84-
scope.$apply();
85-
testSelectedOption('bar');
83+
testSelectedOption('foo');
84+
scope.selection = 'bar';
85+
scope.$apply();
86+
timeout.flush();
87+
testSelectedOption('bar');
8688
});
8789
});
8890

@@ -91,9 +93,40 @@ describe('<select selectize>', function() {
9193
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length);
9294
scope.options.push('qux');
9395
scope.$apply();
94-
selectize = $(selectElement)[0].selectize;
95-
selectize.refreshOptions();
96+
timeout.flush();
97+
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length);
98+
});
99+
100+
it('should not update the selection', function() {
101+
testSelectedOption('foo');
102+
scope.options.push('qux');
103+
scope.$apply();
104+
timeout.flush();
105+
testSelectedOption('foo');
106+
});
107+
});
108+
109+
describe('when both the model and the options are updated', function() {
110+
it('should have the same number of options in the dropdown menu as scope.options', function() {
96111
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length);
112+
113+
scope.selection = 'bar';
114+
scope.options.push('qux');
115+
scope.$apply();
116+
timeout.flush();
117+
118+
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length);
119+
});
120+
121+
it('should update the selection', function() {
122+
testSelectedOption('foo');
123+
124+
scope.selection = 'bar';
125+
scope.options.push('qux');
126+
scope.$apply();
127+
timeout.flush();
128+
129+
testSelectedOption('bar');
97130
});
98131
});
99132
});
@@ -134,6 +167,7 @@ describe('<select selectize>', function() {
134167
testSelectedOption(scope.selection);
135168
scope.selection = 'guid3';
136169
scope.$apply();
170+
timeout.flush();
137171
testSelectedOption(scope.selection);
138172
});
139173
});
@@ -146,9 +180,35 @@ describe('<select selectize>', function() {
146180
text: 'fourth'
147181
});
148182
scope.$apply();
149-
selectize = $(selectElement)[0].selectize;
150-
selectize.refreshOptions();
183+
timeout.flush();
184+
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length);
185+
});
186+
});
187+
188+
describe('when both the model and the options are updated', function() {
189+
it('should have the same number of options in the dropdown menu as scope.options', function() {
151190
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length);
191+
192+
scope.selection = 'bar';
193+
scope.options.push({
194+
value: 4,
195+
text: 'fourth'
196+
});
197+
scope.$apply();
198+
timeout.flush();
199+
200+
assert.strictEqual(selectize.$dropdown_content.children().length, scope.options.length);
201+
});
202+
203+
it('should update the selection', function() {
204+
testSelectedOption(scope.selection);
205+
206+
scope.selection = 'guid3';
207+
scope.options.push('qux');
208+
scope.$apply();
209+
timeout.flush();
210+
211+
testSelectedOption(scope.selection);
152212
});
153213
});
154214
});

0 commit comments

Comments
 (0)