angular-chosen
Version:
Angular bindings for https://github.com/harvesthq/chosen
111 lines (91 loc) • 4.02 kB
text/coffeescript
module.exports = angular.module('angular-chosen', [])
angular.module('angular-chosen').directive 'chosen', ['$timeout', ($timeout) ->
# This is stolen from Angular...
NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+(.*?)(?:\s+track\s+by\s+(.*?))?$/
# Whitelist of options that will be parsed from the element's attributes and passed into Chosen
CHOSEN_OPTION_WHITELIST = [
'noResultsText'
'allowSingleDeselect'
'disableSearchThreshold'
'disableSearch'
'enableSplitWordSearch'
'inheritSelectClasses'
'maxSelectedOptions'
'placeholderTextMultiple'
'placeholderTextSingle'
'searchContains'
'singleBackstrokeDelete'
'displayDisabledOptions'
'displaySelectedOptions'
'width'
]
snakeCase = (input) -> input.replace /[A-Z]/g, ($1) -> "_#{$1.toLowerCase()}"
isEmpty = (value) ->
if angular.isArray(value)
return value.length is 0
else if angular.isObject(value)
return false for key of value when value.hasOwnProperty(key)
true
restrict: 'A'
require: '?ngModel'
terminal: true
link: (scope, element, attr, ngModel) ->
element.addClass('angular-chosen')
# Take a hash of options from the chosen directive
options = scope.$eval(attr.chosen) or {}
# Options defined as attributes take precedence
angular.forEach attr, (value, key) ->
options[snakeCase(key)] = scope.$eval(value) if key in CHOSEN_OPTION_WHITELIST
startLoading = -> element.addClass('loading').attr('disabled', true).trigger('chosen:updated')
stopLoading = -> element.removeClass('loading').attr('disabled', false).trigger('chosen:updated')
chosen = null
defaultText = null
empty = false
initOrUpdate = ->
if chosen
element.trigger('chosen:updated')
else
chosen = element.chosen(options).data('chosen')
if angular.isObject(chosen)
defaultText = chosen.default_text
# Use Chosen's placeholder or no results found text depending on whether there are options available
removeEmptyMessage = ->
empty = false
element.attr('data-placeholder', defaultText)
disableWithMessage = ->
empty = true
element.attr('data-placeholder', chosen.results_none_found).attr('disabled', true).trigger('chosen:updated')
# Watch the underlying ngModel for updates and trigger an update when they occur.
if ngModel
origRender = ngModel.$render
ngModel.$render = ->
origRender()
initOrUpdate()
# This is basically taken from angular ngOptions source. ngModel watches reference, not value,
# so when values are added or removed from array ngModels, $render won't be fired.
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(->
if angular.isUndefined(newVal)
startLoading()
else
removeEmptyMessage() if empty
stopLoading()
disableWithMessage() if isEmpty(newVal)
)
scope.$on '$destroy', (event) ->
$timeout.cancel timer if timer?
]