vue-loader icon indicating copy to clipboard operation
vue-loader copied to clipboard

Is it possible to make "scoped" components style leak optional?

Open grigoryvp opened this issue 7 years ago β€’ 56 comments

What problem does this feature solve?

Right now "scoped" is not really scope: styles from components leak to child components (this is by design, "A child component's root node will be affected by both the parent's scoped CSS and the child's scoped CSS."). Is it possible to make such feature optional? In practice, I often define simple class names like "root" for flex-placing children and it always leak to child components, wreaking hawok to ones that are unfortunate enough to use same short names.

Right now the workaround is to use "module" syntax, but it adds lots of unneded text, especially if compilers like PUG are used for CSS.

Making vuejs config option like "dontLeakScopedModules" with default to "false" will be a bless! I can create a patch, if it's an acceptable feature and i'm not getting anything of this wrong.

What does the proposed API look like?

{ test: /.vue$/, loader: 'vue-loader', options: { dontLeakScopedModules: true

grigoryvp avatar Aug 27 '17 06:08 grigoryvp

I'm not sure that I understand the problem. If you style a child component, it's root node will be affected (which you would want in this case , wouldn't you?) . If you don't, it doesn't. And I don't see how other nested elements within the child complement would be affected, unless the child was not scoped and used generic class names instead of a solid name space - which would be a problem of the child no matter if the parent uses scoped or not.

Yes, css modules offer a higher level of isolation in this case, but I don't see how to easily improve in this direction for "scoped" with our fundamentally changing the whole logic that we have in place.

But if you have a good idea, propose it, we are always happy to discuss new ideas.

LinusBorg avatar Sep 12 '17 20:09 LinusBorg

Thank you for your reply! Sorry for being not clear with description. If it's acceptable I can display a simple use case. A new developer starts a project by creating an empty dir and doing some simple command-line magic:

yarn add vue-cli
vue init webpack .
yarn install
yarn run dev

After playing around a bit they want to create a website with menu to the left and content to the right. Accoring to VueJS/ReactJS/AngularJS architecture this will be 3 components: one for left-right flexbox split layout, one for menu and one for content. Developer starts with layout, replacing Hello.vue with this:

<template>
  <div class="root">
    <div class="left">
      <LeftMenu></LeftMenu>
    </div>
    <div class="right">
      <div>Here be dragons...</div>
    </div>
  </div>
</template>

<script>
import LeftMenu from './LeftMenu.vue';
export default {
  components: {LeftMenu}
}
</script>

<style scoped>
.root {
  display: flex;
}
</style>

After that, developer creates a LeftMenu.vue component:

<template>
  <div class="root">
    <div>Here be left menu</div>
  </div>
</template>

<style scoped>
.root {
  margin: 10px 0 0 10px;
}
</style>

Developer expect "root" to be scoped into componen and do not affect other components. Even comment in sample app hints so: "". The "root" sounds like a short and reasonable name for root div which are completely different for container and for menu. Unfortunately, with beforementioned vue-loader feature the "root" class from container will leak into "menu" componen, making it flexbox which will be totally unexpected and hard to debug.

Introducing a configuration option can disable this style leak and make development much more straightforward. May I offer a patch / merge request / pull request so you can see that this is a really small change?

grigoryvp avatar Sep 13 '17 13:09 grigoryvp

Developer expect "root" to be scoped into componen and do not affect other components

You won't be able to prevent the CSS from cascading with a "non-leaking-style-scope" modifier.

Example:

<parent style="display: flex" non-leaking-style-scope>
  <child>No styles</child>
</parent>

Renders to:

<div class="parent" style="display: flex">
  <div class="child" style="display: flex">No styles.</div>
</div>

Why does this do that? First, Vue components are not rendered in the shadow dom:

It is also totally feasible to offer deeper integration between Vue with Web Component specs such as Custom Elements and Shadow DOM style encapsulation - however at this moment we are still waiting for the specs to mature and be widely implemented in all mainstream browsers before making any serious commitments. -- https://vuejs.org/v2/guide/comparison.html#Polymer

At the cost of a performance hit, the Shadow DOM could work. Full integration would mean a robust plugin (until it's more widely implemented) that could handle the back and forth of passing dynamic modules to the shadow-dom type component. You would get the benefit of styles that don't leak in or out. You might want to include options to pass in styles or stylesheets you want to inherit - lest you get a blank canvas. Here is one way you can add it yourself: https://github.com/karol-f/vue-custom-element

For those like me who would want to know why it doesn't work outside of the Shadow DOM:

CSS is designed to cascade styles from parents to children on the computed DOM tree. In CSS, there are elements and rules which inherit the values of the parent by default. (Flexbox is one of those.) Check out this guide by MDN. Since browsers ship with default styles, we don't need to write all the rule defaults for every single rendered element.

With Vue templates, all rendered html from components will end up in the DOM, and therefore be child elements of some parent element. The CSS code compiled from Vue template blocks <style>, <style scoped>, or <style module> still cascades on those.

So what is the point of Vue's <style scope|module>?

  • <style> rules cascade from global scope. Add .foo here and it affects other elements rendered on the same page following the rules of the CSS cascade.
  • <style scoped> is a basically a cool shortcut that behind the scenes just adds a more specific selector which restricts the cascade to only itself (and it's children).
  • <style module> dynamically simulates 'scoped'. It's children also follow CSS cascading rules, but restricted to itself and it's children. (i.e. If you change the parent to display: flex, the children will still inherit the style.)

Is it possible to make a "non-leaking-styles" method or turn off the cascading rules that display inherited [parent|browser-default] styles?

Your options:

  1. Turn off the Cascading in CSS, or just for that rule.
    • Turning off the cascade or inheritance rule would then require you to add the rule manually, for all the elements. And convince browser vendors it's a good idea to give you that option.
  2. Fake it behind the scenes by adding more CSS "reset-rules-to-what-the-state-would-have-been-rendered-as-previous-to-parent-changing-it". This would require a lot of additions to both JS and CSS. Not to mention your final CSS file would be huge, impacting performance, SEO, site-speed and UX. and the bugs. not the bugs. and the overrides of the overrides.
  3. Make a plugin that has the component become rendered in the Shadow DOM. Headstart with this: https://github.com/karol-f/vue-custom-element

Read up on the terms "specificity" and "inheritance" here: https://www.w3.org/TR/CSS2/cascade.html#specificity http://monc.se/kitchen/38/cascading-order-and-inheritance-in-css https://www.w3.org/TR/CSS2/cascade.html#cascading-order https://developer.mozilla.org/en-US/docs/Learn/CSS/Introduction_to_CSS/Cascade_and_inheritance https://developer.mozilla.org/en-US/docs/Web/CSS/inheritance

P.S. @grigoryvp For layouts, I highly recommend learning CSS Grid because it's children don't inherit the way flexbox does. Once it clicks, you won't want to go back.

EDIT: Forgot about the Shadow DOM

johnathankent avatar Sep 16 '17 02:09 johnathankent

For my coworkers who wonder why I am ok with waiting for the native handlers, the virtual-dom used by vue and react is faster:

Virtual DOM is any kind of representation of a real DOM. Virtual DOM is about avoiding unnecessary changes to the DOM, which are expensive performance-wise, because changes to the DOM usually cause re-rendering of the page. It allows to collect several changes to be applied at once, so not every single change causes a re-render, but instead re-rendering only happens once after a set of changes was applied to the DOM. More precisely re-renders can and will quite heavily hit your hardware resources. Which will inevitably put your app performances into danger. Shadow DOM is mostly about encapsulation of the implementation. A single custom element can implement more-or-less complex logic combined with more-or-less complex DOM. Shadow DOM refers to the ability of the browser to include a subtree of DOM elements into the rendering of a document, but not into the main document DOM tree. -- https://vuejsfeed.com/blog/learn-the-differences-between-shadow-dom-and-virtual-dom

... and I happen to love what I can do with CSS. :)

johnathankent avatar Sep 16 '17 03:09 johnathankent

Thank you.

Maybe this picture will help? DOM structure from my example above with problem highlighted and commented:

image

grigoryvp avatar Sep 16 '17 05:09 grigoryvp

@grigoryvp Current browser specs have no such thing as style "leaking" unless you create elements in a Shadow DOM.

While Vue core doesn't include adding Shadow DOM polyfills (it requires more code and reduced performance while browsers don't support it), you still have options:

  • If you consider yourself a beginner to intermediate javascript level, or if you want to save time and effort: Learn about how Cascading Style Sheets work. I mentioned some links above in my previous comments.

  • If you consider yourself advanced in javascript, don't want to learn about the C in CSS, or just have a scenario that requires sandboxing elements: You can add Shadow DOM elements to your app with Vue using a custom method or something like this plugin: https://github.com/karol-f/vue-custom-element. Be prepared for a lot of custom work and a smaller set of online support.

johnathankent avatar Sep 20 '17 21:09 johnathankent

Thank you again.

As I explained before, maybe "leak" was a very bad word to describe the issue. As I illustrated in the screenshot, the problem is in the "data-v-b37e7c0a" HTML attribute that vue-loader adds in order for "root" class to be applied not only to the component where it's scoped, but also to the root element of a child component. This is a vue-loader feature described in the documentation:

"A child component's root node will be affected by both the parent's scoped CSS and the child's scoped CSS."

I propose to make an optional opt-out for this feature. I'm really sorry for that "leak" word and for spending your time.

grigoryvp avatar Sep 21 '17 06:09 grigoryvp

Well, I think I understand the point which @grigoryvp states. When we have a child component inside parent one - child component top-level node will receive both "data-" attributes - one from parent component instance and one from child. if both parent and child use same naming for css classes, like "root", that leads to pretty unexpected behaviour - when you're looking into template of parent component - you see no class name specified, so your expectation is that no styling from parent component will be applied. But since same class name is used inside child component template - it "magically" inherits styling of parent component.

In other words, when you're using scoped styles in that way, in order to understand styling of component it's not enough to look inside component template, since some classes with exactly same names may be pulled to root node from parent component. This makes things more complex when developing many components separately by big teams

xanf avatar Sep 21 '17 10:09 xanf

Thank you @xanf and @grigoryvp for the clarification. I apologize for missing the obvious that you pointed out in the image.

I missed that Vue had added the parent scoping data attributes onto the child component root element (so that the parent can control layout). If I understand correctly:

What's expected:

Adding the "scope" attribute simplifies development of component styling by automatically reducing CSS specificity required, meaning I can use whatever CSS specifier I want, and style conflicts between components will not happen.

Problem:

Style conficts might happen regardless of scoping, and I really can't use whatever CSS specifier I want without conflict. The documentation is misleading by comparing the scoping to encapsulation done with the Shadow DOM like Polymer.

We have a current workaround:

Understand the caveats, update the documentation (removing Shadow DOM similarity; including with how Cascading inheritance applies to rendered DOM).

Current known caveats when using <style scope>:

  1. Styles cascade to child elements and components.
    • Parent style will cascade inheritance unless the child element overrides it by adding more specific selectors AND overriding the matching rules.
    • "deep" parent components also apply style inheritance to all matching selectors of nested elements or children components.
  2. Styles cascade from global to scoped components.
    • Global or dynamic styles will still cascade inheritance to scoped children.
  3. Matching selectors on children root elements may cause style confilcts.
    • Avoid using selectors that the parent elements use. (and who wins the conflict is determined by which selector is last in the rendered CSS.)
  4. Custom elements using the Shadow DOM can ignore cascading styles from the parent..
    • Custom elements are not part of Vue core, but a developer can add it to a Vue app.

johnathankent avatar Sep 21 '17 22:09 johnathankent

Looks good to me!

Just a note for @grigoryvp and other's interested in that topic: while it is possible to make vue-loader not to add specific data attribute if at this point child component will be inserted, that will prevent some typical and useful scenarios, when you have some styling applied via css classes in parent component, and some - in child on the same node. It's definitely a frequently used case

xanf avatar Sep 21 '17 22:09 xanf

Before we recommend a patch or solution to be implemented, I think a discussion should be had regarding methods to remove the data-scope from the child root element. I predict problems if we recommend and/or implement something that doesn't follow a pattern. Already, the additional option will continue to have the same 'this is not obvious functionality' problem when trying to determine why scoping was/wasn't followed.

Points to discuss:

  • What would be most transparent for developers?
  • What would be easiest to develop, manage, and follows a pattern?
  • Where is it best to apply the option/flag?
    • In the parent template as an option? Vue options/misc: { dontScopeLittleBros: true }
    • In the child template as an option? Vue options/misc: { dontScopeMeBro: true }
    • As an attribute on the child element in the parent? <child-component dontScopeMeBro>
    • As another attribute on style in the parent? <style scoped dontScopeLittleBros>
    • As an attribute in the child? <template dontScopeMeBro>?
  • How will true encapsulation via Shadow DOM fit the pattern when it is finally supported by browsers? <template encapsulated>, or Vue options/misc: { encapsulated: true }

johnathankent avatar Sep 21 '17 23:09 johnathankent

Just had a heated deabed with our front end team lead. It seems that simplest way to "opt-out" from this feature is to add an empty root div, which is very easy. And feature itself seems to be usefull since it allows to modify non-cascading styles for root element like "margin", "border-radius" etc. So I humbly propose following improvement into docks, we can replace

"With scoped, the parent component's styles will not leak into child components. However, a child component's root node will be affected by both the parent's scoped CSS and the child's scoped CSS. This is by design so that the the parent can style the child root element for layout purposes."

with something like:

"With scoped, the parent component's styles will not leak into child components. However, a child component's root node can use classes from both parent's scoped CSS and it's own scoped CSS. This feature allows parent component to change child component's root node non-cascading styles like "margin" or "border-radius". To disable parent component's class names visibility for child component's root node just add a "div" root node without classes to the child component"

If it's acceptable I will do an English proof read and submit a patch

grigoryvp avatar Sep 24 '17 06:09 grigoryvp

@grigoryvp That is a simple workaround, another is to avoid using the same classnames on the root node.

I also think this topic should be revisited when Shadow DOM support is going to be added to Vue core if it makes sense.

johnathankent avatar Sep 25 '17 16:09 johnathankent

@grigoryvp @xanf @LinusBorg I think the docs needed a bit more to help fix the confusion, so I also took a crack at updating the documentation for Scoped CSS. Let me know what you think (forked version here): https://github.com/johnathankent/vue-loader/blob/53a9f3ab52d63aabe9304a61b89f99392fac620c/docs/en/features/scoped-css.md

johnathankent avatar Sep 25 '17 20:09 johnathankent

Thanks, @johnathankent

But I can't see any info that this feature can be disabled by adding empty parent 'div' element. Did I miss something or such info is not important?

Also, offtopic, you wrote that another workaround is to avoid using the same classname on different components. But how is it possible for medium to big sized projects with hundreds or thousands of components? Human focus can handle only 7 plus-minus 2 items at a time :( Or if different developers are creating components - how can they check that root element's classes are unique?

grigoryvp avatar Sep 27 '17 09:09 grigoryvp

@grigoryvp I feel your pain. Communicating front-end CSS and how it works is hard.

The documentation as it currently stands kinda says 'leaks' and I think that has caused some added confusion, so that's why we need to help fix it.

Cascading inheritance is not something that can be stopped. Styles cascade (not 'leak'), and we cannot disable cascading as a 'feature'. Cascading styles are browser functionality and not Vue.js features. Without Shadow DOM custom elements, disabling and leak fixes just ain't gonna happen. Vue components help "encapsulate" styles, but all that means is it's adding specificity so style cascades stay local to itself and all it's rendered children.

But I can't see any info that this feature can be disabled by adding empty parent 'div' element. Did I miss something or such info is not important?

I thought that's what you meant in your earlier comment:

Just had a heated deabed with our front end team lead. It seems that simplest way to "opt-out" from this feature is to add an empty root div, which is very easy. And feature itself seems to be usefull since it allows to modify non-cascading styles for root element like "margin", "border-radius" etc.

I use a pattern where external parent components or stylesheets handle layout using the root node in Vue Components, so I avoid setting styles on it. I think that's what the documentation meant to convey in the first place. And even with wrapper divs, cascading styles are not going to be removed or prevented.

Used as a simple pattern might lessen conflicts, but it's still no guarantee that cascading inheritance will not conflict with styles from a Vue child component.

Also, offtopic, you wrote that another workaround is to avoid using the same classname on different components.

Yeah, I wasn't clear there. I was thinking of team project styleguides. In large teams or project cases as you point out, documenting or syncing classnames is not simple. In that case, a pattern is perfect use case.

I added a couple pattern and tried to clarify a bit more. Let me know what you think: ~~https://github.com/johnathankent/vue-loader/blob/74603af204cc449af2857e1e63e12246cc48ee95/docs/en/features/scoped-css.md~~

Edit: I added a couple changes and realized that all the "Keep in mind" section was technically styleguides. So I propose that we could move the styleguide to a separate page and reference it early - in order to keep it clean and simple.

Updated link here: https://github.com/johnathankent/vue-loader/blob/111bc28c3eaa478ec1b0cdf5943dd7d2545b8ea9/docs/en/features/scoped-css.md

johnathankent avatar Sep 29 '17 04:09 johnathankent

More updates!

I made clarified the headers a bit, and created a troubleshooting section to the styleguide. See: https://github.com/johnathankent/vue-loader/blob/f0c830e6839ab2e9ce04891421ffd1b846c3fde8/docs/en/features/scoped-css.md

johnathankent avatar Sep 29 '17 19:09 johnathankent

Lots of text, but it explains the point about child root nodes and how they are styled by parents. Thanks a lot for the hard work and sorry for all the troubles!

grigoryvp avatar Oct 01 '17 11:10 grigoryvp

@grigoryvp Ain't no trouble. Happy to help. I also apologize for not understanding the real problem at first, too.

Anything else we could do to make the doc easier or cleaner before we submit it?

johnathankent avatar Oct 02 '17 00:10 johnathankent

We've almost got a solution!

As soon as https://github.com/vuejs/vue/issues/7385 is fixed (if it isn't already and I missed it) then we can easily use SkateJS to make Custom Elements written as Vue single-file-components that can be composed by nesting them, and have shadow roots to encapsulate inner parts.

This would keep styles from cascading to child components!

At the moment, vue-custom-element does not support nesting of the generated custom elements, while using SkateJS with a Vue renderer does.

Here's how you generate custom elements from Vue components using SkateJS (it works, except for the above mentioned styling issue):

https://github.com/skatejs/skatejs/issues/1289#issuecomment-355460128

Note the RealSlot definition and the use of <real-slot> in App's template so that we can place a real real slot element there. By default SkateJS automatically creates an element's shadow root, and we used <real-slot> to specify where to distribute the element's content.

The content in the shadow root is generated by the Vue component! So you can use that VueElement function to turn any Vue component into a Custom Element. πŸ˜„

Output looks like the following, and notice the inspector shows both elements nested:

screenshot 2018-01-04 at 6 43 14 pm

So, once we figure out how to get styles placed inside of shadow roots, then we'll be good to go!!

πŸ˜‰

trusktr avatar Jan 05 '18 02:01 trusktr

Okay, admittedly, this doesn't wire up the attributes to the component props, yet...

trusktr avatar Jan 05 '18 07:01 trusktr

Alright you all, I published my solution. Styles work and won't leak into sub-roots: https://github.com/trusktr/vue-web-component

There's some listed caveats, but planning to improve this.

trusktr avatar Jan 22 '18 13:01 trusktr

+1 for being able to turn this feature off.

https://github.com/johnathankent/vue-loader/blob/f0c830e6839ab2e9ce04891421ffd1b846c3fde8/docs/en/features/scoped-css.md#layout-and-child-components

robyedlin avatar Feb 20 '18 17:02 robyedlin

+1 for being able to turn this feature off

alianrock avatar Mar 02 '18 10:03 alianrock

Not sure it should be global setting. Even if it’s global, it would be nice to have an ability to explicitly define it in particular component, overriding global setting, like:

<style scoped parent-scope-off>
<style scoped parent-scope-on>

Also there is already a quite obvious workaround by using well defined class names on components root elements, like: class="left-menu-root"

slavap avatar Apr 04 '18 07:04 slavap

I'm not sure this is the right way to phrase it, but I think something like this would be nice:

<style scoped>
.a .b#no-scope .c#no-scope .d {
    font-size: 24em;
}
</style>

But pretend like #no-scope is syntax for not applying scope to that sub element (it'll still apply to .a and .d). This would be useful when .b and .c are actually located in other components but .d is inserted as a slot.

carcinocron avatar May 08 '18 15:05 carcinocron

+1

yassh avatar Jun 27 '18 11:06 yassh

Whew, this issue branched out. I want the same thing as @grigoryvp (I think), which got illustrated here: https://github.com/vuejs/vue-loader/issues/957#issuecomment-329947476, though still a little confusing.

Here's my take on it, feel free to verify this is the same thing @grigoryvp.

screenshot from 2018-07-05 14-33-35

The atom-wrapper component is scoped, causing all its elements (just a div) to get data-v-4c76af28. But, unexpectedly, the other components thats inside its slot also get data-v-4c76af28.

If I wanted to style, for example, atom-title when its a child of atom-wrapper, I'd just do this inside atom-wrapper: .atom-wrapper >>> .atom-title {margin-top: 30px}

The problem arise on the atom-flex component, which uses a div as its element. It's now affected by atom-wrapper's div selector (matches div[data-v-4c76af28]).


Such a thing would make more sense for me to be opt-in, if anything.
While it seems like shadow dom would solve a part of it, it should be its own thing.

It just bit me by surprise, causing me to think there was a bug in vue-loader. After reading the mind-bending paragraph on vue-loader, I tried to find a way to disable this - and I found this issue.

oles avatar Jul 05 '18 13:07 oles

Not sure it's the same thing, in my example Vue loaders adds two data- things instead of one. In your example they are one-per-element, that looks fine

grigoryvp avatar Jul 05 '18 13:07 grigoryvp

There would be two data-v- tags if I added scoped to atom-flex :)

oles avatar Jul 05 '18 13:07 oles