Without question, Display Suite
is one of the most popular modules in Drupal’s contributed modules
history. It allows the creation of layouts, fields and exposes all sorts
of other powerful tools we use to build the presentation layer of our
Drupal sites.
One of the more powerful features of Display Suite (DS) is the ability to create custom fields that can be displayed inside DS layouts alongside the actual core field values. In Drupal 7, this has been a very popular way of building layouts and showing dynamic data that is not strictly related to the output of any Field API field on the node (or other) entity.
Display Suite has been ported and is being maintained for Drupal 8. Depending on another contributed module called Layout Plugin, the D8 version offers much of what we have available in Drupal 7 and probably even more.
In this article, we are going to look at how we can create our own Display Suite field in Drupal 8 using the new OOP architecture and plugin system. To demonstrate this, we are going to create a DS field available only on the Article nodes that can be used to display a list of taxonomy terms from a certain vocabulary. And we’re going to make it so that the latter can be configured from the UI, namely admins will be able to specify which vocabulary’s terms should be listed. Not much usefulness in this example, I know, but it will allow you to understand how things work.
If you are following along, the code we write is available in this repository inside the
Display Suite also uses the new plugin system to allow other modules to define DS fields. It exposes a
These will be available for selection in the UI under the
To do this, we need to implement the
Since we are referencing the
Although we don’t need it here, in most cases you’ll need to access the node entity that is currently being rendered. That is available inside the configuration array under the
I would like to mention a few more things before we take a look at the actual
Again we see classes which should be injected but were used statically to save some space. With the risk of sounding like a broken record, keep in mind that you should inject these. For now, we must use them at the top:
One of the more powerful features of Display Suite (DS) is the ability to create custom fields that can be displayed inside DS layouts alongside the actual core field values. In Drupal 7, this has been a very popular way of building layouts and showing dynamic data that is not strictly related to the output of any Field API field on the node (or other) entity.
Display Suite has been ported and is being maintained for Drupal 8. Depending on another contributed module called Layout Plugin, the D8 version offers much of what we have available in Drupal 7 and probably even more.
In this article, we are going to look at how we can create our own Display Suite field in Drupal 8 using the new OOP architecture and plugin system. To demonstrate this, we are going to create a DS field available only on the Article nodes that can be used to display a list of taxonomy terms from a certain vocabulary. And we’re going to make it so that the latter can be configured from the UI, namely admins will be able to specify which vocabulary’s terms should be listed. Not much usefulness in this example, I know, but it will allow you to understand how things work.
If you are following along, the code we write is available in this repository inside the
Demo
module. So feel free to check that out.Drupal 8 plugins
Much of the functionality that used to be declared using an_info
hook in Drupal 7 is now declared using plugins in Drupal 8. For more
information on using plugins and creating your own plugin types, make
sure you check out a previous Sitepoint article that talks about just that. Display Suite also uses the new plugin system to allow other modules to define DS fields. It exposes a
DsField
plugin type which allows us to write and maintain all the necessary logic for such a field inside a single plugin class (+ any services we might inject into it). So we no longer implement hook_ds_field_info()
and return an array of field information per entity type, but create a
plugin class with data straight in its annotation and the relevant logic
inside its methods.VocabularyTerms class
Let us start by creating our plugin class calledVocabularyTerms
inside the src/plugins/DsField
folder of our custom module and annotating it for our purposes:namespace Drupal\demo\Plugin\DsField;
use Drupal\ds\Plugin\DsField\DsFieldBase;
/**
* Plugin that renders the terms from a chosen taxonomy vocabulary.
*
* @DsField(
* id = "vocabulary_terms",
* title = @Translation("Vocabulary Terms"),
* entity_type = "node",
* provider = "demo",
* ui_limit = {"article|*"}
* )
*/
class VocabularyTerms extends DsFieldBase {
}
This class alone will hold all of our logic for our very simple
DsField plugin. But here are a couple of remarks about what we have so
far:- The annotation is quite self explanatory: it provides meta information about the plugin.
- The class extends
DsFieldBase
which provides base functionality for all the plugins of this type. - At the time of writing, the
ui_limit
annotation has just been committed to HEAD so it might not be available in the release you are using. Limiting the availability of the field on content types and view modes can be done by overriding theisAllowed()
method of the base class and performing the logic there.
Default configuration
We want our field to be configurable: the ability to select from a list of existing vocabularies. So let’s start off by providing some defaults to this configuration so that if the user selects nothing, theTags
vocabulary which comes with core will be used. For this, we have to implement the defaultConfiguration()
method:/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$configuration = array(
'vocabulary' => 'tags',
);
return $configuration;
}
And since we only have one configuration option, we return an array
with one element keyed by the configuration name. That’s about it. Formatters
We also want to have the ability to specify from the UI if the list of taxonomy terms is a series of links to their term pages or formatter as plain text. We could implement this within the configuration realm but let’s do so using formatters instead. And it’s very simple: we implement theformatters()
method and return an array of available formatters:/**
* {@inheritdoc}
*/
public function formatters() {
return array('linked' => 'Linked', 'unlinked' => 'Unlinked');
}
These will be available for selection in the UI under the
Field
heading of the Manage Display page of the content type. And we’ll be
able to see the choice when we are building the actual field for
display. But more on that in a second. Configuration summary
It’s also recommended that if we are using UI defined settings, we have a summary of what has been selected as a simple string that describes it. This gets printed under theWidget
heading of the Manage Display page of the content type. To do this, we need to implement the
settingsSummary()
method and return said text:/**
* {@inheritdoc}
*/
public function settingsSummary($settings) {
$config = $this->getConfiguration();
$no_selection = array('No vocabulary selected.');
if (isset($config['vocabulary']) && $config['vocabulary']) {
$vocabulary = Vocabulary::load($config['vocabulary']);
return $vocabulary ? array('Vocabulary: ' . $vocabulary->label()) : $no_selection;
}
return $no_selection;
}
Here we start getting more intimate with the actual configuration that was stored with the field, available by calling the getConfiguration()
method on our plugin class. What we do above, then, is check if the
vocabulary setting has been set, we load it based on its machine name
using the Vocabulary
class and return an array of strings that need to be printed. Since we are referencing the
Vocabulary
class, we also need to use it at the top:use Drupal\taxonomy\Entity\Vocabulary;
Important to note: I am using Vocabulary
statically here to load an entity for the sake of brevity. It is highly recommended you inject the relevant storage using dependency injection and use that to load entities. The same goes for most classes you’ll see me referencing statically below. Settings form
Now that we display which configuration has been chosen from the UI, it’s time to provide the actual form which will allow the user to do so. This will be made available by clicking the cogwheel under theOperations
heading of the Manage Display page of the content type. /**
* {@inheritdoc}
*/
public function settingsForm($form, FormStateInterface $form_state) {
$config = $this->getConfiguration();
$names = taxonomy_vocabulary_get_names();
$vocabularies = Vocabulary::loadMultiple($names);
$options = array();
foreach ($vocabularies as $vocabulary) {
$options[$vocabulary->id()] = $vocabulary->label();
}
$settings['vocabulary'] = array(
'#type' => 'select',
'#title' => t('Vocabulary'),
'#default_value' => $config['vocabulary'],
'#options' => $options,
);
return $settings;
}
Like before, we need to implement a method for this. And what we do
inside is load all the taxonomy vocabulary names and prepare an array of
options to be used with a Form API select list. The latter is the only
element we need for this form. Rendering the field
The last thing left to do is implement thebuild()
method responsible for rendering the contents of our field:/**
* {@inheritdoc}
*/
public function build() {
$config = $this->getConfiguration();
if (!isset($config['vocabulary']) || !$config['vocabulary']) {
return;
}
$query = \Drupal::entityQuery('taxonomy_term')
->condition('vid', $config['vocabulary']);
$tids = $query->execute();
if (!$tids) {
return;
}
$terms = Term::loadMultiple($tids);
if (!$terms) {
return;
}
return array(
'#theme' => 'item_list',
'#items' => $this->buildTermList($terms),
);
}
So what do we do here? First, we access the chosen vocabulary from the configuration. Then we run an EntityQuery to find all the terms in this vocabulary. Next, we load all these terms and finally we return a render array that uses the item_list
theme to print our terms. Although we don’t need it here, in most cases you’ll need to access the node entity that is currently being rendered. That is available inside the configuration array under the
entity
key. Moreover, under the build
key you have the actual render array of the node being built. So keep
this in mind and do inspect the other elements of the configuration
array on your own for more information.I would like to mention a few more things before we take a look at the actual
buildTermList()
method. First, for brevity, we used the EntityQuery service statically.
In your project, you should inject it. Second, we used the Term
class statically to load the taxonomy term entities. Again, you should
inject its storage and use that for this purpose. And lastly, we should
import the Term
class at the top with use
:use Drupal\taxonomy\Entity\Term;
Now that this is clear, let’s take a look at our own buildTermList()
method:private function buildTermList(array $terms) {
$config = $this->getConfiguration();
$formatter = isset($config['field']['formatter']) && $config['field']['formatter'] ? $config['field']['formatter'] : 'unlinked';
$items = array();
foreach ($terms as $term) {
$items[] = $this->buildTermListItem($term, $formatter);
}
return $items;
}
This method is responsible for getting the field formatter, looping
through the term entities and building an array of term information that
can be printed using the item_list
theme. As you can see, though, the individual term entity and formatter
are passed to yet another helper method to keep things nice and tidy:private function buildTermListItem(Term $term, $formatter) {
if ($formatter === 'linked') {
$link_url = Url::fromRoute('entity.taxonomy_term.canonical', array('taxonomy_term' => $term->id()));
return \Drupal::l($term->label(), $link_url);
}
return SafeMarkup::checkPlain($term->label());
}
Finally, in the buildTermListItem()
method we either return the sanitized title of the term or a link to it depending on the formatter. Again we see classes which should be injected but were used statically to save some space. With the risk of sounding like a broken record, keep in mind that you should inject these. For now, we must use them at the top:
use Drupal\Core\Url;
use Drupal\Component\Utility\SafeMarkup;
No comments:
Post a Comment
Thanks for your comment.