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