Skip to content

Commit

Permalink
Merge pull request #254 from VanTanev/fix-not-updated-when-data-chang…
Browse files Browse the repository at this point in the history
…e-while-open

Update change detection to not rely on chosen render state, but instead track select value updates
  • Loading branch information
leocaseiro authored Sep 27, 2018
2 parents 44a2825 + 2c07c05 commit ad1c3a6
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 ad1c3a6

Please sign in to comment.