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:
Ryan Florence 2013-03-06 15:28:35 -07:00
parent cdf1d1a00e
commit 824e9d2c76
6 changed files with 281 additions and 28 deletions

View File

@ -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]+)/

View File

@ -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()

View File

@ -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>

View File

@ -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: ->

View File

@ -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'
]

View File

@ -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