Skip to content

Commit

Permalink
Update change detection to not rely on chosen render state, but inste…
Browse files Browse the repository at this point in the history
…ad track select value updates
  • Loading branch information
VanTanev committed Sep 27, 2018
1 parent 44a2825 commit 2c07c05
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 53 deletions.
55 changes: 28 additions & 27 deletions dist/angular-chosen.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
});

chosenModule.directive('chosen', [
'chosen', '$timeout', function(config, $timeout) {
'chosen', '$timeout', '$parse', function(config, $timeout, $parse) {
var CHOSEN_OPTION_WHITELIST, NG_OPTIONS_REGEXP, isEmpty, snakeCase;
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]+?))?$/;
CHOSEN_OPTION_WHITELIST = ['persistentCreateOption', 'createOptionText', 'createOption', 'skipNoResults', 'noResultsText', 'allowSingleDeselect', 'disableSearchThreshold', 'disableSearch', 'enableSplitWordSearch', 'inheritSelectClasses', 'maxSelectedOptions', 'placeholderTextMultiple', 'placeholderTextSingle', 'searchContains', 'singleBackstrokeDelete', 'displayDisabledOptions', 'displaySelectedOptions', 'width', 'includeGroupLabelInSelected', 'maxShownResults'];
Expand All @@ -50,13 +50,18 @@
};
return {
restrict: 'A',
require: '?ngModel',
require: ['select', '?ngModel'],
priority: 1,
link: function(scope, element, attr, ngModel) {
var chosen, directiveOptions, empty, initOrUpdate, match, options, origRender, startLoading, stopLoading, updateMessage, valuesExpr, viewWatch;
link: function(scope, element, attr, ctrls) {
var $render, chosen, directiveOptions, empty, init, match, ngModel, ngSelect, options, startLoading, stopLoading, trackBy, updateMessage, valuesExpr, viewWatch;
scope.disabledValuesHistory = scope.disabledValuesHistory ? scope.disabledValuesHistory : [];
element = $(element);
element.addClass('localytics-chosen');
ngSelect = ctrls[0];
ngModel = ctrls[1];
match = attr.ngOptions && attr.ngOptions.match(NG_OPTIONS_REGEXP);
valuesExpr = match && $parse(match[7]);
trackBy = match && match[8];
directiveOptions = scope.$eval(attr.chosen) || {};
options = angular.copy(config);
angular.extend(options, directiveOptions);
Expand Down Expand Up @@ -84,22 +89,10 @@
};
chosen = null;
empty = false;
initOrUpdate = function() {
var defaultText, dropListDom;
if (chosen) {
dropListDom = $(element.next('.chosen-with-drop'));
if (dropListDom && dropListDom.length > 0) {
return;
}
return element.trigger('chosen:updated');
} else {
scope.$evalAsync(function() {
chosen = element.chosen(options).data('chosen');
});
if (angular.isObject(chosen)) {
return defaultText = chosen.default_text;
}
}
init = function() {
return scope.$evalAsync(function() {
return chosen = element.chosen(options).data('chosen');
});
};
updateMessage = function() {
if (chosen && empty) {
Expand All @@ -109,11 +102,23 @@
}
return element.trigger('chosen:updated');
};
init();
if (ngModel) {
origRender = ngModel.$render;
$render = ngModel.$render;
ngModel.$render = function() {
origRender();
return initOrUpdate();
var isNotPrimitive, nextValue, previousValue, valueChanged;
try {
previousValue = ngSelect.readValue();
} catch (error) {}
$render();
try {
nextValue = ngSelect.readValue();
} catch (error) {}
isNotPrimitive = trackBy || attr.multiple;
valueChanged = isNotPrimitive ? !angular.equals(previousValue, nextValue) : previousValue !== nextValue;
if (valueChanged) {
return element.trigger('chosen:updated');
}
};
element.on('chosen:hiding_dropdown', function() {
return scope.$apply(function() {
Expand All @@ -126,15 +131,11 @@
};
scope.$watch(viewWatch, ngModel.$render, true);
}
} else {
initOrUpdate();
}
attr.$observe('disabled', function() {
return element.trigger('chosen:updated');
});
if (attr.ngOptions && ngModel) {
match = attr.ngOptions.match(NG_OPTIONS_REGEXP);
valuesExpr = match[7];
scope.$watchCollection(valuesExpr, function(newVal, oldVal) {
var timer;
return timer = $timeout(function() {
Expand Down
2 changes: 1 addition & 1 deletion dist/angular-chosen.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 29 additions & 25 deletions src/chosen.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ chosenModule.provider 'chosen', ->
options
}

chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) ->
chosenModule.directive 'chosen', ['chosen', '$timeout', '$parse', (config, $timeout, $parse) ->
# coffeelint: disable=max_line_length
# This is stolen from Angular...
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]+?))?$/
Expand Down Expand Up @@ -51,13 +51,20 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) ->
true

restrict: 'A'
require: '?ngModel'
require: ['select', '?ngModel']
priority: 1
link: (scope, element, attr, ngModel) ->
link: (scope, element, attr, ctrls) ->
scope.disabledValuesHistory = if scope.disabledValuesHistory then scope.disabledValuesHistory else []
element = $ element # Use real JQuery if it wasn't loaded before Angular.
element.addClass('localytics-chosen')

ngSelect = ctrls[0]
ngModel = ctrls[1]

match = attr.ngOptions && attr.ngOptions.match(NG_OPTIONS_REGEXP)
valuesExpr = match && $parse(match[7])
trackBy = match && match[8]

# Take a hash of options from the chosen directive
directiveOptions = scope.$eval(attr.chosen) or {}

Expand Down Expand Up @@ -91,19 +98,9 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) ->
chosen = null
empty = false

initOrUpdate = ->
if chosen
# Fix #56 Don't scroll to top when selecting multiple items with ctrl
dropListDom = $(element.next('.chosen-with-drop')) #uses jQuery instead of Angular.
if dropListDom && dropListDom.length > 0
return
element.trigger('chosen:updated')
else
scope.$evalAsync ->
chosen = element.chosen(options).data('chosen')
return
if angular.isObject(chosen)
defaultText = chosen.default_text
init = ->
scope.$evalAsync ->
chosen = element.chosen(options).data('chosen')

# Use Chosen's placeholder or no results found text depending on whether there are options available
updateMessage = ->
Expand All @@ -113,12 +110,25 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) ->
element.removeAttr('data-placeholder')
element.trigger('chosen:updated')

init()

# Watch the underlying ngModel for updates and trigger an update when they occur.
if ngModel
origRender = ngModel.$render
$render = ngModel.$render
ngModel.$render = ->
origRender()
initOrUpdate()
# We need to try and detect if the select value was changed from outside of chosen
try previousValue = ngSelect.readValue()
$render()
try nextValue = ngSelect.readValue()

isNotPrimitive = trackBy || attr.multiple
valueChanged = if isNotPrimitive
then !angular.equals(previousValue, nextValue)
else previousValue != nextValue

# If it was changed, then we trigger a chosen re-render
if (valueChanged)
element.trigger('chosen:updated')

element.on 'chosen:hiding_dropdown', ->
scope.$apply -> ngModel.$setTouched()
Expand All @@ -128,19 +138,13 @@ chosenModule.directive 'chosen', ['chosen', '$timeout', (config, $timeout) ->
if attr.multiple
viewWatch = -> ngModel.$viewValue
scope.$watch viewWatch, ngModel.$render, true
# If we're not using ngModel (and therefore also not using ngOptions, which requires ngModel),
# just initialize chosen immediately since there's no need to wait for ngOptions to render first
else initOrUpdate()

# Watch the disabled attribute (could be set by ngDisabled)
attr.$observe 'disabled', -> element.trigger('chosen:updated')

# Watch the collection in ngOptions and update chosen when it changes. This works with promises!
# ngOptions doesn't do anything unless there is an ngModel, so neither do we.
if attr.ngOptions and ngModel
match = attr.ngOptions.match(NG_OPTIONS_REGEXP)
valuesExpr = match[7]

scope.$watchCollection valuesExpr, (newVal, oldVal) ->
# Defer execution until DOM is loaded
timer = $timeout(->
Expand Down

0 comments on commit 2c07c05

Please sign in to comment.