Creating Your First Single Directory Component within Drupal

By mherchel, 12 March, 2023
Screenshot of code editor showing single directory components

Single directory components (SDC) is the biggest paradigm shift in Drupal since the introduction of Twig in 2013! At its core, SDC allows you to gather related files from a component (CSS, JS, Twig), and group them into one directory. SDC will automatically generate the library and load it when the component is called.

Because we define the schema within a component’s YAML definition, modules can automatically create forms to populate data. This means that in the future, modules will automatically detect components and insert their respective forms into Layout Builder, CKEditor, Paragraphs, Blocks, Fields, etc! In addition, you’ll be able to integrate your SDC components directly into Storybook or other component library managers!

Background on Single Directory Components (SDC)

Mateu, Lauri, and I started discussing SDC in October 2022. The vision was heavily inspired by Mateu’s work on the CL Components suite of modules, which provides similar base functionality (and much more).

Since then, Mateu has been writing the code for the module, and discussing architectural decisions with Lauri, me, and other developers. Mateu and I also met with the maintainers of UI Patterns to validate the architecture and make sure it will work for them.

During a core committer meeting, it was determined that the best way to bring this into core is as an experimental module. This will allow SDC to validate and modify its architecture as necessary as more developers develop against it and extend it. Our hope is to get SDC committed to Drupal core in time for the 10.1.0 release in June 2023.

Relevant Drupal.org issues for SDC

For a more detailed background of SDC, see Mateu’s article, Getting Single Directory Components in Drupal Core.

Lets migrate the “tabs” Olivero component to SDC!

As part of the roadmap issue, we want to validate the architecture by creating SDC components within both Olivero and Umami (the demo theme). I chose to migrate Olivero’s tabs component because it has CSS, JavaScript, and several variables to be passed into it.

Image
Olivero login screen showing the tabs component
On the login page, the tabs component contains the "Login", "Create an account", and "Reset your password" links

Moving this component into SDC was easier than I expected, and took about 10 minutes. This makes me even more excited about getting this into core!

Step 1: Prepare core by applying SDC patch and setting up Twig debugging.

Drupal core obviously doesn’t yet have SDC available, so we need to apply the patch. To do this, I pulled down the plain diff from the open merge request, and applied it using git apply. Now when I navigate to the modules page, I see Single Directory Components, and can enable it.

Image
Module screen showing Single Directory Components enabled
With Mateu's patch applied, we can now enable Single Directory Components like normal.

Because I’m doing theme development, I also want to enable Twig debugging, as well as enable verbose error logging. This will make sure Drupal outputs errors on the screen, instead of giving a “White screen of death” (WSOD).

$config['system.logging']['error_level'] = 'verbose';

Step 2: Create the directory and files

For SDC to recognize our component, we need to create a components directory within the theme, and then create a subdirectory with the component’s name (in this case the name will be “tabs”).

Within this new components/tabs directory, we’ll create two files

  • tabs.component.yml - This will hold the metadata for the component (friendly name, status, description, etc), as well as the schema for the data that the component will use.
  • tabs.twig - This will hold the markup for the component

Note that Mateu’s CL Generator module provides a Drush command to scaffold the components. It’s also likely that Drush will eventually have built-in support for this once SDC is in Drupal core.

Step 3: Populate the tabs.twig

The final tabs.twig file will look like this:

{% if primary %}
  <h2 id="primary-tabs-title" class="visually-hidden">{{ 'Primary tabs'|t }}</h2>
  <nav role="navigation" class="tabs-wrapper" aria-labelledby="primary-tabs-title" data-drupal-nav-primary-tabs>
    <ul class="tabs tabs--primary">{{ primary }}</ul>
  </nav>
{% endif %}
{% if secondary %}
  <h2 id="secondary-tabs-title" class="visually-hidden">{{ 'Secondary tabs'|t }}</h2>
  <nav role="navigation" class="tabs-wrapper" aria-labelledby="secondary-tabs-title">
    <ul class="tabs tabs--secondary">{{ secondary }}</ul>
  </nav>
{% endif %}

To get here, we copy from the current template file at olivero/templates/navigation/menu-local-tasks.html.twig, but remove the {{ attach_library('olivero/tabs') }}, as this is no longer needed with SDC.

Step 4: Point menu-local-tasks.html.twig to use the new component

Drupal will need to know when to load this template. To do this, we re-populate olivero/templates/navigation/menu-local-tasks.html.twig with the following standard Twig include, and pass the two variables. Note that you can use standard Twig embeds syntax here if you need to pass blocks (aka slots).

{{ include('olivero:tabs', {
  primary: primary,
  secondary: secondary
}) }}

Step 5: Populate the tabs.component.yml

The final tabs.component.yml will look like this:

$schema: https://git.drupalcode.org/project/sdc/-/raw/1.x/src/metadata.schema.json
machineName: tabs
name: Tabs
status: BETA
schemas:
  props:
    type: object
    required:
      - primary
    properties:
      primary:
        type: array
        title: primary
      secondary:
        type: array
        title: secondary
libraryDependencies:
  - core/drupal
  - core/once

To get here, we copy the example YML from the documentation, and modify it (once again, there will eventually be a Drush command to generate this). We look at the Twig template and see that there’s only two variables, primary and secondary, so we add these to the properties.  

My first attempt with the YML was this

$schema: https://git.drupalcode.org/project/sdc/-/raw/1.x/src/metadata.schema.json
machineName: tabs
name: Tabs
status: BETA
schemas:
  props:
    type: object
    required:
      - primary
    properties:
      primary:
        type: object
        title: primary
      secondary:
        type: object
        title: secondary

This failed because the primary and secondary variables are arrays – not objects. Fortunately, SDC was developed using JSON-Schema, which validates the types, and generates useful errors! This is vital in creating an excellent developer experience.

Image
Error message showing that I need to use array instead of object
SDC outputs very helpful error messages when error logging is set to verbose.

Changing the types of primary and secondary type to array and then clearing the cache solved this issue. Note that when making changes to the YML definition, you will have to clear caches.

We also need to make sure that we add the appropriate library dependencies for our component. The tabs JavaScript relies on both the once, and drupal libraries. To do this, we add the following to the bottom of our YML definition (and once again clear cache)

libraryDependencies:
  - core/drupal
  - core/once

Upon a page refresh, we see that our (currently unstyled) markup is loading! 🙌

Image
Login screen showing unstyled tabs
The tabs are unstyled, but its working!

When we inspect the component, we can see additional HTML comments added (and highlighted with a 🥘paella emoji)! Note that the emoji will randomly change per component. This is useful when you have components nested in one another!

Image
Markup for the components showing comments indicating the beginning and ending of the component
Note the paella emoji!

Step 6: Move the CSS and JavaScript

The next step is to move Olivero’s tabs.css and tabs.js files into the olivero/components/tabs directory. Note we also move the source PostCSS file, as that’s what generates the compiled CSS.

Image
Code editor showing the CSS and JS moved to the components directory

SDC will automatically look for a *.css, and .js with the name of the component. I clear caches one more time (so SDC can detect the new files) and hallelujah! I see the tabs styled as expected!

Image
Olivero login screen showing the tabs component
The login page now uses SDC to generate the tabs!

One more note here. The PostCSS file that I moved has an import that needs to be corrected to point to the correct path.

@import "../base/media-queries.pcss.css";

Once we update it, we’re all set!

@import "../../css/base/media-queries.pcss.css";

Step 7: Remove old libraries definition and add module dependency

We now need to do a little bit of cleanup. We can remove the tabs library definition from olivero/olivero.libraries.yml. We also need to declare that our theme now depends on the SDC module. Luckily, as of Drupal 8.9.0, themes can now declare dependencies on modules!

To do this, we add the following to the bottom of olivero/olivero.info.yml:

dependencies:
  - drupal:sdc

We need your help!

Single directory components are being proposed to core as an experimental module. If we want to get this into Drupal 10.1.0 (due in June), we need to get it committed by May. To get this into core, we need your help!

  1. Read the comment questioning if SDC will be used, and if it’s the best option.
  2. Add a comment with your thoughts on how you will use it, and how this will affect your development workflow.
  3. If you have time, download the Drupal 10 patch and migrate your own components to try to find additional bugs.

Wrapping up

The front-end of Drupal has made some really big strides in the last few years, and this promises to be one of the biggest leaps of all. SDC promises to transform the way that we architect our Drupal themes, and promises to re-architect the way we build pages down the road.

I hope you’re as excited about SDC as I am!

Tags

Hey you! Leave a comment!8

Seriously... I really like it when people let me know their thoughts and that they've read this.

The content of this field is kept private and will not be shown publicly.

mstrelan (not verified)

1 year 7 months ago

Great timing on this post, I had plans to look in to SDC today and this is the perfect introduction. I've followed along with this post to port the header-search component in Olivero to SDC and opened issue 3347736 with an MR and some observations.

Sounds interesting.
As a new developer learning how to create Drupal modules I am left wondering what the main benefit is of this new way of doing Module Development compared to how it is done now. As a new developer I am just beginning to understand how it is done now so I am having a bit of perplexity in grasping the benefits.
Maybe add a bit of context to this article to high light the current way vs the new way so new developers will clearly understand what this brings to the table.

Hi Michael,
Thanks for the reply. Note that this technique isn't really for module development -- it's for theming. Basically, right now the related files are scattered throughout the filesystem. This technique (that's not yet in Drupal core) allows you to easily group them into one folder.

Christian (not verified)

1 year 7 months ago

Great work! :)
Two questions: Any intent to

  1. bring this to module development
  2. support JS Modules, instead of Drupal behaviors?

Thanks!

  1. Yep, you'll be able to use SDC within modules (and themes will be able to override)
  2. There's no changes to how Drupal handles JS. The only thing different is that SDC automatically generates a library with the CSS/JS and will inject it where the component is being used.

Robert Ngo (not verified)

1 year 5 months ago

Thank you for the great article!
It seems SDC overlaps with UI Patterns. However, UI Patterns seems to be more powerful at this point (supports configurations in View mode or Views display, preprocess ...).
You've mentioned a discussion with UI Patterns' maintainers, how would you see the future of SDC and UI Patterns? Are they gonna evolve in parallel, independently?