Interactive Blocks

Improvements?

Let us know by posting here.

While most blocks in Concrete CMS are purely presentational, it's possible and quite common for Concrete blocks to contain interactivity. Some of the more obvious examples of this are the form block and the survey block, both of which feature forms that post back to the block controllers and save data against the block record. More subtle examples of interactive blocks include the Page List, Topic List and Tags blocks, which interact with each other: the tags and topic list blocks can take a page list on a particular page and provide it a topic or tag, with which the Page List can filter its page results.

Here's how to add interactivity to your blocks. Note: The contents of this section assume you are running Concrete 5.7.3.2 or greater. This is currently available in Github. Much of this functionality is available in 5.7.3.1 and earlier, but some of the examples may be slightly different.

Example: Handling a Form Submission from a Block's View Layer

The survey block presents the user with a form that, when submitted, tallies the result against a particular option. Here's how that works:

Form Action

To create a form action in a block, use the \Concrete\Core\Block\View\BlockView::action method:

$view->action('form_save_vote');

The $view object is an instance of the \Concrete\Core\Block\View\BlockView object. It is automatically available in a block template's local scope. The name of this action isn't important. It could be "submit" or even "save."

Generated URL

The $view->action('form_save_vote') method generates the following URL:

http://www.example.com/index.php/path/to/page/form_save_vote/{bID}

Where bID is the numerical block ID.

Controller Method

Now create a method named action_method_name, where method_name is the contents of the action() method you used on the page. For the survey block, the name is action_form_save_vote. When the form is submitted, this action will be run. At the end of this method you can redirect the user to another page, but this is not required.

public function action_form_save_vote($bID = false)
{
    if ($this->bID != $bID) {
        return false;
    }

    if (!$this->hasVoted()) {
        // Code omitted
        $c = \Page::getCurrentPage();
        $this->redirect($c->getCollectionPath() . '?survey_voted=1');
    }
}

When we use $view->action(), the current block ID is automatically entered as the first argument. This then gets passed to our action, and we can determine whether to proceed. Some blocks don't care about whether they are being triggered by the same instance as they began. Others, like the form and survey, definitely want to ensure that only the submitted form is the one that is saved for that request.

Example: Passing Data from a custom URL into a Block's View Layer

A block can react to URL parameters without using $view->action(). If you want your block to react to the following URL:

http://www.example.com/index.php/path/to/page/custom_foo/param1/param2/param3

Just create a method named

public function action_custom_foo($param1 = null, $param2 = null, $param3 = null)
{
    // Custom Logic
    $this->view();
}

No need to worry about the block ID in this case. Note: All blocks on this page that have a method named action_custom_foo with the same signature will fire this action.

Blog Example

Concrete CMS's sample blog in the Elemental theme shows a good example of how multiple block types can use action. When a topic in the Topic List block is clicked, the following URL is browsed to:

http://www.example.com/index.php/blog/topic/11/projects

This means that any block on the page that responds to action_topic with a $treeNodeID and a $topicText as its optional second parameters will fire. This includes the following blocks: topic list, page title, and page list.

Topic List

The topic list block uses the action_topic() method to mark the particular clicked-on topic as selected. Here is its action_topic() method in the controller.

public function action_topic($treeNodeID = false, $topic = false)
{
    $this->set('selectedTopicID', intval($treeNodeID));
    $this->view();
}

The method simply sets the selected topic ID into the block view. The view then will check to see if this variable exists, and if it does, it will apply the active CSS class to the clicked-on topic.

Page Title

When the "Archives" custom template is applied to the Page Title block, the Page Title block will listen for action_topic() and display the selected topic text instead of "Blog".

public function action_topic($treeNodeID = false, $topic = false)
{
    if ($treeNodeID) {
        $topicObj = Topic::getByID(intval($treeNodeID));
        $this->set('currentTopic', $topicObj);
    }
    $this->view();
}

First, it uses the topic tree node ID to grab the selected topic, and sets it into the page. The archives custom template then displays it.

<?php
if (is_object($currentTopic)) {
    $title = t('Topic Archives: %s', $currentTopic->getTreeNodeDisplayName());
}
?>
<h1 class="page-title"><?=$title?></h1>

Page List

Finally, the Page List block filters by the topic. First, it routes the "topic" URL slug to the action_filter_by_topic() method (see below for more on how to do this). Then the action_filter_by_topic() method filters the instance of the \Concrete\Core\Page\PageList object that is already instantiated within the Page List block.

public function action_filter_by_topic($treeNodeID = false, $topic = false)
{
    if ($treeNodeID) {
        $this->list->filterByTopic(intval($treeNodeID));
        $topicObj = Topic::getByID(intval($treeNodeID));
        if (is_object($topicObj)) {
            $seo = Core::make('helper/seo');
            $seo->addTitleSegment($topicObj->getTreeNodeDisplayName());
        }
    }
    $this->view();
}

The method even updates the page title using the SEO service to that of the selected topic.

Advanced: Programmatically Disabling action_ URLs for Certain Block Instances

The Page List block provides a number of action_ URLs for other blocks to use to filter the resulting pages displayed. While this is powerful, its only meant to be used when you're displaying page lists in an interactive capacity (e.g. as part of a blog or search results.) If you're just listing featured pages, or recenty updated pages, you may not want the ability to arbitrarily filter these pages via URLs. That's why the Page List block uses a custom internal boolean to control whether this filtering is available. Here's how that works:

Any time an action URL is checked, the \Concrete\Core\Block\BlockController method isValidControllerTask is checked. This checks the class to see if the method exists, and whether it starts with action_. The Page List block controller overrides this method to also check if an additional internal boolean is set to true.

public function isValidControllerTask($method, $parameters = array())
{
    if (!$this->enableExternalFiltering) {
        return false;
    }

    return parent::isValidControllerTask($method, $parameters);
}

This only enables the action_ URLs if the page list "Enable External Filtering" option is checked in the Page List edit interface.

Advanced: Mapping a Custom URL segment to a Differently Named Action

Sometimes you might want your method to be named differently than the standard action_* method name. For example, the Page List block reacts to the "tag", "topic" and "date" parameters:

http://www.example.com/index.php/path/to/page/topic/123/kittens
http://www.example.com/index.php/path/to/page/date/2015/01/01
http://www.example.com/index.php/path/to/page/tags/hats

The core team didn't want to create methods named action_tag, action_date, action_topic – just because amidst all the other methods some of the purpose of these methods might be lost. We wanted to name the methods

  • action_filter_by_tag
  • action_filter_by_date
  • action_filter_by_topic

Without losing the nice, succinct URL segments. To do this, simply override the built-in \Concrete\Core\Block\BlockController

public function getPassThruActionAndParameters($parameters)
{
    if ($parameters[0] == 'topic') {
        $method = 'action_filter_by_topic';
        $parameters = array_slice($parameters, 1);
    } elseif ($parameters[0] == 'tag') {
        $method = 'action_filter_by_tag';
        $parameters = array_slice($parameters, 1);
    } elseif (Loader::helper("validation/numbers")->integer($parameters[0])) {
        // then we're going to treat this as a year.
        $method = 'action_filter_by_date';
        $parameters[0] = intval($parameters[0]);
        if (isset($parameters[1])) {
            $parameters[1] = intval($parameters[1]);
        }
    }

    return array($method, $parameters);
}