qwik icon indicating copy to clipboard operation
qwik copied to clipboard

Allow "scoped" styles to affect the "host" element of a component.

Open petebacondarwin opened this issue 3 years ago • 8 comments

Is your feature request related to a problem?

Consider the following setup:

import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik';

export const App = component$(() => {
  useStylesScoped$(`.red-color { color: red }`);
  useStyles$(`.green-border { border: solid 1px green }`);
  return <p>Hello <Name class="red-color green-border"/> !</p>;
});

export const Name = component$((props: {class:string}) => {
  return <span class={props.class}>PETE</span>
});

Playground

Describe the solution you'd like

I would like that the red-color styles are applied to the "host" <span> element of the <Name> component. I understand that the styles should not leak down into the rest of the Name components template. But the host element feels like it should be special. Otherwise it is not possible to use useStylesScoped() to apply styles to components that live in the current template.

Describe alternatives you've considered

You can work around this by using useStyles() rather than useStylesScoped() but then the styles become global and leak everywhere.

Additional context

No response

petebacondarwin avatar Oct 13 '22 11:10 petebacondarwin

The use case I have is that I want to provide a simple <Image> component that can be styled by applying a CSS class from the containing template.

petebacondarwin avatar Oct 13 '22 11:10 petebacondarwin

OK so I see there was related discussion about this here: https://github.com/BuilderIO/qwik/discussions/1063 And then resolved here: https://github.com/BuilderIO/qwik/commit/d214d3950ed4d4075815bb240c581d2183f3d64a

So I think that I can get around this by combining scoped and global selectors.

petebacondarwin avatar Oct 13 '22 11:10 petebacondarwin

For posterity the solution is to use :global(...):

import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik';

export const App = component$(() => {
  // chaining below the `p` tag ensures that the `:global` only affects descendants
  useStylesScoped$(`p :global(.red-color) { color: red }`);
  // here the `:global` affects the whole DOM.
  useStylesScoped$(`:global(.green-border) { border: solid 1px green }`);
  return <p>Hello <Name class="red-color green-border"/> !</p>;
});

export const Name = component$((props: {class:string}) => {
  return <span class={props.class}>PETE</span>
});

petebacondarwin avatar Oct 13 '22 13:10 petebacondarwin

i imagine we could provide this as well:

import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik';

export const App = component$(() => {
  const {classes} = useStylesScoped$(`.redcolor { color: red }`);
  return <p>Hello <Name class={classes.redcolor} /> !</p>;
});

export const Name = component$((props: {class:string}) => {
  return <span class={props.class}>PETE</span>
});

manucorporat avatar Oct 15 '22 13:10 manucorporat

I guess the question is whether one should special case class or whether Qwik should provide a mechanism for specifying what attributes should be assigned to the host element in general?

petebacondarwin avatar Oct 16 '22 19:10 petebacondarwin

there is no host element

manucorporat avatar Oct 17 '22 06:10 manucorporat

i imagine we could provide this as well:

import { component$, useStyles$, useStylesScoped$ } from '@builder.io/qwik';

export const App = component$(() => {
  const {classes} = useStylesScoped$(`.redcolor { color: red }`);
  return <p>Hello <Name class={classes.redcolor} /> !</p>;
});

export const Name = component$((props: {class:string}) => {
  return <span class={props.class}>PETE</span>
});

Oh I misunderstood your suggestion. This is using CSS modules to pass the style in?

petebacondarwin avatar Oct 17 '22 09:10 petebacondarwin

May I suggest renaming the issue to something like "allow including style scope when passing class to child component"? (since the original name is misleading because components don't have host elements)

literalpie avatar Dec 13 '22 22:12 literalpie

Hmm, here's an updated playground. When you inspect the HTML output, you can see that the <p> and <Inline /> components got the extra class but the <Name/> didn't.

  <body>
    <!--qv q:sstyle=⭐️olukuv-0 q:id=0 q:key=Ncbm:0t_0--><style
      q:style="olukuv-0"
      hidden
    >
      .red-color.⭐️olukuv-0 { color: red }</style
    ><style q:style="o74rpj-1" hidden>
      .green-border { border: solid 1px green }
    </style>
    <p class="⭐️olukuv-0" q:key="4e_3">
      Hello
      <!--qv q:key=4e_1--><span class="⭐️olukuv-0" q:key="4e_0"></span
      ><!--/qv--><!--qv q:id=1 q:key=jGuP:4e_2--><span
        class="red-color green-border"
        q:key="4e_4"
        >PETE</span
      ><!--/qv-->
      !
    </p>
    <!--/qv-->
  </body>

The difference is that Name is a Qwik component, which is rendered slightly differently, in a new qwikContext, and it's not inheriting the ⭐️olukuv-0 class. I'm not sure if this is intentional or a bug.

Current behavior is as follows:

  1. generate scopeId
  2. convert the given css so each selector requires scopeId class
  3. give all plain elements the scopeId as a class. Qwik components don't get the class, they start a new scope

The behavior you would like changes step 3 to pass the scopeId as a class to the entire subtree. So if you use scoped styles in root, every dom element under root will have the extra class of root's scopeId.

Is that good or bad? I'm not sure, but it "feels" like that's not a great idea.

Anyway, I'm personally not a fan of scoped styles, I think modular or atomic styles are better. For modular styles you can use .module.css imports or use Vanilla Extract, and for atomic styles you can use Tailwind CSS, UnoCSS, or PandaCSS.

wmertens avatar Oct 23 '23 14:10 wmertens

Now we can do like this:

export default component$(() => {
  const {scopeId} = useStylesScoped(styles);
  //...
  return (
    <SomeComponent class={scopeId} />
  );
});

genki avatar Jan 02 '24 11:01 genki

@genki so basically, this is the situation, right?

@petebacondarwin does that work for you?

wmertens avatar Jan 02 '24 11:01 wmertens

So the idea is that you get get something back from the call to useStyledScoped$() that can be used to get access to those styles elsewhere? If so, then I think that is good enough for my case.

petebacondarwin avatar Jan 11 '24 09:01 petebacondarwin

@petebacondarwin ok when you verified that it works for you, can you close the issue? You can try it in the playground.

wmertens avatar Jan 11 '24 09:01 wmertens

Sadly this doesn't seem to be enough. The scopeId is just a string that can be used to compute the "scope" of the styles of a component. I can't work out how to use this id to apply styles to a child component.

petebacondarwin avatar Jan 12 '24 12:01 petebacondarwin

@petebacondarwin seems to work for me?

https://qwik.builder.io/playground/#v=1.3.5&f=Q0o0JmZYE40ORv2kowBK2cQkJojnXVJTC4CeR05RBeCAADUtIO0H2%2BroAj0wS0cpvSg1NU8pttYuBagP0uzQRDUwIBEYU6DwREmktnbgpgpEEhjPNiBX6tvBTCAqgUPMry4GedQzpRZoBUblnABUpgd2IzCYEDWPbnJ%2BTn6RFbC6S88oAUtbK9QClSZoIuUbiNsgsQC1wwphGyRyCqAhAg0HBbSsBlEEDlJoyMG0g2PVRh9qx2iWHKpZEgA

import { component$, useStylesScoped$, Slot } from '@builder.io/qwik';

export const Deep = component$((p) => <div class={[p.class,"green"]}>deep</div>)
export const Parent = component$(()=><div>Parents: <Slot/></div>)

export default component$(() => {
  const {scopeId} = useStylesScoped$(`
  .green { background-color: lightgreen; }
  `)
  return <Parent>
    scopeId: {scopeId}
    <p class="green" >Hello Qwik</p>
    <Deep class={scopeId}/>
  </Parent>;
});

wmertens avatar Jan 12 '24 12:01 wmertens

Fair enough. Thank you for bearing with me. Happy to close this.

petebacondarwin avatar Jan 12 '24 13:01 petebacondarwin