Main objective
We want to simplify the front-end development workflow, and improve maintainability of core and contrib themes.
For that we will:
- Reduce the number of framework implementation details required to put templated HTML, CSS, and JS in a Drupal page.
- Define explicit component APIs, and provide a methodology to replace a component provided upstream (in a parent theme or module).
Goals
- HTML markup in core components can be changed without breaking BC.
- CSS and JS for a component is scoped to the component and automatically attached to it.
- CSS and JS for a component provided by core can be changed without breaking BC.
- Components can be provided by any module and/or theme, and then overridden explicitly or using theme specificity rules.
- All the code necessary to render a component is in the component directory.
- Components declare their props explicitly. Component props are the API of the component.
- Rendering a component in Twig uses the familiar include/embed syntax, with support for Twig blocks (slots).
- Facilitate the implementation of component libraries for design systems of themes.
- Provide an optional way to document components.
Architecture
The main concept is the component directory. Everything related to the component will be stored in that directory.
A directory becomes a component directory if it meets the following criteria:
- Contains a component definition file: my-component.component.yml
- Contains, at least, a Twig file with the name of the component: my-component.twig.
- It is stored in a module or a theme directory under "templates/components". Components directories can reside in nested directories for organization, but never inside another component directory.
Image may be NSFW.
Clik here to view.
Discovery
Components are plugins. The parsed component definition file (my-component.component.yml
) constitutes the plugin definition. The plugin discovery will scan all modules and themes that contain a /templates/components
directory recursively using the existing YamlDirectoryDiscovery
(using a recursive filtered iterator).
Automatic libraries
Each component will declare a library automatically with the CSS and JS assets contained in the component directory. This library will be attached to the page automatically when the component is rendered. This means that front-end developers no longer need to learn about declaring libraries, and how to attach them to the page.
It will be possible to override the automatically defined libraries when more control over its definition is needed.
Component negotiation
Including a component in a Twig template looks like this:
{{ include('my-component', {
prop1: content.field_foo
}) }}
When Drupal encounters this it will search for all the components with a machine name 'my-component'. The negotiation will choose the component with the following priority rules.
- The component is in the active theme.
- The component is in one of the base themes of the active theme. The closer in the inheritance chain, the more priority.
- The component is in a module.
This intentionally matches the priority rules used for templates sharing the same name.
If the developer wants to skip the negotiation step, they can include the provider in the template name:
{{ include('olivero:my-component', {
prop1: content.field_foo
}) }}
Component replacement
Themes and modules can replace components provided by other themes and modules.
Themes can replace a component by copy & paste of the original component in a location that has higher priority (see Component negotiation). Themes can replace components provided either by modules or by themes which they extend.
Modules can replace components provided by other modules by copy & paste of the original component and explicitly stating it with the "replaces"
property in the component definition file (my-component.component.yml
). Example, the node module wants to replace "my-component"
originally provided by system
. node/templates/components/foo/bar/my-component/my-component.component.yml
needs to contain: replaces: 'system:my-component'
Additional considerations
We decided against always prefixing components with the provider. This will better reflect the developer's decision of embedding a component leveraging the negotiation rules. Adding the provider in front will have the effect of including the component as defined in the specified provider.
We thought it would be confusing to allow writing {{ include('system:my-component') }}
yet render the my-component
as defined in a sub-theme of Olivero, so we decided against it.
No prefix when including/embedding means "let the negotiation decide the component to render". Adding the prefix will skip negotiation entirely.
Edge cases
PHP code
Some PHP code is very relevant to how a component renders. As a general rule, we want all the code pertinent to how a component renders encapsulated in the component directory.
This means that we will support the my-component.php file inside of the component directory. We will only allow certain "component hooks" in this file. Component hooks are very similar to regular hooks but they can only affect their components.
Initially we will only allow pre-process component hooks.
Multiple module replacements
If two different modules replace the same component, the module with the lowest weight will take precedence over the one with the highest priority. If two modules have the same priority, then the one that comes first alphabetically will take precedence. This is not ideal, yet common in the framework.
Image may be NSFW.
Clik here to view.
Follow-up tasks
- Per #3313520-26: Single directory components in core and #3313520-28: Single directory components in core, create a child issue for discussing incorporation of this concept into entity bundle classes per #3313520-25: Single directory components in core.