| English | Español |
Relaying on contextual links is an excellent UX strategy, they help Drupal site builders to interact with the different entities rendered into a page view. For example, it is common to find a homepage grid view. When the user hovers over the grid view, the contextual links icon will appear, an easy click will show the available options. By default the only option available is to edit such grid view settings:
There is room for extra contextual links don't you think?
A complex use case
Grids use to show a certain sets of elements, maybe each with a photo, a title or an small description. They are just some fields from a set of content types. Power users will appreciate to have an "edit mode" version of the grid view. We can solve this use case by adding another view, with same filters and sorting, yet with a different display style, maybe a table with an exposed filters form.
Pretty useful!
Yet both views are not connected and the edit mode is not an option for everyone. Only certain privileged users should have access to edit mode...
Contextual links to the rescue!
How about adding a context link to access edit mode from the grid view?
Contextual links behind scenes
Contextual links are built upon Drupal and Symfony APIs, with front-end and back-end components. The first requirement is to have a route, by design Drupal will only render links to valid symfony routes, that will make the back-end components happy. For the front-end side Drupal needs each contextual link to have a machine name and the controller is responsible for adding them to the requested page. Things got complex, I know! Get used to that because Drupal is as powerful as hard.
In plain English, contextual links are objects that depend on other objects, such need to be declared before being added to the desired page. Now you can read the previous paragraph again and the official documentation on providing module-defined contextual links in Drupal 8.
Declaring contextual links
Module mymodule
is the namespace from now on. Add the YAML file mymodule.links.contextual.yml
into the module's folder with the following contents:
mymodule.contextual_links:
title: 'Edit mode (power users only)'
route_name: view.view_id.display_id
group: mymodule.contextual_links
Notice that mymodule.contextual_links
is exactly the same for both the ID and the group. Also the route_name
is an existing route object. Forget this and no magic will happen.
Figuring out the route_name
for each view page
Views requires each view page to have an exclusive path, for example: /myview-path
. Also, views takes care of creating a corresponding route_name
that would end up like this: view.myview.page_1
and view.myview.page_2
. Route names would be just arbitrary text, but Drupal has a convenient naming convention:
view
: this is the prefix for all routes generated by viewsview_id
: this is the machine_name of the desired viewdisplay_id
: page_1, page_2, block_1, attachment_2, etc. Not hard to figure out.
Let's assume that our grid page has view.myview.page_1
as route_name and view.myview.page_2
belongs to edit mode (Note page_1 != page_2).
This use case is about linking two views, but you might want to add a contextual link to another kind of route, maybe a node or a custom form, so you got some homework left.
Overriding the view controller
Every page requested in Drupal has a designated controller class, views makes use of ViewPageController
in our particular use case. Since Drupal routes are actual Symfony routes, it is required to override the controller in order to add the already declared contextual links. Don't panic! the new controller is just an extended class of ViewPageController
and there is official documentation on altering existing routes and adding new routes based on dynamic ones.
Add a YAML file mymodule.services.yml
into the module's folder with the following contents:
services:
mymodule.route_subscriber:
class: Drupal\mymodule\Routing\MyModuleRouteSubscriber
tags:
- { name: event_subscriber }
The only thing you should care about here is the class
, which should be stored into the file: src/Routing/MyModuleRouteSubscriber.php
with the following contents:
<?php
namespace Drupal\mymodule\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Listens to the dynamic route events.
*/
class MyModuleRouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
public function alterRoutes(RouteCollection $collection) {
// Lookup the route object by its route_name
if ($route = $collection->get('view.myview.page_2')) {
// Override the view controller with an extended version of it
$route->setDefault(
'_controller',
'Drupal\mymodule\Routing\ViewPageController::handle'
);
}
}
}
If you read previous code carefully, it has comments explaining how the view controller gets overridden.
Extending the default view controller
Now that views is not going to relay on the default ViewPageController
but on a custom one, it is time to code a bit more and complete the task. Create file src/Routing/ViewPageController.php
with the following contents:
<?php
namespace Drupal\mymodule\Routing;
use Drupal\views\Routing\ViewPageController as BaseController;
use Drupal\Core\Routing\RouteMatchInterface;
class ViewPageController extends BaseController {
/**
* {@inheritdoc}
*/
public function handle($view_id, $display_id, RouteMatchInterface $route_match) {
// Let the original controller do what it should
$build = parent::handle($view_id, $display_id, $route_match);
// Make sure to add the contextual link to the desired view page
if ($view_id == 'myview' && $display_id == 'page_1') {
$build['#contextual_links']['mymodule.contextual_links'] = [
'route_parameters' => [
'view' => $view_id,
],
'metadata' => [
'location' => 'page',
'name' => $view_id,
'display_id' => 'page_2',
],
];
}
return $build;
}
}
Conclusion
It all only required two YAML files, two classes and zero hooks to complete the task. I know it was not that easy but I've learned more about Routing, Controllers, Symfony, Drupal 8 and OOP concepts in the process and it worths the effort!
-- @develCuy
This is a great read with great code example, thanks