- π©πͺGermany geek-merlin Freiburg, Germany
FTR: I agree and disagree. I'd like to have directory-local preprocess, AND altering by components.
Thought about it a while and the real problem is HOW we do preprocessing, that formatting is done in advance.
I'll think a bit more about it and write that down in a separate issue, with a title sth like "Allow templates to request formatters and other services". - πΊπΈUnited States ctrladel North Carolina, USA
@e0ipso is there an extension point that would allow another module to add preprocess hooks or the preprocess approach I described to SDC? Perhaps that'd be a decent compromise.
I don't see custom twig functions and filters as being a good enough stand in for preprocessing on the backend.
- It's not any more approachable than plugins to a frontend developer. In some ways it's worse since all the functions and filters will likely end up in one file. Taking twig tweaks class as an sample of what could be needed on a project https://git.drupalcode.org/project/twig_tweak/-/blob/3.x/src/TwigTweakEx..., I wouldn't call this super approachable for a non backend dev.
- As I mentioned in #3346715-5 π± Make twig sandboxed again, and empower themers by injecting services Closed: won't fix direct access of values in during render creates a lot of nuance and possibility for mistakes.
- Custom functions and filters reduce the portability of twig templates. The custom functions/filters have to be stubbed or fully reimplemented to render the template in a system other then Drupal. I'm mainly thinking if you're trying to render the template in a js tool like Storybook.
- Status changed to Active
almost 2 years ago 11:03am 19 April 2023 - e0ipso Can Picafort
Let's continue this discussion in the Core issue queue. Since we don't have preprocess for components now, this is not a beta blocker, but a new feature request.
- πͺπΈSpain idiaz.roncero Madrid
I was initially convinced against the need for a preprocess function for components, as I agreed with this:
We want to keep the principle of "everything to render your component is in a single directory". Hooks will defeat that.
Nevertheless, porting my first component (an implementation of Bootstrap 5 button for Radix theme), i've found that a little bit of preprocessing might be ok, at least for components where some props might have dependencies on others.
For example, this (simplified) Button component might render as a
<button>
or a<a>
HTML element.{# /** * @file * Template for a button component. * * Available config: * - type: primary | secondary | success | danger | warning | info | light | dark * - tag: button, a * - url: string * - attributes: Attributes array. */ #} {% if tag == 'a' and url is not empty %} {% set attributes = attributes.setAttribute('href', url) %} {% endif %} <{{ tag }}{{ attributes.addClass(button_classes) }}> {% block button_content %} {{ button_content }} {% endblock %} </{{ tag }}>
When rendered as an
<a>
link, you can pass aurl
prop... and this prop won't be needed if you are using a<button>
The code works, but we are missing the following features to make it perfect:
- Passing
tag: a
andurl: null
should throw an error. Right now it will render a link with nohref
. - On the opposite, passing
tag: button
andurl: whathever
should at least give a friendly warning. - Ideally, this component could benefit from some checks on wether
url
string is actually a URL.
To summarize, I think there is room for some back-end preprocessing of components, at least when it comes to establishing dependencies between props and performing some basic checks, sanitizations and validations. Many of these things can be done directly on twig but they are cumbersome or verbose, and many others might not even be possible (can Twig throw exceptions / warnings?)
Maybe the middle ground could be a preprocess function for components that doesn't let you alter the
$variables
array (is not passed by reference) but still let you perform checks, validations, and inform the user about any potential misgiving? - Passing
- πͺπΈSpain idiaz.roncero Madrid
Of course another solution for the before mentioned problem is to increase the
*.components.yml
API, but maybe that's increasing the complexity a bit too much:name: Button props: type: object properties: url: type: string title: Url description: A URL target for link buttons. # New entries for declaring dependencies between props and matching values dependencies: tag: a match: /regexp-for-URL/g
- e0ipso Can Picafort
I think most of these are solved in JSON-Schema.
Perhaps you need to define your prop with a more complex schema, if you want to leverage these advanced features.
Pseudocode:
props: element: oneOf: - # Defines a Button element with its attributes. type: object additionalProperties: false required: - tag properties: tag: type: string enum: - button - type: object additionalProperties: false required: - tag - attributes properties: tag: type: string enum: - a attributes: type: object additionalProperties: false required: - href properties: href: type: string format: uri
Would that work? You tell me :-P
- e0ipso Can Picafort
However. There was some chatter at Drupal con to augment the JSON-Schema further to allow referencing constraints. Much in the way config validation is doing it in kwalify.
- π©πͺGermany gooddev
If someone find it useful: One simple approach could be to write your custom twig function and call that inside of the SDC component. e.g. for breakpoint or similar.
{% set breakpoints = get_breakpoints() %}
- π§πͺBelgium wim leers Ghent π§πͺπͺπΊ
- π«π·France pdureau Paris
If someone find it useful: One simple approach could be to write your custom twig function and call that inside of the SDC component. e.g. for breakpoint or similar.
{% set breakpoints = get_breakpoints() %}
Careful with such an approach. The component must act as a "sandbox", getting the props & slots and rendering them, without any calls to Drupal API, and hiding such a call in a custom Twig function is not the solution. Can you send the breakpoints as a prop instead?
Hi all,
Sorry for joining in this late onto the party! I've gone through all the threads, but unfortunately seems like its a bit inconclusive till now.
I've echoed with some of the pointers mentioned above, along with that I'm a bit clouded on some other pointers as well!
First and foremost, I agree with @e0ipso's argument of not having preprocess hooks asHooks allow you to create spooky effects from far away.
and that'll certainly defeat the purpose of SDC where we'll have all the files wrapped inside a "Single Directory." With that being said several downsides can be as follows IMO:
1. On traditional way of theming that we're used to till now, have a clear sort of MVC structure having clarity on the separation of concern, where we pre-process all(mostly) the logics from a hook and just pass that values through some variables onto our twig template which helps to render it efficiently. Agglomerating all the processing and computation will blur out that separation of concern by clubbing 'V' and 'C' of MVC. Which in turn defeats the purpose of SDC, to incorporate better separation of concern.
2. On moving all pre-processing/computation on twig might affect its rendering time as well as more prone towards layout shifts in case of delayed output, as twig might not be that much efficient for handling complex computation as that of Php. Hence it'll negatively impact performance and UX.
3. And as @ctrladel mentioned
direct access of values in during render creates a lot of nuance and possibility for mistakes.
in terms of input sanitization, efficient caching issues for frequently changing data, which otherwise might lead to rendering stale data and security concerns.
4. Along with @ctrladel's pointer:
I don't see custom twig functions and filters as being a good enough stand in for preprocessing on the backend.
I'd like to add, Incorporating custom twig functions and filters for every complex custom operations will be a tedious task for FE devs as well as that'll require a steep learning curve,otherwise it'll rather increase the dependency towards BE devs, which will also defeat one of the main aspect of SDC which promises to empower FE devs and fasten their workflow by independently developing and building components irrespective of the BE architecture and intervention of BE devs.
5. Putting all such preprocess logics inside twigs will not only reduce its portability but also hampers the readability. Though it was not that much great from the beginning itself IMO, but now with more complex logic and custom implementation the template files are intended to grow enormously, leading to very less readability. Perhaps that can be mitigated by integrating Storybook like tools for our SDC components, then we can use autodocs like feature to prepare nice and easy documentation for our components which can be readable by end users also, though that integration is quite a bit tricky (I had to scratch my head for couple of days to integrate SB with SDC properly :( )
6. I'm a bit curious on the aspect, if and when we need to build quite a few components which apparently seems to be specific to that of our BE architecture and pulls values that can only be computed by making DB queries in a custom fashion, how SDC will bring independence to FE devs in that scenario?
Even on the previous ways, we had to be dependent to BE devs to pass in those values, so I won't mind hearing that as of now that sort of decoupling is not possible through SDC :)Please feel free to correct me if I'm having any wrong perception about these, I'm pretty new to SDC.
TIA :)
My 2 cents, replying to the original use case explained in #2 (e.g. preprocess things for an external url).
Whenever I need a preprocess I use a custom Twig function / Twig filter.
Example below shows a custom filter
is_external
.<a{{ attributes }}> {{ text }} {% if url|is_external %} {# β Print an icon here β #} {% endif %} </a>
See this post for reference about how to create a custom Twig filter/function.
Unless I'm wrong, this resolves the needs of providing a preprocess function.
We can 1) redeclare the template of a SDC in our theme and 2) use a custom filter in this new Twig for the specific use case.
- πΊπΈUnited States adamzimmermann
I'm working on a new project where they have built their own version of SDC with a slightly different feature set, but a similar ideology. When I saw it I thought of this issue and wanted to share the approach.
We are integrating with generic Twig templates from a component library, and needed a way to map data to specific variables with specific formatting/values in a way that kept that mapping connected to the Twig template it was used with.
The use of a custom plugin system was the solution they architected. Each component/Twig template has an associated plugin class that does the mapping, but it could be used for preprocessing too.
I agree with @CtrlADel here. We should not have unrelated files change how the component renders, we'd be violating the Single in SDC.
I would second this, and it seems that I'm sharing something similar to the plugin idea shared above in #3321203-18: Have a way to implement the a preprocess function per each SDC component (ideally in the same folder) β .
Hope this helps.
Hi @adamzimmermann,
Incorporating a custom plugin system apparently sounds good but I'm afraid that it'll be too complex to do so for each and every component individually, and certainly be an overkill for small to medium scale projects.
FE devs might not be that much familiar with creation and implementation of custom plugin system and its corresponding management and discovery methods, hence they've to defer to BE devs in such scenarios, which nullifies the independence!Though I've seen that being implemented on certain themes like Civic theme or so, but those are huge in scale. And its quite difficult for FE devs to start off with such implementation and extending the same on their projects in custom way.
I was actually looking for a simpler solution, stressing upon the independence factor!
Thanks :)
- π«π·France pdureau Paris
Whenever I need a preprocess I use a custom Twig function / Twig filter.
Example below shows a custom filter is_external.This
is_external
Twig function may be OK because it doesn't seem to need to call Drupal DB, or API, or config... Just parsing the URL string (withparse_url
or other) may be enough.Is it really the case, Matthieu?
Yes and no, Pierre.
I use the
requestContext
to grab the base URL - as per this snippet as example - but I could certainly get it differently and thus avoid any dependencies.- πΊπΈUnited States freelock Seattle
Hi,
I'm hitting a need for something like this now, trying to create a menu_item SDC for a megamenu.
Using Menu Item Extras, you can add fields to menu_item_content entities, and then use layout builder on top-level menu items to manage the contents of each megamenu pane (as fields on the top level menu items).
We've done this without SDCs. But now trying to make an SDC, we've created a menu_menu SDC and a menu_item SDC, and placed the menu_menu SDC using UI Patterns, as a block. So far so good, we have our menu. But no menu_item_content entity.
Menu Item Extras makes use of preprocess functions to render child links as appropriate -- but these never get called when you hand off to an SDC, and I'm really struggling to figure out how to get them in there. The items do have a "below" array of child items, and each item does have an "original_link" value that returns a plugin -- not the entity, but just a plugin. That does have an id of "menu_item_content:" so what I'm currently doing is extracting that UUID, and then using Twig Tweak's "drupal_entity" to render the actual link.
I think I can get this to work -- but it feels a bit nasty and introduces other dependencies (Twig Tweak). The "drupal_entity" call goes back to the theme layer, calls the theme hook, which then needs the child SDC added to the template.
I would much rather simply call the child SDC (menu_item) from within the parent, but I don't have any access to the menu_item_content entity!
A preprocess function of some kind would allow me to add a reference to the actual entity inside the items array, and then this wouldn't have to jump out to the theme layer and back for each item.
Is there a better way to solve this? Was also thinking I could possibly decorate whatever is building the menu tree to add those references.
- π«π·France pdureau Paris
Hi @freelock,
But now trying to make an SDC, we've created a menu_menu SDC and a menu_item SDC, and placed the menu_menu SDC using UI Patterns, as a block. So far so good, we have our menu.
Nice, an user of UI Patterns π I hope you enjoy using it.
but it feels a bit nasty and introduces other dependencies (Twig Tweak).
Twig Tweak is nasty :) Its philosophy is the reverse of SDC one.
I would much rather simply call the child SDC (menu_item) from within the parent, but I don't have any access to the menu_item_content entity!
Are you using a slot in
menu_menu
to injectmenu_item
components and a slot inmenu_item
to inject menu_menu components?Maybe not because it seems you are manipulating a prop structure here:
The items do have a "below" array of child items...
Can you share with us the definition and the template of the 2 components?
- πΊπΈUnited States freelock Seattle
As discussed at DrupalCon, we found a different approach to solving our particular issue -- @pdureau created a MenuContentSource plugin extending the MenuContent plugin which could then provide the link content we were missing.
So our need was solvable using UI Patterns plugins instead of a preprocess function.
- πΊπΈUnited States dalemoore
@freelock is this actually in UI Patterns now or is it something you implemented in custom code? I am going to need to do something very similar soon to create a mega menu component of sorts. Thanks!
- πΊπΈUnited States freelock Seattle
@dalemoore that's exactly what I was trying to do. It uses a plugin to get child items, which is not currently part of UI Patterns.
I've gone ahead and bundled it up into a module - MegaMenu SDC β . There is a bug in UI Patterns that currently requires a patch - β¨ Cannot subclass MenuSource plugin due to private method getMenuItem() Active .
- π«π·France pdureau Paris
Great, so we can come back to the initial subject: Do we need preprocesses for SDC?
2 years and an half later, I have still not found a good use cases for such preprocesses, and I believe Mateu's initial intuition was right.
Preprocesses have many issues, including:
t- hey are triggered very late, just before the template is used and they are often used instead of an other earlier more suitable API (field formatter plugin, text filter plugin, view plugin...)
- doing so, they can be in conflict with the current config state. A View or an Entity View Display, for example, can configured in a way, but displayed in a different way. This is confusing.
- when used in a Drupal theme instead of a Drupal module, they often break the expected decoupling between business logic / UI logic
Moreover, if we modernize the Render API by deprecating ThemeManager::render() π± Unify & simplify render & theme system: component-based rendering (enables pattern library, style guides, interface previews, client-side re-rendering) Active , preprocesses will be deprecated everywhere. So it may be silly to add a new one now.
- π©πͺGermany mxh Offenburg
One use case I recently had where a preprocess function would have helped me: I created an SDC that renders a third party provider JS widget. In order to connect to the third party I had to provide some configurable global credential settings. Either I wanted to hide that from the component itself to reduce its complexit so it would have loaded them by itself from a global config object, or when I provide it as SDC prop for credentials I wish I could have a way to validate that these were valid credentials.
- π«π·France pdureau Paris
I created an SDC that renders a third party provider JS widget. In order to connect to the third party I had to provide some configurable global credential settings. Either I wanted to hide that from the component itself to reduce its complexit so it would have loaded them by itself from a global config object, or when I provide it as SDC prop for credentials I wish I could have a way to validate that these were valid credentials.
Could the credentials you want to validate be expressed using JSON schema? JSON schema is a powerful validation tool, and it may be enough for string validation.
If JSON schema is not enough because you need logic, it will be business logic, not UI logic, so it doesn't belong to the component template.
So, where are you calling the component renderable? From a block plugin ? A field formatter plugin ? A layout plugin ? This is where you need to put the business logic, because in Drupal business implementation emerges from plugins attachments and configurations in config entities.
- π©πͺGermany mxh Offenburg
So, where are you calling the component renderable?
In my case it's in at least two different places, a field formatter plugin plus as an ECA render action plugin. Without the credentials provided in a certain expected format, the whole component is basically unusable. Yes I could call the validation logic outside of the component, but ideally the code should not be duplicated. It would be great being able to just instruct "At this place render this component", as the field formatter and ECA plugin don't really represent business logic as well, because they just exist to finally render the component. Also the settings to provide are defined site-wide, not per plugin configuration.
- π«π·France pdureau Paris
Also the settings to provide are defined site-wide, not per plugin configuration.
So, this is the place were the validation logic is expected, isn't it?
- π©πͺGermany mxh Offenburg
So, this is the place were the validation logic is expected, isn't it?
Yes, in the moment credentials are being entered, validation is happening where possible.
- π«π·France pdureau Paris
which could be avoided if the component itself is just able to get the right credentials, at least as a default behavior.
Is it possible to express this validation logic only with JSON schema and (native, no custom extension) Twig templating?
- π©πͺGermany mxh Offenburg
Is it possible to express this validation logic only with JSON schema and (native, no custom extension) Twig templating?
The first question is whether I can hide this detail in the component definition. I guess I'm not able to read out the credentials from where it's provided without a custom Twig extension. And as long I don't want to go that route, I'd then need to have a mandatory component prop to pass along the credentials. Which then means every consuming instance needs to first take care of properly receiving the right credentials.
- π©πͺGermany mxh Offenburg
Just read again through this issue and documentation β . It seems the concept of SDCs include that it should not call any Drupal API whatsoever. But I'm a bit confused about the usefulness of an SDC when I cannot put everything in there that is needed to work properly. I understand SDC are meant for frontend concerns only, which generally are not supposed to include things like PHP classes that contain business logic. But whenever such things are needed, duplication and fragmentation of such components start again. I wish this concept could've been a bit more "permissive", letting developers themselves decide what is allowed to put in a component.
One point I noticed in the documentation is the following sentence:
Within SDC, all files necessary to render the component are grouped together in a single directory (hence the name). This includes Twig, YAML, and optional CSS, JavaScript, etc.
- That "etc." kinda makes me curious, whether that may also include a PHP helper class for example ;)
- π«π·France pdureau Paris
I guess I'm not able to read out the credentials from where it's provided without a custom Twig extension.
You have to send them as a prop.
It seems the concept of SDCs include that it should not call any Drupal API whatsoever.
Exactly, SDC are stateless, "dumb", opaque boxes of UI logic. They receive props and slots and they return a renderable, without pulling in other data.
I understand SDC are meant for frontend concerns only, which generally are not supposed to include things like PHP classes that contain business logic. But whenever such things are needed, duplication and fragmentation of such components start again.
If you want to add business logic, SDC is not the good tool for you. UI components are business agnostic to:
- be owned by a front developers and talked about with designers
- be shareable and reusable in a single project and between projects
- avoid (as much as we can) security issues, performance issues, deprecation issues...
- ...
I wish this concept could've been a bit more "permissive", letting developers themselves decide what is allowed to put in a component.
Component authors can allow (nearly) any data to be injected in a component, through the slots (for renderables) or props (expressed with JSON schema, so with primitives). For the credentials you need, you may need a prop.
- πΊπΈUnited States nicxvan
We are working on over hauling preprocess functions to be oop for modules at least, it may be an nice follow up to check for the hook attributes once that is in.
- π©πͺGermany mxh Offenburg
Thanks for taking your time to explain, it gives a good understanding what's being tried to achieved.
When being shared across projects I now need to separately ship the logic for getting the credentials. So I'm basically not talking about the ability to call an external preprocess hook function, instead being able to call PHP logic that is resided inside the component itself. It's possible in SFC β and it makes sense.
It would have been a perfect fit to have the logic resided within the component in order to reduce complexity of understanding the component definition. For frontend developers (I mean people who also create field formatters, block plugins etc.) they wouldn't be interested in "When I want to use this component, how do I now properly pass the credentials to it (everytime I need to use it)?".
In my opinion this is not a good decision of being that restrictive. However I understand the arguments put in #48 and I wish good luck achieving these set goals. For me components with that current state are not that much useful because of this limitation as I cannot hide these things to reduce complexity. For my particular example, I think I'll just stick with the comment approach for now (just describing how to get the credentials in the component definition file).