Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AssetMapper] load of specific CSS files #51329

Closed
Nayte91 opened this issue Aug 9, 2023 · 8 comments
Closed

[AssetMapper] load of specific CSS files #51329

Nayte91 opened this issue Aug 9, 2023 · 8 comments

Comments

@Nayte91
Copy link

Nayte91 commented Aug 9, 2023

Description

Please receive consider this idea: because we expose all our CSS files through the head tag now, we should be able to adapt them to the current page content..

Typical usecase:

  • Imagine a homepage with 15 products to sold, so 15 article cards with 15 "view this product" buttons.
  • Then a product page, with JUST a "let me buy this!" button (weird page but ok).
  • there's a product_card.css and a view_product_button.css files in assets/css.

The goal is to charge the CSS for the cards' and view buttons' homepage only for the homepage, and not to charge the "buy" button CSS here. And on the product page, having only the "buy" button CSS link tag.
We can achieve such a thing with {% extends stylesheets %} blocks, but it's hard to keep track of what will be charged 15 times and if I have to keep parent() or not in specific situations. Let's do this automatically!

  • It reduces the loads for your first page.
  • It reduces the bandwidth used globally if someone doesn't go on every pages.
  • It reduces the memory consumed for each page as we stop loading unnecessary CSS.
  • The bigger is your project, the larger are your saves with this, because you will have a lot of specific pages with specific elements.
  • It improves DX as no one has to optimize assets loading when coding; Just create the new template, link to it a CSS file with 'stylized', and don't think about it ever again.
  • It's compatible with twig templates or partial templates (_foo_section.html.twig), with twig component or live components.

It may be quite simple to do on the twig side, as I can think of a twig custom function to achieve that; But I don't know how to combine this with the assetMapper asset('hashed.version.of.css') and keep this working after bin/console asset-map:compile. Maybe hard, maybe simple?

Please take few seconds to mind it, as I may not be the only one to atomize my css into multiple, component based files. Once again, assetMapper opens a paradigm where multiple CSS files is possible without impacting performances, but rather improves them.

Have a nice day!

Example

Imagine a new {% stylized %} statement or function, that you will put at beginning of all your templates. Let's say article_card.html.twig:

{% stylized asset('foo/article_card.css') %} (or {% stylized %} only with we keep the same folder structure)

you only declare your CSS here; not anymore in layout or anywhere else. It's convenient for the developer as he never forgets to add the 'use' in a js or css bootstraper somewhere; he just adds 'stylized' on top of his template and here we go.

The goal for twig & assetMapper? when a page loads:

  1. Make the inventory of the 'stylized' calls for this page,
  2. Singleton-ize the CSS called multiple times,
  3. Inject in head tag only the required link tags asset-mapper-ized.
    Because assetMapper opens a way where we keep our CSS atomized in multiple files, it's really easier to aim for required and non required CSS parts than before with a huge webpacked file.
@WebMamba
Copy link
Contributor

Yes, I see what you mean here, and I am thinking about something similar. I think the right place to do it is in TwigComponent though. What I imagine is as follows: let's say you have UserCardComponent, this component displays user information and can be added to many different pages. This component came with its own Js and CSS. Here is the component template:

<div class="user-info">
    <p>Name: {{ user.name }}</p>
    ...
</div>

{% twig_component_styles %}
    .user-info {
          border: solid 2px black;
          & p {
               color: red;
          }
    }
{% end_twig_component_styles %}

{% twig_component_javascript %}
      // your component js
{% end_twig_component_javascript %}

Then when Twig is parsing the files, it adds the content of twig_component_styles and twig_component_javascript into a dedicated CSS and javascript and references these files into the import map.

@norkunas
Copy link
Contributor

Cool that I'm not the only one thinking about this topic.
But my minds are always going to turbo/live components topic and how this could work with them 🤔
For example link triggers a turbo stream which renders a dialog which css was not imported yet.

@stof
Copy link
Member

stof commented Aug 10, 2023

To make the CSS compatible with Turbo, it needs to be loaded in the <head>, not in the <body>. And given that the head is the first thing rendered in your layout (because it is the first part of the HTML output), you need to know the CSS at that point. And components used in the rendering of the page won't be known yet at that point.

@weaverryan
Copy link
Member

Parts of this sound similar to https://laravel.com/docs/10.x/blade#stacks from Laravel - the idea of being able to push things onto a "stack", which is then resolved only at the end of the request. That would allow components to define their own styles, which could then be output properly in <head>.

But my minds are always going to turbo/live components topic and how this could work with them

For live components, I'm not sure: it seems like metadata about "styles" would need to be returned in some way that the frontend could parse it out and add it to head. Possible. This is something the Tailwind world doesn't need to worry about :p.

@Nayte91
Copy link
Author

Nayte91 commented Aug 10, 2023

I think there is two distinct topics here:

  1. How to make the inventory of the statically called CSS files for a given page, to adapt the link tags accordingly with AssetMapper;
  2. How to make the inventory of conditional/dynamic parts of a page (modals, AJAX calls, live components, ..) to charge their CSS on-the-fly;

1 --> App <-- 2
Both can work together, but it's not mandatory!

I focus more on the 1, as it's smaller, less ambitious but has a lot of usecases with partial templates, twig components and atomic design pattern. For (I guess!) not so much efforts.

For the 2, you're not forced to end the process with AssetMapper: you can totally use your function to end up to a webpacked specific target, wants to gather the javascripts the same way, and so on.

For the 1, the goal is to leverage the new paradigm that assetMapper and HTTP2 bring:

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Foo {% block title %}{% endblock %}</title>
    <link rel="icon" href="{{ asset('images/favicon.svg') }}" />
    {% block stylesheets %}
        <link rel="stylesheet" href="{{ asset('styles/theme.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/globals.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/atoms/_header_button.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/atoms/_header_link.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/molecules/_table.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/molecules/_call_to_action.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/molecules/_header_authentication_menu.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/molecules/_header_navigation_menu.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/molecules/_header_popover.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/molecules/_replay_controls.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/molecules/_replay_search_preview.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/organisms/header.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/organisms/replay_form.css') }}" />
        <link rel="stylesheet" href="{{ asset('styles/organisms/replay_research_form.css') }}" />
    {% endblock %}

    {% block javascripts %}
        {{ importmap() }}
    {% endblock %}
</head>

As we don't pack files up now (hello @weaverryan! Idea came from your answer on symfonycasts ❤️ ), we end up with a lot of link tags? No worries, let's use this as an advantage instead of an annoying inconvenient! I feel like AssetMapper can have a function that delivers the stacked CSSs in the head tag, like importmap() does lower here. Something like {{ stylesheets() }} that delivers the result of the {% stylized %} loaded-and-hashed parts, no more, no less :)

@stof
Copy link
Member

stof commented Aug 11, 2023

There is a third point here actually: the architecture suggested by @WebMamba above colocating styles with the component template looks quite similar to the architecture of projects using CSS-in-JS to keep styles with their JS components. But there is a major thing in those CSS-in-JS artchitectures: the styles shipped to the browser are not actually just an extraction of the styles being in the code. The shipped styles are scoped only to the component itself, by changing both the CSS selectors and the HTML of the component.
This would be very hard to implement such a solution in our architecture based on Twig.

If you still require all selectors you write to be namespaced properly by the developer to avoid conflicts between 2 components reusing the same selector (because they use simple class name like .card for instance), co-locating the CSS with the component instead of keeping the CSS managed in a central place (in the assets folder) is a huge footgun to me.

@Nayte91
Copy link
Author

Nayte91 commented Sep 16, 2023

I think we should stick on the point 1. given how simple (I don't speak for coding it, I don't know) is it as a concept.

I was wondering if there is some capability also to check if stimulus controllers are called for a given page, and to load them accordingly? Same for translations also, but I think this is uncorrelated and can't be managed by the front part.

Anyway, I still don't see any args against such a feat, beside "who code it?", but if you want to roast it, feel free :)

@weaverryan
Copy link
Member

Hi!

ICYMI, #51543 adds CSS support to AssetMapper. The current iteration allows you to import './foo.css' like Webpack.

And yes, it also has the ability to render a bunch of <link rel="stylesheet"> onto the page for you. This actually happens right from the {{ importmap() }} call.

I was wondering if there is some capability also to check if stimulus controllers are called for a given page, and to load them accordingly?

Yup! https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers

And with #51543, if you import CSS from within a lazy CSS controller, that CSS will be downloaded & added to the page only when that Stimulus controller is first needed on the page.

About this issue specifically, I won't weigh in if it's a good idea, but @WebMamba's proposal with TwigComponents is likely possible, as (not shown in his example) we control an {{ attributes }} variable in Twig components, where a dynamic class could be added. But that's a topic for Twig components. I'm going to close this issue here as I think the scope of this idea is too large, at least for now.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants