I am working on building out some pagination for a page on a SilverStripe site that is meant to show all articles at first by default, but the user can select which articles
There's definitely room for improvement when you return JSON to build HTML markup from JSON…
I also think it's good practice to write your application logic in a way that works without JS, then add the JS to progressively enhance your application. That way you don't lock out every non-JS device/reader/user.
So here's what I'd do (prepare for extensive answer):
First of all, you want to be able to filter your records by year. I think your approach of enabling filtering via URL is fine, so that's what we're going to do:
In your controller, add/modify the following method:
public function PaginatedReleases($year = null)
{
$list = NewsReleaseArticlePage::get()->sort('ArticleDate', 'DESC');
if ($year) {
$list = $list->where(array('YEAR("ArticleDate") = ?' => $year));
}
return PaginatedList::create($list, $this->getRequest());
}
This will allow you to get all entries, or only the ones from a certain year by passing in the $year
parameter.
public static $allowed_actions = array(
'year' => true
);
public function year()
{
$year = $this->request->param('ID');
$data = array(
'Year' => $year,
'PaginatedReleases' => $this->PaginatedReleases($year)
);
return $data;
}
After running dev/build
, you should already be able to filter your entries by year by changing the URL (eg. mypage/year/2016
or mypage/year/2015
etc.)
Add the following to your controller to create a form to filter your entries:
public function YearFilterForm()
{
// get an array of all distinct years
$list = SQLSelect::create()
->addFrom('NewsReleaseArticlePage')
->selectField('YEAR("ArticleDate")', 'Year')
->setOrderBy('Year', 'DESC')
->addGroupBy('"Year"')->execute()->column('Year');
// create an associative array with years as keys & values
$values = array_combine($list, $list);
// our fields just contain the dropdown, which uses the year values
$fields = FieldList::create(array(
DropdownField::create(
'Year',
'Year',
$values,
$this->getRequest()->param('ID')
)->setHasEmptyDefault(true)->setEmptyString('(all)')
));
$actions = FieldList::create(array(
FormAction::create('doFilter', 'Submit')
));
return Form::create($this, 'YearFilterForm', $fields, $actions);
}
Implement the doFilter
function. It simply redirects to the proper URL, depending what year was selected:
public function doFilter($data, $form)
{
if(empty($data['Year'])){
return $this->redirect($this->Link());
} else {
return $this->redirect($this->Link('year/' . $data['Year']));
}
}
Don't forget to add the Form name to the allowed_actions
:
public static $allowed_actions = array(
'YearFilterForm' => true, // <- this should be added!
'year' => true
);
Now delete your input field from your template and replace it with:
$YearFilterForm
.
After running dev/build
, you should have a page with a form that allows filtering by year (with working pagination)
With AJAX, we want to be able to load only the changed portion of the page. Therefore the first thing to do is:
Create a template Includes/ArticleList.ss
<% loop $PaginatedReleases %>
$ArticleDate.format("F j, Y"), $H1
<% end_loop %>
<% if $PaginatedReleases.MoreThanOnePage %>
<% if $PaginatedReleases.NotFirstPage %>
Prev
<% end_if %>
<% loop $PaginatedReleases.Pages %>
<% if $CurrentBool %>
$PageNum
<% else %>
<% if $Link %>
$PageNum
<% else %>
...
<% end_if %>
<% end_if %>
<% end_loop %>
<% if $PaginatedReleases.NotLastPage %>
Next
<% end_if %>
<% end_if %>
Your page template can then be stripped down to:
$YearFilterForm
<% include ArticleList %>
After dev/build
, everything should work as it did before.
Since this affects calls to year
and index
(unfiltered entries), create a helper method in your controller like this:
protected function handleYearRequest(SS_HTTPRequest $request)
{
$year = $request->param('ID');
$data = array(
'Year' => $year,
'PaginatedReleases' => $this->PaginatedReleases($year)
);
if($request->isAjax()) {
// in case of an ajax request, render only the partial template
return $this->renderWith('ArticleList', $data);
} else {
// returning an array will cause the page to render normally
return $data;
}
}
You can then add/modify the index
and year
methods to look identical:
public function year()
{
return $this->handleYearRequest($this->request);
}
public function index()
{
return $this->handleYearRequest($this->request);
}
(function($) {
$(function(){
// hide form actions, as we want to trigger form submittal
// automatically when dropdown changes
$("#Form_YearFilterForm .Actions").hide();
// bind a change event on the dropdown to automatically submit
$("#Form_YearFilterForm").on("change", "select", function (e) {
$("#Form_YearFilterForm").submit();
});
// handle form submit events
$("#Form_YearFilterForm").on("submit", function(e){
e.preventDefault();
var form = $(this);
$("#ArticleList").addClass("loading");
// submit form via ajax
$.post(
form.attr("action"),
form.serialize(),
function(data, status, xhr){
$("#ArticleList").replaceWith($(data));
}
);
return false;
});
// handle pagination clicks
$("body").on("click", "a.pagination", function (e) {
e.preventDefault();
$("#ArticleList").addClass("loading");
$.get(
$(this).attr("href"),
function(data, status, xhr){
$("#ArticleList").replaceWith($(data));
}
);
return false;
});
});
})(jQuery);
You now have a solution that gracefully degrades on non JS devices. Filtering via dropdown and pagination is AJAX enabled. The markup isn't defined in JS and in templates, it's just the SilverStripe templates that are responsible for the markup.
All that is left to do is add a nice loading animation when content refreshes ;)