Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save arches/1976732 to your computer and use it in GitHub Desktop.
Save arches/1976732 to your computer and use it in GitHub Desktop.
Javascript for Ajax Pagination
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.");
};
<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">&hellip;</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">&hellip;</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">&hellip;</span>
<a href="/stories?page=4019">4019</a>
<a rel="next" class="next_page" href="/stories?page=23">Next</a>
</div>
</div>
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