Next level theming with Pinto

In this blog post, we focused on the basic principles of Pinto. We also looked at how to hit the ground running with some nice helper functions and integration with a design system such as Storybook. In a perfect world, we want to reuse our frontender’s twig templates in the component’s Storybook directory. Storybook generally lives outside of your web root, but with a few helpers we can easily target these templates. Let’s assume your Storybook directory structure looks something like this, where components is at the root of your repository:For the latter two methods, it’s as simple as defining the root directory where your compiled CSS and JS files are output. We usually just define these as:

Issues with traditional Drupal theming

We can utilise the Components module to give us a handy twig namespace to use. Let’s define this in our my_project_ds.info.yml file:

  1. A Frontender builds components in Storybook, containing CSS, JS, and twig templates
  2. A Backender implements one or more of the following:
    1. Theme hooks in a module, profile, or theme
    2. Templates are copied from Storybook and massaged into Drupal’s theme system
    3. A field formatter injects field data into the theme function
    4. Preprocess hooks massage or stitch together other data
    5. Twig templates contain logic to further massage variables for output in twig
    6. Entity view displays wire up the formatter

/**
* {@inheritdoc}
*/
public function templateDirectory(): string {
$reflection = new ReflectionEnumUnitCase($this::class, $this->name);
$definition = ($reflection->getAttributes(MyProjectComponent::class)[0] ?? NULL)?->newInstance() ?? throw new LogicException('All component cases must have a `' . MyProjectComponent::class . '.');
return sprintf('@my_project_components/%s', $definition->componentPath);
}

MyProjectObjectTraitnamespace Drupalmy_project_dsThemeObject;

use DrupalCoreCacheCacheableDependencyInterface;
use Drupalmy_project_dsMyProjectDsMyProjectObjectTrait;
use PintoAttributeThemeDefinition;

#[ThemeDefinition([
'variables' => [
  'title' => '',
  'description' => '',
  'image' => '',
],
])]
final class Card implements CacheableDependencyInterface {
use MyProjectObjectTrait;

private function __construct(
  private readonly string $title,
  private readonly array $image,
  private readonly ?string $description,
) {}

protected function build(mixed $build): mixed {
  return $build + [
    '#title' => $this->title,
    '#description' => $this->description,
    '#image' => $this->image,
  ];
}
}

use PintoAttributeAssetCss;

#[Css('card.css', preprocess: TRUE)]
final class Card implements CacheableDependencyInterface {

This approach can lead to several maintenance issues in the long term. $card = new Card('Title', 'Description', $image);
$build = $card();

In this example, we will define a Card Theme object:namespace Drupalmy_project_dsMyProjectDs;

use Drupalmy_project_dsThemeObjectCard;
use PintoListObjectListTrait;

enum MyProjectList: string implements ObjectListInterface {

use ObjectListTrait;

#[Definition(Card::class)]
case Card = 'card';

Pinto is a new module written by Daniel Phin (dpi). At PreviousNext, all new projects use it to dramatically improve the developer experience and velocity for theming Drupal sites. 

  • templateDirectory – This tells Pinto where to look for the twig template for the specific theme object it is registering.
  • cssDirectory – This tells Pinto where to find css files. For this demonstration, we’ll assume your design system outputs all CSS and JS files into a libraries directory inside the project’s web root.
  • jsDirectory – As above, but for JS files!

#[Definition(Card::class)]
#[MyProjectComponent('Components/Card')]
case Card = 'card';

Pinto eliminates all of this guesswork by allowing you to break components down into theme objects (i.e. PHP classes) and encapsulate all logic in a single place to be reused anywhere that component is needed.Each Theme object must be defined in the Enum as a case.Pinto has built-in attributes that automatically include CSS and JS when a theme object is invoked. This is done by adding the attributes to the Theme object class.The enum will look something like this. We recommend placing it inside a module dedicated to your design system. For this demo, we’ll call it my_project_ds.Traditional Drupal theming might look something like this:ConstructorIt may seem like a lot of set up upfront, but hopefully, you can see that once the initial enums and traits are established, it’s very easy to add new Theme objects.namespace Drupalmy_project_dsMyProjectDs;

use PintoListObjectListInterface;
use PintoListObjectListTrait;

enum MyProjectList: string implements ObjectListInterface {
use ObjectListTrait;
}

This blog post assumes the use of a design system tool such as Storybook, which will help us paint a picture of how Pinto can level up your Drupal theming.Before we talk about Attributes, let’s look at an example of how to define a Theme Object in the Enum itself.<div class="card">
<div class="image">
  {{ image }}
</div>
<div class="card__content">
  <div class="card__title">{{ title }}</div>
  {% if description %}
    <div class="card__description">
      {{ description }}
    </div>
  {% endif %}
</div>
</div>

ThemeDefinition attributeNOTE: Clear the cache after adding new cases for Pinto to discover newly added theme objects.The Attribute class:Pinto allows all theming logic to be encapsulated in reusable Theme Objects, removing the need for many traditional Drupal theming architectures such as theme hooks, preprocessing, field formatters, and even Display settings. It can easily integrate with tools such as Storybook to reuse templates—eliminating duplicated markup and making your frontenders happy!Now that we’re all set up, we can invoke this Theme object anywhere on our Drupal site! We can even chain/nest these Theme Objects together! namespace Drupalmy_project_dsMyProjectDs;

use DrupalCoreCacheCacheableDependencyInterface;
use DrupalCoreCacheCacheableMetadata;
use DrupalCoreCacheRefinableCacheableDependencyTrait;
use DrupalpintoObjectDrupalObjectTrait;

trait MyProjectObjectTrait {

use DrupalObjectTrait;
use RefinableCacheableDependencyTrait;

public function __invoke(): mixed {
  return $this->pintoBuild(function (mixed $build): mixed {
    if (!$this instanceof CacheableDependencyInterface) {
      throw new LogicException(static::class . ' must implement CacheableDependencyInterface');
    }
    (new CacheableMetadata())->addCacheableDependency($this)->applyTo($build);
    return $this->build($build);
  });
}

abstract protected function build(mixed $build): mixed;
}

What about CSS and JS?

  1. A title
  2. An image
  3. An optional description

In this section, we’ll discuss how to set Pinto up and explain some of the architecture of how the module works.namespace Drupalmy_project_dsMyProjectDs;

#[Attribute(flags: Attribute::TARGET_CLASS_CONSTANT)]
class MyProjectComponent {

public function __construct(
  public readonly string $componentPath,
) {
}

}

The constructor on the Theme object lets us define strictly typed properties for what we need for each theme variable. Generally speaking, we use a mix of constructors and factory methods to create Theme objects and then invoke them, as we’ve seen above.NOTE: This assumes the module is inside app/modules/custom/my_project_ds, hence the four parent directories.$image = new Image('path/to/image.png');
$card = new Card('My Card Title', 'My Description', $image());
return $card();

Our object trait also extends Pinto traits and core’s trait for adding cacheable metadata to a Theme object—more on this in the next blog post!Next, we need to define an attribute that takes a component path and will be used to wire up our Theme object with the directory it lives in./**
* {@inheritdoc}
*/
public function cssDirectory(): string {
 return '/libraries/my_project';
}

/**
* {@inheritdoc}
*/
public function jsDirectory(): string {
return '/libraries/my_project';
}

composer require drupal/pinto

Now, back to our templateDirectory. We can define our own Attributes to make it easy to specify which directory a component’s template is in.In the next blog, we’ll look at integrating this with bundle classes to streamline theming and really make Pinto sing! Following that, we’ll also be doing a comparison with Single Directory Components (SDC) and discuss why we prefer to use Pinto instead.This is another helper trait that will be used across all components. It provides convenience functions and wraps other traits to make adding new Theme objects a breeze.name: My Project Design System
description: 'Provides Pinto theme objects for my project.'
core_version_requirement: '>=10'
type: module
components:
namespaces:
  my_project_components:
    - ../../../../components/src

This is essentially Pinto’s version of hook_theme. It takes whatever is inside the theme definition and uses it when registering the Theme object as a Drupal theme hook. In this example, we simply define the variables we need to pass through. Pinto will even throw exceptions when any of these variables are missing from the resulting render array!First off, we need to install Pinto:

How to render Pinto Theme objects

After this setup, it becomes extremely easy to add new theme objects, vastly speeding up your theming development. Let’s look at how to do that now!Following on from the previous example, let’s set up a Card component. A Card is a fairly universal component in almost every project. In this example, a Card has:The twig template in Storybook might look something like this (reminder that this would be inside the components/src/Components/Card/card.html.twig file):You may have noticed that the $image parameter for the Card was an array. Invoked Theme objects output a render array, so we could do something like this:

Next up

Have you ever looked at a page and wondered how something got there? Twig debugging can help track down specific templates, but it can be difficult to pinpoint exactly how a particular piece of data made its way onto the page.parameters:
 pinto.namespaces:
- 'MyProjectDs'

Pinto Theme objects are registered via a PHP Enum inside the namespace defined above. This Enum must implement PintoListObjectListInterface. The Enum should also use the Pinto library’s ObjectListTrait which contains logic for asset discovery and automatically registering theme definitions from your Theme objects.

Similar Posts