Skip to content

Instantly share code, notes, and snippets.

@kevinansfield
Created June 20, 2014 15:46
Show Gist options
  • Select an option

  • Save kevinansfield/bf5ece6c04253e304fb9 to your computer and use it in GitHub Desktop.

Select an option

Save kevinansfield/bf5ece6c04253e304fb9 to your computer and use it in GitHub Desktop.
TokenInputComponent example (handles configurable property for token/result display text, calls out to external search method for auto-complete results)
<p class="token-input__token__text">{{text}}</p>
<span class="token-input__token__delete" {{action deleteToken on=mouseDown}}>x</span>
# adapted from http://loopj.com/jquery-tokeninput/
# rewritten to work directly with ember objects and offload searching to
# external methods
KEY =
BACKSPACE: 8
TAB: 9
ENTER: 13
ESCAPE: 27
SPACE: 32
PAGE_UP: 33
PAGE_DOWN: 34
END: 35
HOME: 36
LEFT: 37
UP: 38
RIGHT: 39
DOWN: 40
NUMPAD_ENTER: 108
COMMA: 188
App.TokenInputComponent = Ember.Component.extend
classNames: ['token-input']
### external attributes/bindings ###
selected: null
searchMethod: null
searchResults: null
tokenProperty: 'name'
resultProperty: 'name'
hintText: 'Type to search...'
noResultsText: 'Sorry, no matches were found'
searchingText: 'Searching...'
searchDelay: 150
tokenLimit: null
preventDuplicates: true
### internal attributes ###
# keep internal list of tokens/results to decorate with additional properties
tokens: Ember.A()
results: Ember.A()
searchTerm: null
isSearching: false
### actions ###
actions:
addToken: (result) ->
@get('selected').pushObject result.get('content')
@send 'reset'
@send 'focus'
addSelectedResult: ->
unless Ember.isEmpty @get('selectedResult')
@send 'addToken', @get('selectedResult')
deleteToken: (token) ->
@get('selected').removeObject token.get('content')
@send 'reset'
deleteLastToken: ->
unless Ember.isEmpty(@get 'tokens')
@send 'deleteToken', @get('tokens')[@get('tokens.length')-1]
selectResult: (result) ->
if result
@get('results').forEach (result) ->
result.set 'isSelected', false
result.set 'isSelected', true
selectNextResult: ->
results = @get 'results'
selectedResult = @get 'selectedResult'
unless Ember.isEmpty results
currentIndex = results.indexOf(@get('selectedResult'))
if currentIndex+1 < results.get('length')
newResult = results[currentIndex + 1]
@send 'selectResult', newResult
selectPreviousResult: ->
results = @get 'results'
selectedResult = @get 'selectedResult'
unless Ember.isEmpty results
currentIndex = results.indexOf(@get('selectedResult'))
if currentIndex-1 >= 0
newResult = results[currentIndex - 1]
@send 'selectResult', newResult
focus: ->
@$('input').focus()
reset: ->
@set 'searchTerm', null
@set 'results', Ember.A()
### properties ###
message: (->
return @get 'searchingText' if @get 'isSearching'
if @get 'hasFocus'
return @get 'hintText' if Ember.isEmpty @get('searchTerm')
return @get 'noResultsText' if !@get 'results.length'
null
).property('isSearching', 'results.length', 'searchTerm', 'hasFocus')
showDropdown: (->
@get('hasFocus') && (@get('message') || !Ember.isEmpty(@get('results')))
).property('hasFocus', 'message', 'results')
selectedResult: (->
@get('results').filterBy('isSelected')[0]
).property('[email protected]')
### observers ###
updateTokensFromSelected: (->
tokens = @get('tokens')
selected = @get('selected')
# remove tokens without matches
tokens = tokens.reject (token) ->
!selected.contains token.get('content')
# add new matches
contents = tokens.mapBy 'content'
selected.forEach (selected) ->
unless contents.contains selected
token = new Ember.Object
token.set 'content', selected
tokens.pushObject token
@set 'tokens', tokens
).observes('selected.@each').on('init')
searchTermChanged: (->
@set 'results', Ember.A()
if !Ember.isEmpty @get('searchTerm')
@set 'isSearching', true
Ember.run.debounce @, @getSearchResults, @get('searchDelay')
else
@set 'isSearching', false
).observes('searchTerm')
getSearchResults: ->
@set 'isSearching', true
@sendAction 'searchMethod', @get('searchTerm')
updateResultsFromSearchResults: (->
results = Ember.A()
searchResults = @get 'searchResults'
if @get 'preventDuplicates'
selected = @get 'selected'
searchResults = searchResults.reject (searchResult) ->
selected.contains searchResult
searchResults.forEach (searchResult, index) ->
result = new Ember.Object
result.set 'content', searchResult
result.set 'isSelected', true if index == 0
results.pushObject result
@set 'results', results
@set 'isSearching', false
).observes('searchResults.@each', 'selected.@each', 'preventDuplicates')
App.TokenInputInputComponent = Ember.TextField.extend
classNames: ['token-input__new-token__field']
keyDown: (event) ->
parentView = @get('parentView')
switch event.keyCode
when KEY.UP
event.preventDefault()
parentView.send 'selectPreviousResult'
when KEY.DOWN
event.preventDefault()
parentView.send 'selectNextResult'
when KEY.TAB, KEY.ENTER, KEY.NUMPAD_ENTER, KEY.COMMA
event.preventDefault()
parentView.send 'addSelectedResult'
when KEY.BACKSPACE
if Ember.isEmpty(@get 'value')
event.preventDefault()
parentView.send 'deleteLastToken'
when KEY.ESCAPE
event.preventDefault()
@$().blur()
focusIn: (event) ->
@get('parentView').set 'hasFocus', true
focusOut: (event) ->
@get('parentView').send 'reset'
@get('parentView').set 'hasFocus', false
App.TokenInputTokenComponent = Ember.Component.extend
tagName: 'li'
classNameBindings: [
':token-input__token',
'token.isSelected:token-input__token--selected'
]
token: null
actions:
deleteToken: ->
@get('parentView').send 'deleteToken', @get('token')
# we need to dynamicaly define the property as we don't know the dependent
# key until runtime
textProperty: (->
tokenProperty = @get('parentView.tokenProperty')
Ember.defineProperty @, 'text', Ember.computed(->
if content = @get 'token.content'
content.get tokenProperty
).property("token.content.#{tokenProperty}")
).observes('parentView.tokenProperty').on('init')
App.TokenInputResultComponent = Ember.Component.extend
tagName: 'li'
classNameBindings: [
':token-input__dropdown__result',
'result.isSelected:token-input__dropdown__result--selected',
]
result: null
# using mouseDown/mouseUp here as the focusOut event on the input fires before
# 'click' and clears our list preventing our own events from firing
mouseDown: (event) ->
event.preventDefault()
mouseUp: (event) ->
event.preventDefault()
@get('parentView').send 'addToken', @get('result')
mouseEnter: (event) ->
@get('parentView').send 'selectResult', @get('result')
# we need to dynamicaly define the property as we don't know the dependent
# key until runtime
textProperty: (->
resultProperty = @get('parentView.resultProperty')
Ember.defineProperty @, 'text', Ember.computed(->
if text = @get('result.content').get resultProperty
openingTag = '<em class="token-input__dropdown__result__match">'
closingTag = '</em>'
searchTerm = @get('parentView.searchTerm')
regex = new RegExp "(#{searchTerm})", 'gi'
text.replace regex, "#{openingTag}$1#{closingTag}"
).property("result.content.#{resultProperty}")
).observes('parentView.resultProperty').on('init')
<ul class="token-input__token-list" {{action 'focus' on='click'}}>
{{#each tokens}}
{{token-input-token token=this}}
{{/each}}
<li class="token-input__new-token">
{{token-input-input value=searchTerm}}
</li>
</ul>
{{#if showDropdown}}
<div class="token-input__dropdown">
{{#if message}}
<p class="token-input__dropdown__message">{{message}}</p>
{{/if}}
{{#if results}}
<ul class="token-input__dropdown__result-list">
{{#each results}}
{{token-input-result result=this}}
{{/each}}
</ul>
{{/if}}
</div>
{{/if}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment