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

<style bind:styles={styles}> and <style styles={styles}> - CSS Binding and Passing scoped styles between components and scoped svelte CSS import. #6422

Closed
lukaszpolowczyk opened this issue Jun 21, 2021 · 8 comments

Comments

@lukaszpolowczyk
Copy link

lukaszpolowczyk commented Jun 21, 2021

Is your feature request related to a problem? Please describe.
I want to have a common part of the CSS style, at the same time also I don't want to have to throw every little bit of CSS out of the *.svelte files and I don't want to use global CSS.

Describe the solution you'd like
CSS Binding and Passing.
It is the most intuitive, transparent and sveltish solution, at the same time referring to the CSS assert in custom element proposal.

Style set in App.svelte in style tag with bind:styles attribute, passed to the Component.svelte and Component2.svelte components in style with styles atribute. Scoped.

Example:

<!-- App.svelte  -->
<script>
 import Component from "$lib/components/Component.svelte";
 import Component2 from "$lib/components/Component2.svelte";
 let styles;
</script>
<Component styles={styles}>
<Component2 styles={styles}>
<style bind:styles={styles}>
 /* simple styles */
</style>
<!-- Component.svelte  -->
<script>
export let styles;
</script>
<style styles={styles}/>
  • Style should not be global, but scoped at the point of use.

  • I can see that in every component with <style styles={styles} />, the same hash like svelte-n323vl is used for classes.

  • The variable styles is an object that cannot be modified, it is used only to pass the preset style to the component.

Questions:

  1. It should be possible to use multiple style bind:styles={styles}(both declaration and use) and plain <style> at the same time.
    Is something in the way?
    OR: <style bind:styles={ [styles, styles2] } /> or <style bind:styles={ {...styles, ...styles2} } /> - and only the declaration could be multiple.

  2. Should the binded style in App.svelte also be applied to the HTML elements in App.svelte?
    Or should it be optional? Attribute of type onlydeclaration (I don't have a good idea of the name).

<style bind:styles={styles} onlydeclaration>
 /* simple styles */
</style>

I don't know if it should work in App.svelte by default and need to be turned off with the attribute, or vice versa?

Another option - better? - at the same time simpler, but also a bit circular (OF COURSE, this is not usually used, only in exceptional cases):

<!-- App.svelte  -->
<style bind:styles={styles}>
 /* simple styles */
</style>
<style styles={styles}/>

or even just:

<!-- App.svelte  -->
<style bind:styles={styles} styles={styles}>
 /* simple styles */
</style>
  1. Should css custom properties work?:
<!-- App.svelte  -->
<script>
 import Component from "$lib/components/Component.svelte";
 import Component2 from "$lib/components/Component2.svelte";
 let styles;
</script>
<Component styles={styles}>
<Component2 styles={styles}>
<style bind:styles={styles}>
 div {
  background-color: var(--css-var);
 }
</style>
<!-- Component.svelte  -->
<script>
export let styles;
</script>
<div style="--css-var: red;"></div>
<style styles={styles}/>

Probably yes, but maybe there will be some obstacles?

  1. Maybe in order for <style bind:styles={styles}></style> to not confuse with the standard <style></style> behavior (i.e. applying styles to App.svelte), you need to use <svelte:style bind:styles={styles}></svelte:style> ?

  2. Other idea...:
    Instead of requiring separate tags (<style bind:styles> or <svelte:style bind:styles>), I figured out a way to keep the number of <style> tags constant.

<!-- App.svelte  -->
<script>
 import Component from "$lib/components/Component.svelte";
 import Component2 from "$lib/components/Component2.svelte";
 let styles;
</script>
<Component styles={styles}>
<Component2 styles={styles}>
<style bind:exposed={styles}>

:expose {
  /* exposed styles */
}

 /* App.svelte own styles  */
</style>
<!-- Component.svelte  -->
<script>
export let styles;
</script>
<style styles={styles}>
  
 /* Component.svelte own styles  */
</style>

Where:

  • everything contained in :expose {} has a separate hash
  • binds to the styles object is the hash of the elements in :expose {}
  • using the styles object in <style styles={style}> causes the hash to be used in Component.svelte elements - Component.svelte has its own hash, and the one passed is an additional hash, e.g .:
    <p class="svelte-15kka16 svelte-edszo3"></p>
    svelte-15kka16 is the hash added with styles={styles} and svelte-edszo3 is the hash Component.svelte.
  • styles in :expose {} default are NOT applied to App.svelte. To make them, you have to use:
    <style bind:exposed={styles} styles={styles}>

I think it would be nice to be able to name the displayed block styles and use more than one:

<!-- App.svelte  -->
<script>
 import Component from "$lib/components/Component.svelte";
 import Component2 from "$lib/components/Component2.svelte";
 let styles;
</script>
<Component styles={styles.name1}>
<Component2 styles={styles.name1}>
<style bind:exposed={styles}>
  :expose(name1) {}
  :expose(name2) {}

Each of them would have a separate hash.

Most importantly, it is not global, and is controlled where this :expose {} block is to be used.

Summary:
The whole thing seems clear, refers to the upcoming standard, and at the same time is "sveltish".

Describe alternatives you've considered

<style>
:global(...)
</style>
<svelte:head>
   <link rel="stylesheet" href="styles.css">
</svelte:head>
<style src="./styles.css"></style>
<style>
	@import "./style.css";
</style>

Some do not work, none offer the type of possibilities I suggest, nor are they so sveltish.

If anyone wants to do RFC, please do.

How important is this feature to you?
The point is not to use global CSS or do some redundant CSS.
In fact, such something is needed all the time.

This is where my basic proposition ends.


FURTHER THINKING - Import CSS file and Passing Classes:

Import CSS file
Seems like can be used to... import CSS.
There is a proposition import styleSheet from "./styles.css" assert { type: "css" }; for WebComponents.

<!-- Component.svelte  -->
<script>
 import Component from "$lib/components/Component.svelte";
 import Component2 from "$lib/components/Component2.svelte";
 import styles from" ./styles.css "assert {type: "css"};
</script>
<Component styles={styles}>
<Component2 styles={styles}>
<style styles={styles}/>

Such an imported svelte style would also be scoped.

I can see an easy use of this in <svelte:options tag="my-element" />.
I will not be surprised if such placing of imported CSS will be interesting for someone, and passing between components less...

Question:
Do you allow import ("./styles.css", {assert: {type: "css"}});? Probably not.

Passing Classes
Binding classes with the style tag and passing to components. Extracting classes from the binding variable style.

Example:

<!-- App.svelte  -->
<script>
 import Component from "$lib/components/Component.svelte";
 import Component2 from "$lib/components/Component2.svelte";
 let styles;
</script>
<Component el={styles.el}>
<Component2 styles={styles}>
<style bind:styles={styles}>
 .el {
  /* props */
 }
</style>
<!-- Component.svelte  -->
<script>
export let el;
</script>
<div class="el"></div>
<style styles={{el}}/>

...or as in css-modules?:

<!-- Component.svelte  -->
<script>
export let el;
</script>
<div class={el}></div>
@non25
Copy link

non25 commented Jun 23, 2021

@lukaszpolowczyk
Copy link
Author

@non25 Yes, I mentioned css-modules fit there.

What do you think about my entire proposal?

I admit that the lack of any reaction worries me a bit.
I don't know if the proposal is so insignificant that there is no reaction?
It seems to me that what I have described is comprehensive and consistent and simple.

@non25
Copy link

non25 commented Jun 23, 2021

I don't think it is productive to discuss something like this now.
They already gone for sveltejs/rfcs#13, and recommend using that as a tool to do style overrides in an explicit way.
You can explore #2888 and #2870 to understand what their position looks like.
Svelte way is all about wrapping stuff with divs, not about what you described. I'm sorry.
Also there's a technical limitation with svelte-scoping and class passing: read more.

I recommend using svelte-preporcess-cssmodules, as I don't see anything like this will ever be implemented, and svelte-preprocess-cssmodules solves everything for me and gives me a feeling that I'm not using something external, like with css-modules.

@lukaszpolowczyk
Copy link
Author

I see it like this:

  • passing css custom properties is another thing (done here Passing CSS custom properties to components rfcs#13)
  • passing classes is another thing (css modules etc.)
  • another thing is importing a CSS file (there are also some non-sveltish suggestions)
  • another thing is to pass a block of styles - and that's what I suggest

And it's easy to expand on my proposal to handle CSS import and class passing - but that is another matter, it is not my basic proposal, but a possible extension of the proposal.

So "limitation with svelte-scoping" is more about passing classes, and passing a block of styles is different and much simpler.
There are no problems of penetrating, confusing, turning everything upside down.
All is explicitly passed through the binding object.

Well, in fact, it makes no sense to write about it until some person in charge of svelte gives a signal.

@tanhauhau
Copy link
Member

@lukaszpolowczyk thank you for such an elaborate proposal, here are some of my thoughts on the proposal, and why I think it is not feasible to implement this in Svelte at the moment.


before that, to understand how scoped styles work in Svelte, when you write:

<h1>Heading 1</h1>
<h2>Heading 2</h2>

<style>
   h1 { color: blue; }
   p { color: yellow; }
</style>

it get compiled to something that looks like this:

<h1 class="svelte-xxx">Heading 1</h1>
<h2>Heading 2</h2>

<style>
   h1.svelte-xxx { color: blue; }
</style>

the style h1 { color: blue } is now scoped to only <h1> element within the Svelte component, by adding a svelte-xxx class that is unique to this component.

  • if you write h1 { color: green } in another Svelte component, a different svelte-yyy class is generated, to prevent the styles conflicting across components
  • only h1 gets added with svelte-xxx, not h2. Svelte doesn't add class to elements that are not selected within <style>
  • p { color: yellow } gets removed because it does not match to any of the elements within the component

So, if you understand how the scope style works, now to pass <style> from 1 component to another, how do you:

  • inform another component about the svelte-xxx used, unique to the current component?
  • how do you know in the other component, what elements will be targeted by the style from the current component? do you append the svelte-xxx to all of the elements?
  • how do you know what are the selectors within the styles are unused, if the style will be passed into another components during runtime?

if you think hard enough, you may come up with some solution to this problems, which most likely undo all the optimisations comes with Svelte.

on that note, I'm going to close this issue for now.

@lukaszpolowczyk
Copy link
Author

lukaszpolowczyk commented Aug 3, 2021

@tanhauhau I will try to answer as far as I can:

1.
If <style> is used with the bind:styles attribute in A.svelte, a copy of the entire contents of the style tag is saved in "air".

The styles object has a indicator to this style copy.

A indicator to the style copy is passed to the B.svelte component.

Answer: There would be no redundant removal at this point. It would be generated individually.

2.
At the compilation stage: If a component has the styles attribute in the <style> tag and the styles attribute takes a variable that is in export - pass the component element "map" to the runtime - map is a code that would be a list of indicators to the elements used in the component (and probably the names of the animation, because svelte also marks them with a hash.

3.
At the compilation stage: If the component has a bind:styles attribute in the <style> tag - make a copy of the <style> content and pass it to runtime and to the styles object write indicator to this style copy.

2. and 3.
Answer: If in a B.svelte component a <style> tag is used styles={styles}, the runtime takes the "air" copy of the style indicated to by the indicator, and normally matches which selectors match which elements. and which are not.

if you think hard enough, you may come up with some solution to this problems, which most likely undo all the optimisations comes with Svelte.

So svelte-xxx would be added to the element only as it needs to be. And a copy of the styles would only be made as it needs to be.
In a place where you do not use the tag <style> with the bind:styles and styles, optimization will not be completely nothing different.
And for a component that uses bind:styles etc., the only loss is:

  • storing the code responsible for the map of indicators
  • storing a copy of the style (although this can also be limited)
  • comparison of selectors and elements during assembly (if variants were not prepared in advance - as described in Additional thoughts)

Additional thoughts:
If the style transfer would be less dynamic, more static, then at the compilation stage the styles could be prepared for a specific B.svelte component - e.g. a style with A_1.svelte for B.svelte, and the style A_2.svelte for B.svete - then there would not have to be a copy of the entire style from A.svelte, it would only be toggled on or off if used (e.g. by commenting a style from A_1.svelte prepared under B.svelte a uncommenting the style from A_2.svelte prepared under B.svelte. an element indicators map would still be needed).

Overall, the goal is:

  • NOT using global
  • NO duplication of style in project code (but generated less of a problem)

I have no idea how to pass the indicator less dynamically. Maybe using setContext or something like that? I do not know.

@tanhauhau
Copy link
Member

a copy of the entire contents of the style tag is saved in "air".

maybe can elaborate more on this "air" ?

I have no idea how to pass the indicator less dynamically

me too. and i dont think it's possible at the current state of Svelte

@lukaszpolowczyk
Copy link
Author

maybe can elaborate more on this "air"?

In the form of a simple string or inside an additional <style> tag, but commented out, attached to a bundle.
And this string is to be accessed by the component creator via a indicator. Component creator copies the string and processes it into a component.

me too. and i dont think it's possible at the current state of Svelte

Just to understand it well - in general, it is possible to implement the whole mechanism.
What I have just described is enough for bind:styles etc. to work.

I just wrote that if I did it less dynamically, it could all be prepared on the compiler side, and now it can be done, but doing some extra runtime side operations.

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

3 participants