Unify & simplify render & theme system: component-based rendering (enables pattern library, style guides, interface previews, client-side re-rendering)

Created on 7 April 2016, almost 9 years ago
Updated 9 August 2024, 8 months ago

Problem/Motivation

Drupal's render & theme system are too complex to use. Let's improve this.

Goals

  1. Improve the TX by un-WTF-ifying the theme and render system, which is currently a maze of:
    • hook_theme() (with variables vs render element), preprocess hooks, theme suggestions and many more related hooks — all tied together using the theme registry
    • #type (@RenderElement plugins) vs. #theme, which are kind of the same thing but not really: when to use which is unclear
    • callback buffet: #pre_render, #post_render, #lazy_builder
    • mysterious keys in enormous render arrays AKA render arrays of DOOM
    • #render_children, #theme_wrappers and friends determining where the render system morphs into the theme system and back again
    • different systems calling each other: understanding the entire flow is nearly impossible, and probably rivals the complexity of some simpler biological organisms
    • — @c4rl

    These have been known problems for years! First there was Form API, then Render API sprouted from that, and all the while there was the theme system, but starting in Drupal 7, the Render API and the theme system got deeply, deeply intertwined. Drupal 8 actually made it slightly better, but not enough.

  2. Improve the TX for non-JS front end people ():
    • automatically generated pattern library (== all #type/@RenderElements — but without having to know those details)
    • automatically generated style guides for every theme (== pattern library with the theme's overrides/extensions applied)

    in other words: bring style guide-driven development to Drupal as a default rather than a labor-intensive, hacked-on after-thought (without the need to duplicate markup and thus keep them in sync).

  3. Long-term: make it possible to reuse templates on the client-side ().
  4. Retain compatibility with the existing Render & Theme systems. Allow for a gradual transition.
  5. Support interface previews: Interface previews/skeleton screens through optional "preview" or "placeholder" templates Fixed

Requirements

To address all of those goals, I believe a component library can be the gateway to a solution. It can be, if the following requirements are met:

  1. components are not deeply tied to Drupal, and in fact, can be developed independently of Drupal — this is how we can guarantee simplicity and ease of getting started: we actively prevent components from being tightly coupled to Drupal code
  2. components have:
    1. markup: *.html.twig (Twig template — which may include some logic to process received variables, just like in Drupal 8)
    2. assets: CSS, JS files
    3. metadata: YAML file

    Nothing else.

  3. modules and themes can specify components — modules can define patterns that any other module can use, themes can specify theme-specific components
  4. components are defined in a simple directory structure:
    <extension> (module or theme)
     |- components
        |- <component name>
           |- <component name>.yml
           |- <component name>.twig
    

    Concrete example:

    core/modules/system      
     |- components           
        |- label             
           |- label.yml      
           |- label.html.twig
           |- label.css
    cores/themes/classy      
     |- components           
        |- label             
           |- label.yml      
           |- label.html.twig
           |- ajaxified-label.js
    
  5. The YAML file specifies:
    1. the variables (inputs) of the component. For each variable:
      • type: only A) primitives such as string/integer/bool, B) arrays of primitives such as string[], C) other components: component or components to slot in other components (perhaps even component:<name> to only allow certain components ) — this enables 3 big wins:
        1. improved TX: type validation, to avoid weird bugs
        2. improved TX: no messy Doxygen/PHPDoc comments repeated in both templates and preprocess functions, and all overrides of either of those
        3. client-side re-rendering

        (Also: having type specifications in Twig templates instead is A) undesirable, B) quite likely impossible, C) quite likely impossible to parse without refactoring Twig, D) would pick up calculated variables.)

      • description
      • default value, if any
      • example value (to be used in pattern library & style guide)
      • preview value (to be used when the component's data is not available yet, because it's being used for an interface preview)
    2. documentation: purpose, when to use, how to use, accessibility, related links — in other words: information to show in the pattern library & style guides
    3. less important metadata: human-readable name, which other components this component includes, whether this component supports interface previews …
  6. The Twig template (*.html.twig) performs all the necessary processing of the variables received, this ensures we don't depend on preprocess functions. This removes the need for front-end developers to dive into PHP.
  7. components can be extended: add attributes, modify markup, and so on
  8. components can be composed: combine multiple components to create a new component
  9. To allow for a gradual transition, we cannot fully back away from render arrays nor the existing theme system. At best, we'll be able to remove render arrays, the current render system and the current theme system in Drupal 9.

Proposed resolution — or: how to transition

Long-form rationale

This part is less precise, but tells a hopefully helpful story of the two most important considerations: how we ended up with the current painful system (recommended reading: http://hackingdistributed.com/2016/04/05/how-software-gets-bloated/) and how we can at the same time start to make it possible to integrate with JS more easily.
And, how, funny enough, those actually need the same fundamental thing: simplicity.

JS: web apps vs web sites

"Apps" are the hot thing in software this decade. But building native apps for every platform is very expensive. So "web apps" are a thing: build once, run anywhere, deploy instantly.

Web apps must be written in JS (or at least compiled to JS), so, consequently, JavaScript is the hot language of this decade. As a result, we've seen enormous investments in JS: Node.js, browser engines' JS interpreters have become incredibly fast, and … JS-based frameworks. From jQuery UI to Angular to Ember to React to …

Everybody wants "an app". And so quite often what would have been a web site a few years ago now is a "web app". And since web apps are written in JS, this means Drupal is less likely to be chosen for those scenarios.

(I personally think the distinction between the two can be made by determining whether the business logic happens in JS on the client or on the server. Only if it's on the client, it is a "web app".)

But that doesn't mean there's no more need for "regular" web sites anymore. It doesn't make sense to build an app just to present hyperlinked information… because for that, we already have an app: the browser. We just need to feed it web sites: documents of structured content, with excellent accessibility & usability, beautiful layout & typography and most importantly: great information architecture.

(And indeed, there is a very, very blurry border between "web sites" and "web apps". I'm just trying to paint a picture of the two extremes, where the world of the web is still finding an equilibrium somewhere in between. And of course, in some cases, it makes sense to have parts on the client side and other parts on the server side.)

Drupal 8 is even better at building web sites than prior versions.

But Drupal is getting pressure to also support "web apps".

So we have a tension between wanting to improve Drupal for what it has traditionally done (improve front-end experience by improving its templates, its markup, removing preprocess functions, etc) and making it possible to build more app-like experiences with Drupal.
But Drupal is not written in JS. Perhaps at some point in the future, there will be tight integration. But we have no idea what that would look like, if it will happen.

Getting ready for the future, and facing the past

However, even with the current system, we have long-standing problems. Extremely frustrated reports go back to at least 2011. The theme system and render system are deeply intertwined. It's very confusing. The experience even to build just web sites (not web apps) is far from ideal. Drupal 7 introduced the render system, but let's not forget its origin: Form API. The render system was originally just for forms, but it's since been generalized to be used for all rendering.

  • Drupal 6: Form API + theme system
  • Drupal 7: Form API + theme system, and both depend on the render system

Drupal 8 has made big steps forward: Twig, removal of most preprocess functions, much cleaner templates, #theme is gone in favor of #type (but not at all completely…), no longer necessary to to sometimes call render() in templates …

Unfortunately, as soon as the render and theme system interact, it's still painful.

We started working on an experiment at Acquia, where we worked with the Angular & Ember.js teams to do a prototype of what a better commenting experience for Drupal would look like. A reference implementation in Drupal using the AJAX system ("the Drupal way") versus what they would do. They had to reuse our Twig templates. A big problem there was the fact that just about every template has "blobs of HTML": {{ content }}, which actually contained the majority of the interesting stuff. And those blobs of HTML are… yes, render arrays!

Of course, no JS is ever going to be able to render render arrays, because they're so deeply intertwined with PHP code. And it's impossible to automatically determine which Twig templates are associated with every subtree in a render array.

So this makes it effectively impossible to reuse our Twig templates in JS. Ideally, that would be possible, it'd make Drupal better prepared for the future. It's better to at least have that possibility than not to.

However, even today, and in fact, in years past, this very same problem has been a major frustration for themers: they could only go so far with achieving what they needed to achieve by creating/modifying templates and writing preprocess functions. Very often, they would need to dive deep into render arrays and implement lots of hooks.

Imagine if that weren't the case, and we'd have templates all the way down, rather than enormous blobs of the resulting HTML being defined in render arrays. Imagine if the resulting HTML was wholly based on templates. Templates all the way down. Power to the themers.

And imagine that rather than ill-defined variables, they'd actually have type hints. And validation. And examples. And actually usable documentation rather than incomplete (and duplicated) docblocks. Together, that would allow us to automatically generate a style guide/pattern library.

As a bonus, client-side (re-)rendering becomes possible.

References

A close-to-comprehensive list of the references I've used to write/build the above.

Remaining tasks

Lots.

User interface changes

None.

API changes

None, only additions.

Data model changes

None, only additions.

🌱 Plan
Status

Active

Component

Idea

Created by

🇧🇪Belgium wim leers Ghent 🇧🇪🇪🇺

Live updates comments and jobs are added and updated live.
  • DrupalWTF

    Worse Than Failure. Approximates the unpleasant remark made by Drupal developers when they first encounter a particular (mis)feature.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • 🇺🇸United States dalemoore

    I wonder where this fits now that we have SDC, and Experience Builder is in early development?

  • 🇳🇿New Zealand quietone

    The Ideas project is being deprecated. This issue is moved to the Drupal project. Check that the selected component is correct. Also, add the relevant tags, especially any 'needs manager review' tags.

  • 🇫🇷France pdureau Paris

    I wonder where this fits now that we have SDC, and Experience Builder is in early development?

    We still need to work on this because:

    • SDC is addressing only UI components, an important part of a modern Render API but not everything
    • Experience Builder is a display builder, so an higher level tool. We need to address the lower levels

    Nearly 5 years after my previous comment, here we are.

    Current status

    Let's not forget the Render API is great:

    • Declarative: Easy to type. (de)Serializable if clean.
    • Easy nesting: The Virtual DOM of Drupal. We are building a tree.
    • Data bubbling: Declare locally, impact globally
    • Asset libraries management: Our beloved libraries.yml
    • Clever caching: Context, tags, keys…

    ❤️ Building display by assembling configurable plugins returning renderables.

    However, the renderables themselves are an issue.

    They are too many of them and they are of 3 kinds:

    • 35 render elements in Core:
      $ grep -hoEr "#type' => '(\S+)'" core/ --exclude-dir tests | sort | uniq -c | sort -nr
      138 #type' => 'details'
        94 #type' => 'container'
        93 #type' => 'link'
        68 #type' => 'table'
        47 #type' => 'actions'
        36 #type' => 'inline_template'
        27 #type' => 'fieldset'
        23 #type' => 'html_tag'
        21 #type' => 'status_messages'
        14 #type' => 'pager'
    • 99 theme hooks in Core:
      $ grep -hoEr "#theme' => '(\S+)'" core/ --exclude-dir tests | grep -v "__" | sort | uniq -c | sort -nr
      84 #theme' => 'item_list'
      21 #theme' => 'username'
      16 #theme' => 'image'
        7 #theme' => 'status_messages'
        7 #theme' => 'image_style'
        6 #theme' => 'table'
        6 #theme' => 'links'
        5 #theme' => 'update_version'
        5 #theme' => 'indentation'
        5 #theme' => 'file_upload_help'
        …
      
    • 2 special ones:
         1016 '#markup'
             85 '#plain_text'
      

    😱 135 renderables to learn/use? This is the issue.

    What can we remove?

    We have SDC for UI components, so:

    • #type => component can replace a lot of hook_theme, the one describing UI components: breadcrumb, progress_bar, links...
    • We can also replace the 34 render elements which are wrapper around hook_theme (because what they do, adding asset libraries, variable typing... is already done by SDC) we removed : table, status_messages, pager...

    The hook_theme not suitable for for conversion to SDC are often annoying wrappers we can get rid of anyway : block, node, view, field

    The few remaining render elements can't be expressed as UI components:

    • some must be kept because they represent other design systems artefacts (icons...) or low level bricks (HTML elements, placeholders...)
    • some may (not found in my today look) be shortcut to Drupal callables returning renderables, it is better to explicitly call the callable (service, function, method..) instead

    Deprecation of ThemeManager::render()

    If all template file based renderable are managed by SDC and because SDC is not passing through the ThemeManager, we dont need the template loading feature of the ThemeManager and we get remove it to get roi of the old confusing stuff:

    • Theme wrappers: Confusing and useless. You can always use an upper level instead.
    • Templates suggestions: Not discoverable. Messy. Blur the business / view separation.
    • Preprocess hooks: Risky. Unfriendly. Blur the business / view separation. Unpredictable order of execution.

    Conclusion

    We can have an equally useful Render API with less than 10 renderables (not counting Form API):

    • Design systems artefacts:
      • #type => component: for UI components
      • #type => icon: for UI components
    • Lower level bricks:
      • #type => inline_template
      • #type => html_tag : for HTML elements
      • #type => link
      • #markup
      • #plain_text
      • #lazy_builder

      Keeping the beloved universal render properties: #attached, #cache... and adding some related to design systems: #styles, #tokens, #mode...

      And now we have an purely design/UI based Render API!

      If we do that, step by step, deprecating slowly, not breaking anything, the display building tools from Core and Contrib will have a simpler implementation and will have an easier time introducing new features.

      To play safe, we can also introduce a new Render API alongside the existing, using @ instead of # for example:

      • #type => component, #component= "foo:bar" @component => "foo:bar"
      • #type => icon@icon => []
      • #type => inline_template, #template => "foo"@template => "foo"
      • #type => html_tag, #tag => "foo"
        @element => "foo"</li>
            <li><code>#type =>link

        @link

      • #markup@markup</li>
      • #plain_text@plain_text
      • #lazy_builder@lazy_builder
  • 🇺🇸United States bkline Virginia

    I'm going to take the optimism path and assume that moving this from "ideas" to a real core issue is a promotion which increases its visibility and chances for success. 😅

  • 🇬🇧United Kingdom catch

    I don't fully understand #type => component or how to entirely get rid of preprocess via SDCs yet (would be great to get separate issues for those opened maybe?), but everything else in #99 I'm 100% agreed with, and I would love to get rid of preprocess and as many (all?) theme hooks as possible.

    Adding three concrete active issues that could use reviews/help.

    There are also a lot of open child issues of #1804614: [meta] Consolidate similar twig templates and properly use theme suggestions in core still.

  • 🇨🇭Switzerland znerol

    how to entirely get rid of preprocess via SDCs yet

    I'm more likely to implement a twig extension instead of a preprocess function nowadays. Maybe it would be enough to just improve the DX around twig extensions. E.g., attributes:

    #[TwigFilter('without')]
    public function withoutFilter($element, ...$args) {
    // [...]
    }
    
    #[TwigFunction('url', options: ['is_safe_callback' => Foo::isUrlGenerationSafe])]
    public function getUrl($name, $parameters = [], $options = []) {
    // [...]
    }
    
    
  • 🇫🇷France pdureau Paris

    but everything else in #99 I'm 100% agreed

    @catch in #106 ? ;)

    I don't fully understand #type => component or how to entirely get rid of preprocess via SDCs yet

    In UI Suite community (not only the contributors in drupal.org, but also the agencies doing projects I am interacting with), we are not using preprocess hooks anymore, for many years, except for forms. I don't really know why it is a less an issue for us. Maybe because we fully switched to a design system + display builder paradigm. It is worth investigating and writing that down.

    Adding three concrete active issues that could use reviews/help.

    Thanks you. I will have a look after Atlanta.

    I'm more likely to implement a twig extension instead of a preprocess function nowadays.

    Twig extensions have a high risk of being harmful:

    • Written in PHP, they may hide some business logic specific to a specific project, or calls to CMS API. A SDC component is just a "dumb" piece of UI logic receiving already resolved data.
    • Written in PHP, they may introduce PHP in a Drupal theme (which we cant to avoid, a theme must be a front dev friendly place) or a dependency to module.
    • Instead of fixing the root cause (why this data is not OK to be injected in my template? We are moving the problematic mechanism elsewhere
  • 🇬🇧United Kingdom catch

    In UI Suite community (not only the contributors in drupal.org, but also the agencies doing projects I am interacting with), we are not using preprocess hooks anymore, for many years, except for forms. I don't really know why it is a less an issue for us.

    The projects might not be using them directly, but there are dozens/hundreds of preprocess hooks running on those projects via core and contrib modules. https://api.drupal.org/api/drupal/core%21modules%21node%21node.module/fu... is a monster just by itself. Removing just one variable is non-trivial, although at least we have deprecation support now.

  • 🇫🇷France pdureau Paris

    The projects might not be using them directly, but there are dozens/hundreds of preprocess hooks running on those projects via core and contrib modules.

    Of course.

  • 🇫🇷France pdureau Paris

    Also, it may be the opportunity to challenge the various additions Drupal made to Twig last 10 years.

    Today, in my very personal and humble opinion, I see only 5 additions which will stay relevant if we move to SDC, decoupling between UI logic and business logic, and design system philosophy:

    • the PrintNode visitor to send all printed variable to the renderer service. That's the core of Drupal Twig rendering, it works very well and must be kept.
    • Attribute object and the related create_attribute() function, but I hope this issue will allow us to get rid of it one of those days: HTML attributes as Twig mappings instead of PHP obejcts Active
    • add_class() & set_attribute() filters, they make sense used with the SDC slots.
    • the new icon() function for the Icon API. It follows Twig philosophy of using functions to generate printable data
    • the translations helpers (t function, trans tag). Important feature for a CMS. Why not proposing this to upstream Twig?

    So, other Drupal additions (clean_id(), attach_library(), link(), |add_suggestion()...) seems OK to be deprecated to me, once we have "cleaned" the Render API. I would happy to discuss this.

Production build 0.71.5 2024