-
-
Save zspencer/2979078 to your computer and use it in GitHub Desktop.
A step by step refactoring where I separate a jquery plugin into pieces with reduced responsibilities.
This file contains hidden or 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
/* | |
This first iteration only paginated an initial set of pages. Adding or removing pages without | |
reinitializing the plugin would stay with the initial set. | |
Everything was encapsulated within a single jQuery plugin which knew everything about pagination: | |
* buttons/etc for moving around pages | |
* jumping to a page | |
* animating moving to a new page | |
* where in the dom the page numbers were stored | |
* updating the ui based upon which page you scrolled to | |
*/ | |
$.fn.extend({ | |
paginate: function() { | |
var $self = $(this); | |
var currentPage; | |
var pages = function() { | |
return $self.find('.page'); | |
} | |
var init = function() { | |
currentPage = pages().first().data('page'); | |
pages().waypoint(function() { | |
currentPage = $(this).data('page'); | |
$('#jump-pages').val(currentPage); | |
}); | |
$("#jump-pages").html(''); | |
_.each(pages(), function(page) { | |
var p = $(page).data('page'); | |
$('#jump-pages').append('<option value="' + p + '">' + p + '</option>'); | |
}); | |
} | |
var scrollToPage = function(pageCount) { | |
var ps = _.map(pages(), function(p) { return $(p).data('page') }); | |
currentPage = ps[_.indexOf(ps, currentPage) + pageCount] || currentPage; | |
var offset = pageCount < 0 ? -10 : 10; | |
$('html, body').animate({ scrollTop: $('.page[data-page="'+ currentPage + '"]').offset().top + offset }); | |
} | |
$('#next-page').on('click', function() { | |
scrollToPage(+1); | |
}); | |
$('#jump-pages').on('change', function() { | |
scrollToPage($(this).val() - currentPage); | |
}); | |
$('#previous-page').on('click', function() { | |
scrollToPage(-1); | |
}); | |
init(); | |
return { refresh: init } | |
} | |
}); |
This file contains hidden or 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
$.fn.extend({ | |
rendersPages: function(pages) { | |
var paginator = Paginator({ | |
pageNumber: $('#jump-pages'), | |
previous: $('#previous-page'), | |
next: $('#next-page'), | |
}, $(this)); | |
var pageTemplate = { | |
page: _.template($('#page-template').html()) | |
} | |
pages.bind('pageLoaded', function(p) { | |
var newPage = $(pageTemplate.page(p)); | |
$this.append(newPage); | |
paginator.paginate(); | |
}); | |
} | |
}); | |
(function() { | |
window.Paginator = function(controls, $pagesContainer) { | |
var pages = function() { | |
return $pagesContainer.find('.page'); | |
} | |
var currentPage = pages().first().data('page'); | |
controls.next.on('click', function() { | |
scrollToPage(+1); | |
}); | |
controls.pageNumber.on('change', function() { | |
scrollToPage($(this).val() - currentPage); | |
}); | |
controls.previous.on('click', function() { | |
scrollToPage(-1); | |
}); | |
var paginate = function() { | |
pages().waypoint(function() { | |
currentPage = $(this).data('page'); | |
controls.pageNumber.val(currentPage); | |
}); | |
controls.pageNumber.html(''); | |
_.each(pages(), function(page) { | |
var p = $(page).data('page'); | |
controls.pageNumber.append('<option value="' + p + '">' + p + '</option>'); | |
}); | |
} | |
var scrollToPage = function(pageCount) { | |
var ps = _.map(pages(), function(p) { return $(p).data('page') }); | |
var nextPage = ps[_.indexOf(ps, currentPage) + pageCount] || currentPage; | |
$('html, body').animate({ scrollTop: $('.page[data-page="'+nextPage + '"]').offset().top + pageCount }); | |
} | |
return { paginate: paginate } | |
} | |
})(); |
This file contains hidden or 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
$.fn.extend({ | |
rendersPages: function(pages) { | |
$pages = $(this); | |
var paginator = $pages.paginate(); | |
pages.bind('pageLoaded', function(p) { | |
var newPage = $(pageTemplate.page(p)); | |
$pages.append(newPage); | |
paginator.refresh(); | |
}); | |
} | |
}); | |
$.fn.extend({ | |
paginate: function() { | |
var $pages = (function($this) { return function() { | |
return $this.find('.page'); | |
} })($(this)) | |
var convertDomPagesToArray = function() { | |
return _.map($pages(), function(p) { return '' + $(p).data('page'); }); | |
} | |
var paginator = Paginator($('#jump-pages').val(), convertDomPagesToArray()); | |
paginator.refresh = function() { | |
paginator.paginate($('#jump-pages').val(), convertDomPagesToArray()); | |
$("#jump-pages").html(''); | |
_.each(pages, function(p) { | |
$('#jump-pages').append('<option value="' + p + '">' + p + '</option>'); | |
}); | |
$pages().waypoint(function() { | |
var currentPage = $(this).data('page'); | |
paginator.updateCurrentPage(currentPage); | |
$('#jump-pages').val(currentPage) | |
}); | |
} | |
$('#previous-page').on('click', paginator.previous); | |
$('#next-page').on('click', paginator.next); | |
$('#jump-pages').on('change', function() { | |
paginator.jumpTo($(this).val()); | |
}); | |
paginator.on('pageChanged', function(p, difference) { | |
var offset = difference < 0 ? -1 : 1 | |
$('html, body').animate({ scrollTop: $('#page-' + p).offset().top + offset}); | |
}) | |
return paginator; | |
} | |
}); | |
(function(exports) { | |
exports.Paginator = function(currentPage, pages) { | |
var paginator = {}; | |
asEvented.call(paginator); | |
paginator.previous = function() { | |
scrollToPage(-1); | |
}; | |
paginator.next = function() { | |
scrollToPage(+1); | |
} | |
paginator.jumpTo = function(newPage) { | |
scrollToPage(_.indexOf(pages, newPage) - _.indexOf(pages, currentPage)); | |
} | |
var paginate = paginator.paginate = function(newCurrentPage, newPageSet) { | |
pages = newPageSet; | |
updateCurrentPage(newCurrentPage); | |
} | |
var updateCurrentPage = paginator.updateCurrentPage = function(newCurrentPage) { | |
currentPage = newCurrentPage; | |
} | |
var scrollToPage = function(pageCount) { | |
updateCurrentPage(pages[_.indexOf(pages, currentPage) + pageCount] || currentPage); | |
paginator.trigger('pageChanged', currentPage, pageCount); | |
} | |
return paginator | |
} | |
})(window); |
When scanning the first example, it's easier for me to devise what's going on. Also, I like the idea of having all the knowledge required to paginate encapsulated in the single plugin. That, in itself, is a form of separating responsibility by keeping that knowledge out of the presentation layer.
On the last example, the first thought I had was; Why would you need to reuse the Paginator object itself outside the context of the plugin? You've named the dom attributes required to bootstrap the paginator generic enough that the plugin itself is quite reusable. I know now why you did it (SoC), but I don't think it's worth the extra code and I think exposing another interface is confusing.
I'm tending to agree with your final assertion.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@chrisjpowers: I've updated the first step with a version that is functionally equivalent to later sections.