Implementing Ajax in Block View Templates

It's easy to implement AJAX in a block view template. You might want to do this for a number of reasons. For example, with AJAX you can

  1. Update a list of recent articles
  2. Show that a user has added a page to his or her favorites
  3. Add a comment

all without reloading the page.

Block View

The block view will contain the HTML that you want to update via AJAX, and JavaScript that will perform these operations. Take, for example, the "Likes This" block that allows you to add a heart to a page, and mark that page as "Liked." This is the content on the page itself:

<div class="ccm-block-likes-this-wrapper">

    <?php if ($userLikesThis) { ?>

        <i class="fa fa-thumbs-up"></i> <?=t('You Liked.')?>

    <?php } else { ?>

        <div class="button">
        <a rel="nofollow" href="<?php
            echo $view->action('like', Core::make('token')->generate('like_page'))?>"
           data-action="block-like-page" class="general_button_type_3"><i class="fa fa-heart"></i> <?=t('Like')?></a>

        </div>

    <?php } ?>
</div>

This is pretty simple. If the $userLikesThis boolean is set to true (and the setting happens in the block controller) we display the notification that the user has already liked the page. Otherwise, we get an opportunity to like the page. The real important stuff happens here (irrelevant HTML attributes have been removed):

<a href="<?php echo $view->action('like',
    Core::make('token')->generate('like_page'))?>"
    data-action="block-like-page"><?=t('Like')?>
</a>

The href itself is the action() method as run on the BlockView instance that's automatically injected into the current block's view template. We're going to run a method named "like", and we're passing a second argument to that method, which is a CSRF token generated against the phrase "like_page". This will help protect this method from CSRF attacks. Finally, there's a data attribute (block-like-page) that we will attach our JavaScript onclick to. That JavaScript is run in the block's view.js file:

JavaScript (view.js)

$(function() {
    $('a[data-action=block-like-page]').on('click', function() {
        jQuery.ajax({
            url: $(this).attr('href'),
            dataType: 'html',
            type: 'post',
            success: function(response) {
                $('div.ccm-block-likes-this-wrapper').replaceWith(response);
            }
        });
        return false;
    });
});

This is pretty standard jQuery. Basically, for every anchor link on the page with the block-like-page data attribute, we attach an AJAX handler that hits the URL in the link's href. Then, the wrapper DIV is replaced with the contents that come back from the server. That way, we can refresh the block with its own contents without reloading the whole page!

The URL

What's the URL look like when generated on an example page? Like this:

http://mysite.com.com/path/to/page/like/1449092546%3Acd36b79545b999eec596e1f403477b2e/2279/

There are some interesting components here. First, notice that the URL is still linking to the specific page that we were already on (/path/to/page). But it does have some additional components. First, the "like" method is found in the URL, after the page path. Next, the CSRF token is found. Finally, the number 2279 actually corresponds to the block ID of the block that's being rendered. We use this number in our server-side processing to ensure that multiple blocks with actions on the page don't all fire their events at once.

The Controller

The block controller handles the final bit, which is the server-side processing component. To make our block controller respond to the URL above, we have to make sure that it implements a method named "action_like", with a block ID parameter. Here's how it might look:

public function action_like($token = false, $bID = false)
{
    if ($this->bID != $bID) {
        return false;
    }
    if (Core::make('token')->validate('like_page', $token)) {
        $page = Page::getCurrentPage();
        $u = new User();
        $this->markLike($page->getCollectionID(), $page->getCollectionTypeID(), $u->getUserID());
        if ($_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
            $b = $this->getBlockObject();
            $bv = new BlockView($b);
            $bv->render('view');
        } else {
            Redirect::page($page)->send();
        }
    }
    exit;
}

There's a lot happening in this method but it should be pretty easy to follow. First, we check to see if the block ID passed to this method matches the one in the current instance of the controller. If it doesn't, we return. This way we can have multiple instances of the same block type on the page, and only the proper one will submit at once. This isn't that necessary in this example, but for block types like Form it can be very important.

Next, we validate the passed token parameter to make sure it is correct. This ensures that someone can't just go to this URL or copy it and send it out – it needs to be generated by the server.

Finally, we mark the page as liked, and depending on the header of the request, we either redirect the user back to the page, or we render the block view again. This way, if the page is called via AJAX, it'll re-render the block in the page without reloading, but if something cancels the AJAX request and the user simply clicks the link and goes to the link, concrete5 is smart enough to redirect them to the page.

Loading Conversation