added PaginatedCollectionView
test plan: 1. see the specs, there is no implementation yet Change-Id: I277300a507581cba4fe24682cad2d1ebbdf4147f Reviewed-on: https://gerrit.instructure.com/18361 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Joe Tanner <joe@instructure.com> QA-Review: Ryan Florence <ryanf@instructure.com>
This commit is contained in:
parent
cdf1d1a00e
commit
824e9d2c76
|
@ -24,7 +24,7 @@ define [
|
|||
capitalize = (string = '') ->
|
||||
string.charAt(0).toUpperCase() + string.substring(1).toLowerCase()
|
||||
|
||||
class @PaginatedCollection extends Backbone.Collection
|
||||
class PaginatedCollection extends Backbone.Collection
|
||||
|
||||
# Matches the name of each link: "next," "prev," "first," or "last."
|
||||
nameRegex: /rel="([a-z]+)/
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
define [
|
||||
'jquery'
|
||||
'underscore'
|
||||
'compiled/views/CollectionView'
|
||||
'jst/paginatedCollection'
|
||||
], ($, _, CollectionView, template) ->
|
||||
|
||||
##
|
||||
# General purpose lazy-load view. It must have a PaginatedCollection.
|
||||
#
|
||||
# TODO: We should replace all PaginatedView instances with this
|
||||
#
|
||||
# example:
|
||||
#
|
||||
# new PaginatedCollectionView
|
||||
# collection: somePaginatedCollection
|
||||
# itemView: SomeItemView
|
||||
|
||||
class PaginatedCollectionView extends CollectionView
|
||||
|
||||
defaults:
|
||||
|
||||
##
|
||||
# Distance to begin fetching the next page
|
||||
|
||||
buffer: 500
|
||||
|
||||
##
|
||||
# Container with observed scroll position, can be a jQuery element, raw
|
||||
# dom node, or selector
|
||||
|
||||
scrollContainer: window
|
||||
|
||||
##
|
||||
# Adds a loading indicator element
|
||||
|
||||
els: _.extend({}, CollectionView::els,
|
||||
'.loadingIndicator': '$loadingIndicator'
|
||||
)
|
||||
|
||||
@optionProperty 'scrollContainer'
|
||||
|
||||
template: template
|
||||
|
||||
##
|
||||
# Initializes the view
|
||||
|
||||
initialize: ->
|
||||
super
|
||||
@initScrollContainer()
|
||||
@attachScroll()
|
||||
|
||||
##
|
||||
# Extends parent to detach scroll container event
|
||||
#
|
||||
# @api private
|
||||
|
||||
attachCollection: ->
|
||||
super
|
||||
@collection.on 'fetched:last', @detachScroll
|
||||
@collection.on 'beforeFetch', @showLoadingIndicator
|
||||
@collection.on 'fetch', @hideLoadingIndicator
|
||||
|
||||
##
|
||||
# Sets instance properties regarding the scrollContainer
|
||||
#
|
||||
# @api private
|
||||
|
||||
initScrollContainer: ->
|
||||
@scrollContainer = $ @scrollContainer
|
||||
@heightContainer = if @scrollContainer[0] is window
|
||||
# window has no scrollHeight
|
||||
document.body
|
||||
else
|
||||
@scrollContainer[0]
|
||||
|
||||
##
|
||||
# Attaches scroll event to scrollContainer
|
||||
#
|
||||
# @api private
|
||||
|
||||
attachScroll: ->
|
||||
event = "scroll.pagination:#{@cid}, resize.pagination:#{@cid}"
|
||||
@scrollContainer.on event, @checkScroll
|
||||
|
||||
##
|
||||
# Removes the scoll event from scrollContainer
|
||||
#
|
||||
# @api private
|
||||
|
||||
detachScroll: =>
|
||||
@scrollContainer.off ".pagination:#{@cid}"
|
||||
|
||||
##
|
||||
# Determines if we need to fetch the collection's next page
|
||||
#
|
||||
# @api public
|
||||
|
||||
checkScroll: =>
|
||||
return if @fetchingPage
|
||||
distanceToBottom = @heightContainer.scrollHeight -
|
||||
@scrollContainer.scrollTop() -
|
||||
@scrollContainer.height()
|
||||
if distanceToBottom < @options.buffer
|
||||
@collection.fetch page: 'next'
|
||||
|
||||
##
|
||||
# Remove scroll event if view is removed
|
||||
#
|
||||
# @api public
|
||||
|
||||
remove: ->
|
||||
@detachScroll()
|
||||
super
|
||||
|
||||
|
||||
##
|
||||
# Hides the loading indicator after render
|
||||
#
|
||||
# @api private
|
||||
|
||||
afterRender: ->
|
||||
super
|
||||
@hideLoadingIndicator()
|
||||
|
||||
##
|
||||
# Hides the loading indicator
|
||||
#
|
||||
# @api private
|
||||
|
||||
hideLoadingIndicator: =>
|
||||
@$loadingIndicator.hide()
|
||||
|
||||
##
|
||||
# Shows the loading indicator
|
||||
#
|
||||
# @api private
|
||||
|
||||
showLoadingIndicator: =>
|
||||
@$loadingIndicator.show()
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{{#if collection.length}}
|
||||
<ul class="collectionViewItems"></ul>
|
||||
{{else}}
|
||||
<p>
|
||||
{{#t "no_items"}}No items.{{/t}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<div class="loadingIndicator"></div>
|
||||
|
|
@ -1,33 +1,8 @@
|
|||
define [
|
||||
'Backbone'
|
||||
'compiled/collections/PaginatedCollection'
|
||||
], (Backbone, PaginatedCollection) ->
|
||||
|
||||
# helper to get a fake page from the "server", gives you some fake model data
|
||||
# and the Link header, don't send it a page greater than 10 or less than 1
|
||||
getFakePage = (thisPage = 1) ->
|
||||
url = (page) -> "/api/v1/context/2/resource?page=#{page}&per_page=2"
|
||||
lastID = thisPage * 2
|
||||
urls =
|
||||
first: url 1
|
||||
last: url 10
|
||||
links = []
|
||||
if thisPage < 10
|
||||
urls.next = url thisPage + 1
|
||||
links.push '<' + urls.next + '>; rel="next"'
|
||||
if thisPage > 1
|
||||
urls.prev = url thisPage - 1
|
||||
links.push '<' + urls.prev + '>; rel="prev"'
|
||||
links.push '<' + urls.first + '>; rel="first"'
|
||||
links.push '<' + urls.last + '>; rel="last"'
|
||||
|
||||
urls: urls
|
||||
header: links.join ','
|
||||
data: [
|
||||
id: lastID - 1, foo: 'bar', baz: 'qux'
|
||||
,
|
||||
id: lastID, foo: 'bar', baz: 'qux'
|
||||
],
|
||||
'helpers/getFakePage'
|
||||
], (Backbone, PaginatedCollection, getFakePage) ->
|
||||
|
||||
module 'PaginatedCollection',
|
||||
setup: ->
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
define ->
|
||||
|
||||
# helper to get a fake page from the "server", gives you some fake model data
|
||||
# and the Link header, don't send it a page greater than 10 or less than 1
|
||||
getFakePage = (thisPage = 1) ->
|
||||
url = (page) -> "/api/v1/context/2/resource?page=#{page}&per_page=2"
|
||||
lastID = thisPage * 2
|
||||
urls =
|
||||
first: url 1
|
||||
last: url 10
|
||||
links = []
|
||||
if thisPage < 10
|
||||
urls.next = url thisPage + 1
|
||||
links.push '<' + urls.next + '>; rel="next"'
|
||||
if thisPage > 1
|
||||
urls.prev = url thisPage - 1
|
||||
links.push '<' + urls.prev + '>; rel="prev"'
|
||||
links.push '<' + urls.first + '>; rel="first"'
|
||||
links.push '<' + urls.last + '>; rel="last"'
|
||||
|
||||
urls: urls
|
||||
header: links.join ','
|
||||
data: [
|
||||
id: lastID - 1, foo: 'bar', baz: 'qux'
|
||||
,
|
||||
id: lastID, foo: 'bar', baz: 'qux'
|
||||
]
|
||||
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
define [
|
||||
'jquery'
|
||||
'compiled/collections/PaginatedCollection'
|
||||
'compiled/views/PaginatedCollectionView'
|
||||
'helpers/getFakePage'
|
||||
], ($, PaginatedCollection, PaginatedCollectionView, fakePage) ->
|
||||
|
||||
server = null
|
||||
collection = null
|
||||
view = null
|
||||
fixtures = $ '#fixtures'
|
||||
|
||||
createServer = ->
|
||||
server = sinon.fakeServer.create()
|
||||
server.sendPage = (page, url) ->
|
||||
@respond 'GET', url, [200, {
|
||||
'Content-Type': 'application/json'
|
||||
'Link': page.header
|
||||
}, JSON.stringify page.data]
|
||||
|
||||
class ItemView extends Backbone.View
|
||||
tagName: 'li'
|
||||
template: ({id}) -> id
|
||||
initialize: ->
|
||||
# make some scrolly happen
|
||||
@$el.css 'height', 100
|
||||
|
||||
class TestCollection extends PaginatedCollection
|
||||
url: '/test'
|
||||
|
||||
module 'PaginatedCollectionView',
|
||||
setup: ->
|
||||
fixtures.css height: 100, overflow: 'auto'
|
||||
createServer()
|
||||
collection = new TestCollection
|
||||
view = new PaginatedCollectionView
|
||||
collection: collection
|
||||
itemView: ItemView
|
||||
scrollContainer: fixtures
|
||||
view.$el.appendTo fixtures
|
||||
view.render()
|
||||
|
||||
teardown: ->
|
||||
server.restore()
|
||||
fixtures.attr 'style', ''
|
||||
view.remove()
|
||||
|
||||
assertItemRendered = (id) ->
|
||||
$match = view.$list.children().filter (i, el) -> el.innerHTML is id
|
||||
ok $match.length, 'item found'
|
||||
|
||||
scrollToBottom = ->
|
||||
fixtures[0].scrollTop = fixtures[0].scrollHeight
|
||||
ok fixtures[0].scrollTop > 0
|
||||
|
||||
test 'renders items', ->
|
||||
collection.add id: 1
|
||||
assertItemRendered '1'
|
||||
|
||||
test 'renders items on collection fetch and fetch next', ->
|
||||
collection.fetch()
|
||||
server.sendPage fakePage(), collection.url
|
||||
assertItemRendered '1'
|
||||
assertItemRendered '2'
|
||||
collection.fetch page: 'next'
|
||||
server.sendPage fakePage(2), collection.urls.next
|
||||
assertItemRendered '3'
|
||||
assertItemRendered '4'
|
||||
|
||||
test 'fetches the next page on scroll', ->
|
||||
collection.fetch()
|
||||
server.sendPage fakePage(), collection.url
|
||||
scrollToBottom()
|
||||
# scroll event isn't firing in the test :( manually calling checkScroll
|
||||
view.checkScroll()
|
||||
ok collection.fetchingNextPage, 'collection is fetching'
|
||||
server.sendPage fakePage(2), collection.urls.next
|
||||
assertItemRendered '3'
|
||||
assertItemRendered '4'
|
||||
|
||||
test 'stops fetching pages after the last page', ->
|
||||
# see later in the test why this exists
|
||||
fakeEvent = "foo.pagination:#{view.cid}"
|
||||
fixtures.on fakeEvent, ->
|
||||
ok false, 'this should never run'
|
||||
collection.fetch()
|
||||
server.sendPage fakePage(), collection.url
|
||||
for i in [2..10]
|
||||
collection.fetch page: 'next'
|
||||
server.sendPage fakePage(i), collection.urls.next
|
||||
assertItemRendered '1'
|
||||
assertItemRendered '20'
|
||||
# this is ghetto, but data('events') is no longer around and I can't get
|
||||
# the scroll events to trigger, but this works because the
|
||||
# ".pagination:#{view.cid}" events are all wipe out on last fetch, so the
|
||||
# assertion at the beginning of the test in the handler shouldn't fire
|
||||
fixtures.trigger fakeEvent
|
||||
|
Loading…
Reference in New Issue