A custom Styles library with Config Entities

Image

In this project we had built a collection of components using a combination of Paragraphs and referenced block entities. While the system we built was incredibly flexible, there were a number of variations we wanted to be able to apply to each component. We also wanted the system to be easily extensible by the client team going forward. To this end, we came up with a system of configuration entities that would allow us to provide references to classes and thematically name these styles. We built upon this by extending the EntityReferenceSelections plugin, allowing us to customize the list of styles available to a component by defining where those styles could be used. The use of configuration entities allows the client team to develop and test new style variations in the standard development workflow and deploy them out to forward environments, giving an opportunity to test the new styles in QA prior to deployment to Production.

The Styles configuration entity

This configuration entity is at the heart of the system. It allows the client team to come in through the UI and create the new style. Each style is comprised of one or more classes that will later be applied to the container of the component the style is used on. The Style entity also contains configuration allowing the team to identify where this style can be used. This will be used later in the process to allow the team to limit the list of available styles to just those components that can actually make use of them.  

The resulting configuration for the Style entity is then able to be exported to yml, versioned in the project repository and pushed forward through our development pipeline. Here's an example of a Style entity after export to the configuration sync directory.

uuid: 7d112e4e-0c0f-486e-ae36-b608f55bf4e4
langcode: en
status: true
dependencies: {  }
id: featured_blue
label: 'Featured - Blue'
classes:
  - comp__featured-blue
uses:
  rte: rte
  cta: cta
  rail: '0'
  layout: '0'
  content: '0'
  oneboxlisting: '0'
  twoboxlisting: '0'
  table: '0'

 

Uses

For "Uses" we went with a simple configuration form. The result of this is form is stored in the key value store for Drupal 8. We can then access that configuration from our Styles entity and from our other plugins in order to retrieve and decode the values. Because the definition of each use was a simple key and label, we didn't need anything more complex for storage.

 

Assigning context through a custom Selection Plugin

By extending the core EntityReferenceSelection plugin, we're able to combine our list of Uses with the uses defined in each style component. To add Styles to a component, the developer would first add a entity reference field to the the Styles config entity to the component in question. In the field configuration for that entity reference field, we can chose our custom Selection Plugin. This exposes our list of defined uses. We can then select the appropriate use for this component. The end result of this is that only the applicable styles will be presented to the content team when they create components of this type.

<?php

/**
 * Plugin implementation of the 'selection' entity_reference.
 *
 * @EntityReferenceSelection(
 *   id = "uses",
 *   label = @Translation("Uses: Filter by where the referenced entity will be used."),
 *   group = "uses",
 *   weight = 0
 * )
 */
class UsesSelection extends SelectionPluginBase implements ContainerFactoryPluginInterface {

  use SelectionTrait;

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);

    $options = Styles::getUses();
    $uses = $this->getConfiguration()['uses'];

    if ($options) {
      $form['uses'] = [
        '#type' => 'checkboxes',
        '#title' => $this->t('Uses'),
        '#options' => $options,
        '#default_value' => $uses,
      ];
    }
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
    $uses_config = $this->getConfiguration()['uses'];

    $uses = [];
    foreach ($uses_config as $key => $value) {
      if (!empty($value)) {
        $uses[] = $key;
      }
    }

    $styles = \Drupal::entityTypeManager()
      ->getStorage('styles')
      ->loadMultiple();

    $return = [];
    foreach ($styles as $style) {
      foreach ($style->get('uses') as $key => $value) {
        if (!empty($value)) {
          if (in_array($key, $uses)) {
            $return[$style->bundle()][$style->id()] = $style->label();
          }
        }
      }
    }
    return $return;
  }

}

In practice, this selection plugin presents a list of our defined uses in the configuration for the field. The person creating the component can then select the appropriate use definitions, limiting the scope of styles that will be made available to the component.

 

Components, with style.

The final piece of the puzzle is how we add the selected styles to the components during content creation. Once someone on the content team adds a component to the page and selects a style, we then need to apply the style to the component. This is handled by preprocess functions for each type of component we're working with. In this case, Paragraphs and Blocks.  

In both of the examples below we check to see if the entity being rendered has our 'field_styles'. If the field exists, we load its values and the default class attributes already applied to the entity. We then iterate over any styles applied to the component and add any classes those styles define to an array. Those classes are merged with the default classes for the paragraph or block entity. This allows the classes defined to be applied to the container for the component without a need for modifying any templates.

/**
 * Implements hook_preprocess_HOOK().
 */
function bcbsmn_styles_preprocess_paragraph(&$variables) {
  /** @var Drupal\paragraphs\Entity\Paragraph $paragraph */
  $paragraph = $variables['paragraph'];
  if ($paragraph->hasField('field_styles')) {
    $styles = $paragraph->get('field_styles')->getValue();
    $classes = isset($variables['attributes']['class']) ? $variables['attributes']['class'] : [];
    foreach ($styles as $value) {
      /** @var \Drupal\bcbsmn_styles\Entity\Styles $style */
      $style = Styles::load($value['target_id']);
      if ($style instanceof Styles) {
        $style_classes = $style->get('classes');
        foreach ($style_classes as $class) {
          $classes[] = $class;
        }
      }
    }
    $variables['attributes']['class'] = $classes;
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function bcbsmn_styles_preprocess_block(&$variables) {
  if ($variables['base_plugin_id'] == 'block_content') {
    $block = $variables['content']['#block_content'];
    if ($block->hasField('field_styles')) {
      $styles = $block->get('field_styles')->getValue();
      $classes = isset($variables['attributes']['class']) ? $variables['attributes']['class'] : [];
      foreach ($styles as $value) {
        /** @var \Drupal\bcbsmn_styles\Entity\Styles $style */
        $style = Styles::load($value['target_id']);
        if ($style instanceof Styles) {
          $style_classes = $style->get('classes');
          foreach ($style_classes as $class) {
            $classes[] = $class;
          }
        }
      }
      $variables['attributes']['class'] = $classes;
    }
  }
}

 

Try it out

We've contributed the initial version of this module to Drupal.org as the Style Entity project. We'll continue to refine this as we use it on future projects and with the input of people like you. Download Style Entity and give it a spin, then let us know what you think in the issue queue.