SilverStripe - Create pagination based on dropdown

2019-02-25 09:31发布

问题:

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 to view by selecting a year from a dropdown control.

Here is the template for the articles. Right now I have code in place that adds pagination when the page first loads or is reloaded and all articles are grabbed from the server:

<select id="SelectNewsYear">
    <option value="">Select a year</option>
    <% loop $GroupedNewsByDate.GroupedBy(PublishYear) %>
        <option value="$PublishYear">$PublishYear</option>
    <% end_loop %>
    <option value="all">Show all</option>
</select>
<br /><br />

<div class="RecentNews">
    <% loop $PaginatedReleases %>
       $ArticleDate.format("F j, Y"), <a href="$URLSegment">$H1</a><br />
    <% end_loop %>

    <% if $PaginatedReleases.MoreThanOnePage %>
        <% if $PaginatedReleases.NotFirstPage %>
            <a class="prev" href="$PaginatedReleases.PrevLink">Prev</a>
        <% end_if %>
        <% loop $PaginatedReleases.Pages %>
            <% if $CurrentBool %>
                $PageNum
            <% else %>
                <% if $Link %>
                    <a href="$Link">$PageNum</a>
                <% else %>
                    ...
                <% end_if %>
            <% end_if %>
        <% end_loop %>
        <% if $PaginatedReleases.NotLastPage %>
            <a class="next" href="$PaginatedReleases.NextLink">Next</a>
        <% end_if %>
    <% end_if %>
</div>

The PaginatedReleases function in Page.php:

//Returns a paginted list of news releases
public function PaginatedReleases(){
    $newslist = NewsReleaseArticlePage::get()->sort('ArticleDate', "DESC");
    return new PaginatedList($newslist, $this->getRequest());
}

The problem now is figuring out how to maintain the pagination feature whenever a year is selected from the dropdown. Initially, I did not concern myself with pagination as I was more concerned with the functionality of the dropdown list. This is the jQuery and AJAX code I have set up currently that grabs the year value from the dropdown list and passes it to the server to the appropriate function:

(function($) {
    $(document).ready(function() {

        var SelectNewsYear = $('#SelectNewsYear');

        var month = new Array();
        month[0] = "January";
        month[1] = "February";
        month[2] = "March";
        month[3] = "April";
        month[4] = "May";
        month[5] = "June";
        month[6] = "July";
        month[7] = "August";
        month[8] = "September";
        month[9] = "October";
        month[10] = "November";
        month[11] = "December";


        SelectNewsYear.change(function() {

            if (SelectNewsYear.val() != "" && SelectNewsYear.val() != null &&  SelectNewsYear.find('option:selected').attr('value') !="all") {
                sendYear();
            }
            else{
                showAll();
            }
        });

        //get all articles by the year selected
        function sendYear(){
            var year = SelectNewsYear.find('option:selected').attr('value');
            $.ajax({
                type: "POST",
                url: "/home/getNewsByYear/"+year,
                dataType: "json"
            }).done(function (response) {
                var list = '';
                var newsSection = $('.RecentNewsByYear');

                for (var i=0;i<response.length;i++){
                    var newsDate = new Date(response[i].date);
                    var monthFullName = month[newsDate.getUTCMonth()];

                    list += monthFullName + " " + newsDate.getUTCDate() +", " +newsDate.getFullYear()  + ', ' + '<a href="' + response[i].article + '"target="_blank">' + response[i].title +"</a> <br />";
                }
                newsSection.empty();
                newsSection.append(list);
            });
        }

    });
}(jQuery));

$ = jQuery.noConflict();

And the getNewsByYear function from Page.php:

//Get all recent news by year based on selected year from dropdown
    public function getNewsByYear(){
        //Get the year selected by the dropdown
        $newsReleaseYear = $this->getRequest()->param('ID');

        //Group together all news that are associated with that selected year
        $newsReleases = NewsReleaseArticlePage::get();

        $return = array();

        //put the news releases into the array that match the selected year
        foreach($newsReleases as $newsRelease){
            $newsDate = date("Y", strtotime($newsRelease->ArticleDate));

            if($newsDate == $newsReleaseYear){
                $return[] = array(
                    'title' => $newsRelease->H1,
                    'date' => $newsRelease->ArticleDate,
                    'article' => $newsRelease->URLSegment
                );
            }
        }

        return json_encode($return);
    }

The getNewsByYear function works fine as is, but I am not sure how to incorporate the SilverStripe PaginationList feature here. I am wondering if there is a way to return the selected articles without relying on json encoded data?

回答1:

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

Enable filtering by year

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:

1. Get paginated list, optionally filtered by year

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.

2. Add a year action to your controller

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.)

Use a form with a dropdown to filter

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 <select> 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)

Enable AJAX

With AJAX, we want to be able to load only the changed portion of the page. Therefore the first thing to do is:

1. Create a separate template for the content that should be loaded asynchronously

Create a template Includes/ArticleList.ss

<div id="ArticleList" class="RecentNews">
    <% loop $PaginatedReleases %>
       $ArticleDate.format("F j, Y"), <a href="$URLSegment">$H1</a><br />
    <% end_loop %>

    <% if $PaginatedReleases.MoreThanOnePage %>
        <% if $PaginatedReleases.NotFirstPage %>
            <a class="prev pagination" href="$PaginatedReleases.PrevLink">Prev</a>
        <% end_if %>
        <% loop $PaginatedReleases.Pages %>
            <% if $CurrentBool %>
                $PageNum
            <% else %>
                <% if $Link %>
                    <a href="$Link" class="pagination">$PageNum</a>
                <% else %>
                    ...
                <% end_if %>
            <% end_if %>
        <% end_loop %>
        <% if $PaginatedReleases.NotLastPage %>
            <a class="next pagination" href="$PaginatedReleases.NextLink">Next</a>
        <% end_if %>
    <% end_if %>
</div>

Your page template can then be stripped down to:

$YearFilterForm
<% include ArticleList %>

After dev/build, everything should work as it did before.

2. Serve partial content, when page is requested via AJAX

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);
}

3. Wire everything with some JavaScript

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

Conclusion

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 ;)