Created
March 5, 2012 05:15
-
-
Save arches/1976732 to your computer and use it in GitHub Desktop.
Javascript for Ajax Pagination
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Arch; if (!Arch) Arch = {}; // set up custom namespace 'Arch' | |
Arch.AjaxPagination = function(paginationContainer, storiesContainer, filtersContainer) { | |
// Only html5 browsers can use ajax pagination. We don't want no stinkin' hashbang URLs. | |
if (!history.pushState) { | |
return; | |
} | |
this.$paginationContainer = $(paginationContainer); | |
this.$storiesContainer = $(storiesContainer); | |
this.$filtersContainer = $(filtersContainer); | |
this.attachPaginationHandlers(); | |
this.ajaxing = false; | |
this.initialLocation = this.getLocation(); | |
}; | |
Arch.AjaxPagination.prototype.attachPaginationHandlers = function() { | |
var me = this; | |
this.$paginationContainer.find("a").live('click', function() { | |
me.onClick(this); | |
return false; | |
}); | |
this.$filtersContainer.find("a").live('click', function() { | |
this.filter = true; | |
me.onClick(this); | |
return false; | |
}); | |
$(window).bind('popstate', function() { | |
me.onPopstate(); | |
}); | |
}; | |
Arch.AjaxPagination.prototype.currentPage = function() { | |
return parseInt(this.$paginationContainer.find("em").html()); | |
}; | |
// Wrapped window.location.href so we can spoof it in tests | |
Arch.AjaxPagination.prototype.getLocation = function() { | |
return window.location.href; | |
}; | |
Arch.AjaxPagination.prototype.onPopstate = function() { | |
// webkit fires a popstate event on page load, don't do anything in that case | |
if (this.initialLocation && this.getLocation() == this.initialLocation) { | |
this.initialLocation = null; | |
return; | |
} | |
// update the filters accordingly | |
var filterClass = "." + Arch.URI.getQueryParam("filter"); | |
this.$filtersContainer.find("a").removeClass("selected"); | |
if (filterClass == ".") { | |
this.$filtersContainer.find(".default").addClass("selected"); // select the default if no filter specified in URL | |
} | |
else { | |
this.$filtersContainer.find(filterClass).addClass("selected"); | |
} | |
// fake a click to get the right page from the server | |
var fauxLink = {href: this.getLocation(), backButton: true}; | |
this.onClick(fauxLink); | |
}; | |
Arch.AjaxPagination.prototype.onClick = function(link) { | |
if (this.ajaxing) return; | |
this.$paginationContainer.addClass("ajaxing"); | |
this.$storiesContainer.addClass("ajaxing"); | |
if (link.filter) { | |
this.$filtersContainer.find("a").removeClass("selected"); | |
$(link).addClass("selected"); | |
} | |
$.ajax({ | |
accept: "text/html", | |
contentType: "application/json", | |
context: this, | |
type: 'GET', | |
error: this.onPageLoadFailure, | |
success: function(data, textStatus, xhr) { | |
this.onPageLoadSuccess(data, link); | |
}, | |
url: link.href | |
}); | |
this.ajaxing = true; | |
}; | |
Arch.AjaxPagination.prototype.onPageLoadSuccess = function(data, link) { | |
this.ajaxing = false; | |
if (link && !link.backButton) history.pushState("", "", link.href); | |
data = JSON.parse(data || "{}"); | |
this.$storiesContainer.html(data.story_grid); | |
if (this.$paginationContainer.length > 0) { | |
var pc = $(data.pagination); | |
this.$paginationContainer.replaceWith(pc); | |
this.$paginationContainer = pc; | |
} | |
this.$paginationContainer.removeClass("ajaxing"); | |
this.$storiesContainer.removeClass("ajaxing"); | |
}; | |
Arch.AjaxPagination.prototype.onPageLoadFailure = function(data) { | |
this.ajaxing = false; | |
this.$paginationContainer.removeClass("ajaxing"); | |
this.$storiesContainer.removeClass("ajaxing"); | |
Arch.Flash.showError("Sorry, there was an error loading more stories."); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div id="story_grid"></div> | |
<div id="filters"> | |
<a class="our-picks selected default" href="/stories"></a> | |
<a class="popular" href="/stories?filter=popular"></a> | |
<a class="newest" href="/stories?filter=newest"></a> | |
</div> | |
<div id="page1"> | |
<div class="pagination"> | |
<span class="previous_page disabled">Previous</span> | |
<em>1</em> | |
<a rel="next" href="/stories?page=2">2</a> | |
<a href="/stories?page=3">3</a> | |
<a href="/stories?page=4">4</a> | |
<a href="/stories?page=5" class="link5">5</a> | |
<span class="gap">…</span> | |
<a href="/stories?page=4019">4019</a> | |
<a rel="next" class="next_page" href="/stories?page=2">Next</a> | |
</div> | |
</div> | |
<div id="page22"> | |
<div class="pagination"> | |
<a rel="prev" class="previous_page" href="/stories?page=21">Previous</a> | |
<a rel="start" href="/stories?page=1">1</a> | |
<span class="gap">…</span> | |
<a href="/stories?page=20">20</a> | |
<a rel="prev" href="/stories?page=21">21</a> | |
<em>22</em> | |
<a rel="next" href="/stories?page=23">23</a> | |
<a href="/stories?page=24">24</a> | |
<span class="gap">…</span> | |
<a href="/stories?page=4019">4019</a> | |
<a rel="next" class="next_page" href="/stories?page=23">Next</a> | |
</div> | |
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
describe("The ajax paginator", function() { | |
beforeEach(function() { | |
loadFixtures("scopes/topics/ajax_pagination.html"); | |
}); | |
it("knows what page it's on", function() { | |
var ap; | |
ap = new Arch.AjaxPagination($("#page1 .pagination")); | |
expect(ap.currentPage()).toEqual(1); | |
ap = new Arch.AjaxPagination($("#page22 .pagination")); | |
expect(ap.currentPage()).toEqual(22); | |
}); | |
it("adds an 'ajaxing' class during pagination", function() { | |
spyOn($, 'ajax'); | |
var ap = new Arch.AjaxPagination($("#page1 .pagination"), $("#story_grid")); | |
ap.onClick({href: "/stories"}); | |
expect($("#page1 .pagination")).toHaveClass("ajaxing"); | |
expect($("#story_grid")).toHaveClass("ajaxing"); | |
}); | |
it("removes the 'ajaxing' class when ajax completes", function() { | |
spyOn($, 'ajax'); | |
var ap = new Arch.AjaxPagination($("#page1 .pagination"), $("#story_grid")); | |
$("#page1 .pagination, #story_grid").addClass("ajaxing"); | |
ap.onPageLoadFailure(); | |
expect($("#page1 .pagination")).not.toHaveClass("ajaxing"); | |
expect($("#story_grid")).not.toHaveClass("ajaxing"); | |
$("#page1 .pagination, #story_grid").addClass("ajaxing"); | |
ap.onPageLoadSuccess("", {}); | |
expect($("#page1 .pagination")).not.toHaveClass("ajaxing"); | |
expect($("#story_grid")).not.toHaveClass("ajaxing"); | |
}); | |
}); | |
describe("Pagination links", function() { | |
beforeEach(function() { | |
loadFixtures("scopes/topics/ajax_pagination.html"); | |
}); | |
it("are intercepted", function() { | |
var ap = new Arch.AjaxPagination($("#page1 .pagination")); | |
spyOn($, 'ajax'); // don't need to actually hit the server | |
var e = $.Event('click'); | |
$("#page1 .next_page").trigger(e); | |
expect(e.result).toEqual(false); | |
}); | |
it("are ajaxed to the server", function() { | |
var ap = new Arch.AjaxPagination($("#page1 .pagination")); | |
spyOn($, 'ajax'); | |
$("#page1 .next_page").click(); | |
expect($.ajax).toHaveBeenCalled(); | |
expect($.ajax.argsForCall[0][0].url).toMatch(".*stories.page=2"); | |
}); | |
it("are blocked during ajax calls", function() { | |
var ap = new Arch.AjaxPagination($("#page1 .pagination")); | |
spyOn($, 'ajax'); // don't need to actually hit the server | |
$("#page1 .next_page").click(); | |
$("#page1 .link5").click(); | |
expect($.ajax.argsForCall.length).toEqual(1); | |
}); | |
it("are not blocked after ajax calls", function() { | |
var ap = new Arch.AjaxPagination($("#page1 .pagination")); | |
spyOn($, 'ajax'); // don't need to actually hit the server | |
// on success | |
$("#page1 .next_page").click(); | |
var link = JSON.stringify('<div class="pagination"><a class="link5" href="/stories?page=5">Next</a></div>'); | |
ap.onPageLoadSuccess('{"pagination": ' + link + '}'); | |
$("#page1 .link5").click(); | |
expect($.ajax.argsForCall.length).toEqual(2); | |
// on failure | |
ap.onPageLoadFailure(); | |
$("#page1 .link5").click(); | |
expect($.ajax.argsForCall.length).toEqual(3); | |
}); | |
}); | |
describe("A failed ajax call", function() { | |
it("shows an error", function() { | |
loadFixtures("scopes/topics/ajax_pagination.html"); | |
var ap = new Arch.AjaxPagination($("#page1 .pagination")); | |
spyOn(Arch.Flash, 'showError'); | |
ap.onPageLoadFailure(); | |
expect(Arch.Flash.showError).toHaveBeenCalledWith("Sorry, there was an error loading more stories."); | |
}); | |
//TODO: test that we reset any blocked elements (both functionally and stylistically) | |
}); | |
describe("A successful ajax call", function() { | |
var ap; | |
beforeEach(function() { | |
loadFixtures("scopes/topics/ajax_pagination.html"); | |
ap = new Arch.AjaxPagination($("#page1 .pagination"), $("#story_grid")); | |
spyOn(history, 'pushState'); // don't need to actually hit the server | |
}); | |
it("replaces the current page of thumbnails with the downloaded data", function() { | |
ap.onPageLoadSuccess('{"story_grid": "a new page"}'); | |
expect($("#story_grid").html()).toEqual("a new page"); | |
}); | |
it("replaces the pagination links with the downloaded data", function() { | |
var link = JSON.stringify('<div class="pagination"><a href="/stories?page=2">Next</a></div>'); | |
ap.onPageLoadSuccess('{"pagination": ' + link + '}'); | |
expect($("#page1 .pagination").html()).toEqual('<a href="/stories?page=2">Next</a>'); | |
// have to make sure it's repeatable | |
link = JSON.stringify('<div class="pagination"><a href="/stories?page=4">Next</a></div>'); | |
ap.onPageLoadSuccess('{"pagination": ' + link + '}'); | |
expect($("#page1 .pagination").html()).toEqual('<a href="/stories?page=4">Next</a>'); | |
}); | |
it("reattaches event handlers to the new pagination links", function() { | |
var link = JSON.stringify('<div class="pagination"><a href="/stories?page=6">Next</a></div>'); | |
ap.onPageLoadSuccess('{"pagination": ' + link + '}'); | |
spyOn($, 'ajax'); // don't need to actually hit the server | |
$("#page1 .pagination a").click(); | |
expect($.ajax).toHaveBeenCalled(); | |
expect($.ajax.argsForCall[0][0].url).toMatch(".*stories.page=6"); | |
}); | |
it("adds a history item", function() { | |
var link = JSON.stringify('<div class="pagination"><a href="/stories?page=6">Next</a></div>'); | |
ap.onPageLoadSuccess('{"pagination": ' + link + '}', {href: "/stories?page=2"}); | |
expect(history.pushState).toHaveBeenCalledWith("", "", "/stories?page=2"); | |
}); | |
}); | |
describe("Clicking the back button", function() { | |
var ap; | |
beforeEach(function() { | |
loadFixtures("scopes/topics/ajax_pagination.html"); | |
ap = new Arch.AjaxPagination($("#page22 .pagination"), $("#story_grid"), $("#filters")); | |
}); | |
it("updates the page based on the URL", function() { | |
ap.getLocation = function() { | |
return "/stories?page=8"; | |
}; | |
spyOn($, 'ajax'); | |
ap.initialLocation = null; | |
ap.onPopstate(); | |
expect($.ajax.argsForCall[0][0].url).toMatch(".*stories.page=8"); | |
}); | |
it("doesn't create a new history entry", function() { | |
spyOn($, 'ajax'); | |
spyOn(history, 'pushState'); | |
ap.onPopstate(); | |
ap.onPageLoadSuccess("{}", {backButton: true}); | |
expect(history.pushState).not.toHaveBeenCalled(); | |
}); | |
it("selects the filter in the query params", function() { | |
spyOn($, 'ajax'); | |
spyOn(Arch.URI, 'getQueryString').andReturn("filter=popular"); | |
$("#filters .our-picks, #filters .newest").addClass("selected"); | |
$("#filters .popular").removeClass("selected"); | |
ap.initialLocation = null; | |
ap.onPopstate(); | |
expect($("#filters .our-picks")).not.toHaveClass("selected"); | |
expect($("#filters .popular")).toHaveClass("selected"); | |
expect($("#filters .newest")).not.toHaveClass("selected"); | |
}); | |
it("selects the default filter if no filter is in the query params", function() { | |
spyOn($, 'ajax'); | |
$("#filters .popular, #filters .newest").addClass("selected"); | |
$("#filters .our-picks").removeClass("selected"); | |
ap.initialLocation = null; | |
ap.onPopstate(); | |
expect($("#filters .our-picks")).toHaveClass("selected"); | |
expect($("#filters .popular")).not.toHaveClass("selected"); | |
expect($("#filters .newest")).not.toHaveClass("selected"); | |
}); | |
}); | |
describe("Extraneous popstate events", function() { | |
it("don't do anything", function() { | |
// webkit has a tendency to fire popstate events on page load. | |
spyOn($, 'ajax'); | |
var ap = new Arch.AjaxPagination($("#page1 .pagination")); | |
ap.initialLocation = "/stories?page=8"; | |
ap.getLocation = function() { | |
return "/stories?page=8"; | |
}; | |
ap.onPopstate(); | |
expect($.ajax).not.toHaveBeenCalled(); | |
}); | |
}); | |
describe("Clicking a filter link", function() { | |
var ap; | |
beforeEach(function() { | |
loadFixtures("scopes/topics/ajax_pagination.html"); | |
ap = new Arch.AjaxPagination($("#page1 .pagination"), $("#story_grid"), $("#filters")); | |
}); | |
it("ajax loads a new page", function() { | |
spyOn($, 'ajax'); | |
$("#filters .newest").click(); | |
expect($.ajax).toHaveBeenCalled(); | |
expect($.ajax.argsForCall[0][0].url).toMatch(".*stories.filter=newest"); | |
}); | |
it("selects that filter", function() { | |
spyOn($, 'ajax'); | |
$("#filters .newest").click(); | |
expect($("#filters .newest")).toHaveClass("selected"); | |
expect($("#filters .our-picks")).not.toHaveClass("selected"); | |
}); | |
}); | |
describe("When no pagination exists", function() { | |
var ap; | |
beforeEach(function() { | |
loadFixtures("scopes/topics/ajax_pagination.html"); | |
ap = new Arch.AjaxPagination($("#page1 .pagination_EXCEPT_NOT"), $("#story_grid"), $("#filters")); | |
}); | |
it("don't try to update it", function() { | |
spyOn(ap.$paginationContainer, 'html'); | |
ap.onPageLoadSuccess("{}", {}); | |
expect(ap.$paginationContainer.html).not.toHaveBeenCalled(); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment