Skip to content

Instantly share code, notes, and snippets.

@huafu
Last active December 25, 2015 13:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save huafu/6984862 to your computer and use it in GitHub Desktop.
Save huafu/6984862 to your computer and use it in GitHub Desktop.
Mixins and view to handle searchable array controller and infinite scroll list view
App = require 'app'
###*
Controller ExampleItemsController
@class ExampleItemsController
@namespace App
@extends Ember.ArrayController
###
module.exports = App.ExampleItemsController = Ember.ArrayController.extend App.Mixins.PagedSearchableArrayController,
###*
Holds our content
@property content
@type Array
###
content: []
###*
@inheritDoc
###
searchFilterKeys: ['name']
###*
Initialize the search filters
@method initializeSearch
###
initializeSearch: (() ->
@set 'searchFilters.name', ''
).on('searchInit')
###*
@inheritDoc
###
searchGrabResults: (filters) ->
@get('store').findQuery 'example', filters
{{input type="text" value="controller.searchFilters.name"}}
<div class="container example-list-container">
{{collection App.InfiniteScrollListView contentBinding="controller"
height=500 rowHeight=50}}
{{#if controller.isLoadingMoreResults}}
<div class="loading-more-examples">
Loading more examples...
</div>
{{/if}}
</div>
App = require 'app'
###*
View InfiniteScrollListView
@class InfiniteScrollListView
@namespace App
@extends Ember.VirtualListView
###
module.exports = App.InfiniteScrollListView = Ember.VirtualListView.extend
###*
Load more results when being that pixels close to the bottom
@property loadMoreAtPixelsFromBottom
@type Number
###
loadMoreAtPixelsFromBottom: 20
###*
Our initializer
@method _intiailizeInfiniteScroll
###
_intiailizeInfiniteScroll: (() ->
# be sure our content is of good type
Ember.assert 'The content of the infinite scroll list view must be using App.Mixins.PagedSearchableArrayController',
App.Mixins.PagedSearchableArrayController.detect @get 'content'
).on('init')
###*
Listen for scroll Y changes to be able to trigger teh load more
@method handleScrollYChanges
@param {Number} y
###
handleScrollYChanges: ((y) ->
maxScrollTop = @get('maxScrollTop')
if maxScrollTop and y > 0 and y >= maxScrollTop - @get('loadMoreAtPixelsFromBottom')
@get('content').loadNextPage()
).on('scrollYChanged')
App = require 'app'
###*
Mxin PagedSearchableArrayController
@class PagedSearchableArrayController
@namespace App.Mixins
###
module.exports = App.Mixins.PagedSearchableArrayController = Ember.Mixin.create App.Mixins.SearchableArrayController,
###*
@inheritDoc
###
searchFilterKeys: ['limit']
###*
@inheritDoc
###
searchFilterAppendKeys: ['offset']
###*
Default limit
@property searchDefaultLimit
@type Number
###
searchDefaultLimit: 20
###*
Is there more pages?
@property searchHasMoreResults
@type Boolean
###
searchHasMoreResults: yes
###*
Initialize our mixin
@method _initializePagedSearch
###
_initializePagedSearch: (() ->
if (sf = @get 'searchFilters')
sf.set 'offset', 0
sf.set 'limit', @get 'searchDefaultLimit'
@set 'searchHasMoreResults', yes
).on('searchInit')
###*
@inheritDoc
###
handleSearchFiltersChange: (() ->
changed = @get 'changedSearchFilterKeys'
nonAppendedKeys = @get 'searchFilterKeys'
for key in changed when key in nonAppendedKeys
@set 'searchFilters.offset', 0
break
).on('searchFiltersDidChange')
###*
Load the next page with the same serach filters
@method loadNextPage
###
loadNextPage: () ->
# get out if we are already running a search or if we don't have more results
return if @get('isSearching') or not @get('searchHasMoreResults')
@incrementProperty 'searchFilters.offset', @get 'searchFilters.limit'
# trigger the search directly, no need to wait
@checkFiltersAndSearch()
###*
Called when we have new results
@method gotNewResults
@param {Ember.Enumerable} parsedResults
###
handleGotNewResults: ((parsedResults) ->
# we update the hasMoreResults
@set 'searchHasMoreResults', Boolean(parsedResults.get('length') >= @get 'searchFilters.limit')
).on('gotNewResults')
###*
Is loading more results
@property isLoadingMoreResults
@type Boolean
###
isLoadingMoreResults: (() ->
Boolean(@get('isSearching') and @get('runningSearchWillAppend'))
).property('isSearching', 'runningSearchWillAppend')
App = require 'app'
###*
Mxin SearchableArrayController
@class SearchableArrayController
@namespace App.Mixins
###
module.exports = App.Mixins.SearchableArrayController = Ember.Mixin.create Ember.Evented,
###*
@inheritDoc
###
concatenatedProperties: ['searchFilterKeys', 'searchFilterAppendKeys']
###*
Search on initialization?
@property searchOnInit
@type Boolean
###
searchOnInit: yes
###*
Filters
@property searchFilters
@type Ember.Object
###
searchFilters: null
###*
Search delay (the milliseconds to wait for other filter changes before running the search
@property searchDelay
@type Number
###
searchDelay: 500
###*
Our filter keys
@property searchFilterKeys
@type Array<String>
###
searchFilterKeys: Ember.required Array
###*
Search filter append keys
@property searchFilterAppendKeys
@type Array<String>
###
searchFilterAppendKeys: []
###*
Last search filters cache key
@property lastSearchFiltersCacheKey
@type String
###
lastSearchFiltersCacheKey: null
###*
Last search filters values
@property lastSearchFilterValues
@type Object
###
lastSearchFilterValues: null
###*
Function which will lookup results
Must return a promise which should resolve to the results
@property searchGrabResults
@type Function
###
searchGrabResults: Ember.required Function
###*
Are we running a search?
@property isSearching
@type Boolean
###
isSearching: no
###*
Running search will append or update results?
@property runningSearchWillAppend
@type Boolean
###
runningSearchWillAppend: no
###*
Running search cache key
@property runningSearchCacheKey
@type String
###
runningSearchCacheKey: null
###*
Serialized search cache key
@property serializedSearchCacheKey
@type String
###
serializedSearchCacheKey: (() ->
props = @get 'allSearchFilterKeys'
JSON.stringify @get('searchFilters').getProperties props
).property().volatile().readOnly()
###*
All search filter keys
@property allSearchFilterKeys
@type Array<String>
@private
###
allSearchFilterKeys: (() ->
props = []
props.addObjects @get 'searchFilterKeys'
props.addObjects @get 'searchFilterAppendKeys'
props.uniq()
).property('searchFilterKeys.@each', 'searchFilterAppendKeys.@each').readOnly()
###*
Returns the keys of all search filters which have changed
@property changedSearchFilterKeys
@type Array<String>
###
changedSearchFilterKeys: (() ->
changed = []
all = @get 'allSearchFilterKeys'
last = @get 'lastSearchFilterValues'
if last
all.forEach (key) =>
if @get("lastSearchFilterValues.#{key}") isnt @get "searchFilters.#{key}"
changed.push key
else
changed = all
changed
).property().volatile().readOnly()
###*
Called on initialization
@method _initializeSearch
###
_initializeSearch: (() ->
@set 'searchFilters', Ember.Object.create() unless @get 'searchFilters'
@get('searchFilterKeys').forEach (key) =>
@addObserver "searchFilters.#{key}", @, '_searchFiltersDidChange'
@get('searchFilterAppendKeys').forEach (key) =>
@addObserver "searchFilters.key", @, '_searchFiltersDidChange'
@trigger 'searchInit'
@_searchFiltersDidChange() if @get 'searchOnInit'
).on('init')
###*
Called on destroy
@method _destroySearch
###
_destroySearch: (() ->
@get('searchFilterAppendKeys').forEach (key) =>
@removeObserver "searchFilters.key", @, '_searchFiltersDidChange'
@get('searchFilterKeys').forEach (key) =>
@removeObserver "searchFilters.#{key}", @, '_searchFiltersDidChange'
).on('willDestroy')
###*
Called when one of the search filter changed
@method searchFiltersDidChange
@private
###
_searchFiltersDidChange: () ->
Ember.run.debounce @, 'checkFiltersAndSearch', @get 'searchDelay'
###*
Check the filters for changes and run the search if necessary
@method checkFiltersAndSearch
###
checkFiltersAndSearch: () ->
# get out if we're already running a search
return if @get 'isSearching'
# get the old cache key and generate a new one
oldCacheKey = @get 'lastSearchFiltersCacheKey'
newCacheKey = @get 'serializedSearchCacheKey'
if oldCacheKey is newCacheKey
# the cache key is the same, no parameter change => get out
no
else
# we trigger a search filter change, so that any dependent stuff
# in the search filters can be changed
@trigger 'searchFiltersDidChange'
# we re-generate the cache key in case any parameter has been changed in the event handler
newCacheKey = @get 'serializedSearchCacheKey'
if oldCacheKey is newCacheKey
# again, if it's the same as the previous search, let's get out
no
else
# the parameters did change, we need to run the search
@set 'isSearching', yes
# get the list of changed parameters
changed = @get 'changedSearchFilterKeys'
appendKeys = @get 'searchFilterAppendKeys'
replaceKeys = @get 'searchFilterKeys'
# we gonna check if the search parameters which changed would just append results
# or if it needs to replace the results
append = no
replace = no
for key in changed
append = yes if not append and key in appendKeys
replace = yes if not replace and key in replaceKeys
# at the end we will be in append mode only if replace is false and append is true
append = not replace and append
# unserializing the cache key will give us a simple object with search filters
# but all would be a copy so any change doesn't matter
filters = JSON.parse newCacheKey
# we update the running search paraemters
@set 'runningSearchWillAppend', append
@set 'runningSearchCacheKey', newCacheKey
@searchGrabResults(filters).then((results) =>
# once we got the result we can set the last search
@set 'lastSearchFiltersCacheKey', newCacheKey
@set 'lastSearchFilterValues', JSON.parse newCacheKey
# we call the results parser
results = @parseResults results
# we trigger our event
@trigger 'gotNewResults', results
# we append or replace the results
if append
@addObjects results
else
@set 'content', results
# we clear all data set for the running search
@set 'runningSearchCacheKey', null
@set 'runningSearchWillAppend', null
@set 'isSearching', no
# to do the search again in case the filters changed in between
@_searchFiltersDidChange()
).fail (reason) =>
#TODO: log the error and try again once or twice
@set 'runningSearchCacheKey', null
@set 'runningSearchWillAppend', null
@set 'isSearching', no
# we return OK since the search will be running
yes
###*
Parse the results
@method parseResults
@param {Ember.Enumerable} results
@returns Ember.Enumerable
###
parseResults: (results) ->
Ember.A results
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment