HOW TO add custom contextual links to a views page in Drupal 8steemCreated with Sketch.

in #drupal7 years ago (edited)

| 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 views
  • view_id: this is the machine_name of the desired view
  • display_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

Sort:  

Coin Marketplace

STEEM 0.18
TRX 0.14
JST 0.030
BTC 58639.60
ETH 3167.30
USDT 1.00
SBD 2.43