vue-loader
vue-loader copied to clipboard
Allow incremental scoped styling for child components
What problem does this feature solve?
Disclaimer: I could not create this issue via the Vue.js issue helper and therefore unfortunately could not auto-attach the
feature request
label. Got a414
error from the GitHub servers, apparently this proposal is just too long to fit into a URL. 🙃
For Clarification: Whenever this proposal mentions parent/child components, it's not about nesting (as in
vm.$parent
/vm.$children
) but about the relation between two components where oneextends
the other.
Status Quo
As described in this forum thread, there currently seems to be a lack of an easy way to incrementally add scoped styles to .vue
child components. This makes creating reusable components harder than it needs to be.
Regarding styles, using extends
currently is an all-or-nothing approach:
-
When no
<style>
block is present on a child component, it gets the samedata-v-*
attribute as its parent and thus inherits all of its styling.By example:
Parent Component Source
<template> <button class="button">Click me!</button> </template> <style scoped> .button { border: 1px solid red; } </style>
Child Component Source
<script> import Parent from 'parent' export default { extends: Parent } </script>
Pseudo-Compiled Output
<!-- Rendered Parent Component --> <!-- has a red border --> <button class="button" data-v-parent>Click me!</button> <!-- Rendered Child Component --> <!-- has a red border --> <button class="button" data-v-parent>Click me!</button> <style> .button[data-v-parent] { border: 1px solid red; } </style>
-
When a scoped
<style>
block is added to the child, it gets its owndata-v-*
attribute and parent styles are no longer applied to it at all.By example:
Parent Component Source
The same as before.
<template> <button class="button">Click me!</button> </template> <style scoped> .button { border: 1px solid red; } </style>
Child Component Source
<script> import Parent from 'parent' export default { extends: Parent } </script> <style scoped> .button { color: blue; } </style>
Pseudo-Compiled Output
<!-- Rendered Parent Component --> <!-- has a red border --> <button class="button" data-v-parent>Click me!</button> <!-- Rendered Child Component --> <!-- has blue text but no red border --> <button class="button" data-v-child>Click me!</button> <style> .button[data-v-parent] { border: 1px solid red; } </style> <style> .button[data-v-child] { color: blue; } </style>
What does the proposed API look like?
I'd like to propose a new option for Vue component definitions. The name is debateable, but for now let's agree on calling it extendsScopedStyle
.
The option
- is a boolean flag, defaulting to
false
- is only available in Single File Components
- does only apply if the component
extend
s another Single File Component
Setting extendsScopedStyle
to true
on a child component definition will cause its relevant DOM nodes to inherit the parent's data-v-*
attribute as well as receiving its own custom data-v-*
attribute. This will apply both, parent and child styles, to the child component.
This also is best explained by example. The following code reflects the proposed behaviour with a custom <style>
on the child component. The child does receive the parent's styling as well as its own styling.
Parent Component Source
Still the same as before.
<template>
<button class="button">Click me!</button>
</template>
<style scoped>
.button {
border: 1px solid red;
}
</style>
Child Component Source
<script>
import Parent from 'parent'
export default {
extends: Parent,
extendsScopedStyle: true
}
</script>
<style scoped>
.button {
color: blue;
}
</style>
Pseudo-Compiled Output
<!-- Rendered Parent Component -->
<!-- has a red border -->
<button class="button" data-v-parent>Click me!</button>
<!-- Rendered Child Component -->
<!-- has blue text AND a red border -->
<button class="button" data-v-parent data-v-child>Click me!</button>
<style>
.button[data-v-parent] {
border: 1px solid red;
}
</style>
<style>
.button[data-v-child] {
color: blue;
}
</style>
Gotchas / Caveats
Specificity
To make sure parent styles are actually overridden by child styles, the order of <style>
blocks in the resulting bundle matters. The child component styles would have to be inserted after the parent styles.
As far as I can tell there shouldn't be any conflicts in determining that order since there shouldn't be any kinds of cyclic dependencies between components. (Am I right here?)
Dependency Chains
While some component A
extends component B
, component B
may extend component C
and thus also be a child component itself.
Therefore, the data-v-*
attribute of C
needs to fall through to B
as well as to A
.
Location of the Flag
To be honest, having a new property on the Vue component object for an approach tied so tightly to Single File Components does not feel great.
However I was encouraged by the fact that
-
it'd not be the first option to not be globally applicable (speaking of
name
being respected everywhere but in Single File Components) -
the alternative ways to signal the described behaviour seem terrible to me:
Regarding the feature's scope, an
extends-scoped-style
attribute on the<template>
block would probably be the best fitting option, since the template is what's actually affected. However, this is not possible because child components may not even have a<template>
block.An
extends-scoped-style
attribute on a<style>
block (which was my first approach for this proposal) does not feel very clean either because the described behaviour actually has nothing to do with a specific<style>
block.Also it's valid to have multiple
<style scoped>
blocks in the same component — some may have that marker attribute, others may not. This would be a weird inconsistency because the feature can only be an on/off thing for the component as a whole.
I just encountered this same problem when I was trying to figure out how to extend a base component, using the same template and methods on the parent, but overriding some of the styles. I definitely don't want to start playing with css imports. The proposed solution sounds good to me.
I encountered this problem too. Will be great if the proposed solution accepted.
same stuff for me. We have shared components, that we use in other projects. And at some point we need to change styles a little bit in some particular project for some particular shared component. With this extends problem we can't provide appropriate solution for this(
So was this feature ever implemented???
If it was, this issue would have been closed. 😉
That's too bad...
As almost always with Vue these days, there is a workaround with a slight amount of overhead, but the upside of being really expressive about what you are doing:
<template functional>
<extended-component v-bind="props" v-on="listeners" class="extended-component" />
</template>
<script>
import ExtendableComponent from "@/extendable-component"; // Just an example
// Here we extend the component's internal behavior (methods, computed, injections, etc..)
// In other words: Here goes all the stuff messing with the instance of the extendable component:
const ExtendedComponent = ExtendableComponent.extend({ /* Here be dragons */});
// Here (and in the template) we extend the component's external behavior (props, events, etc...):
export default {
functional: true, // We don't need an instance if we only want to statelessly wrap the component
components: {
ExtendedComponent
},
// Here be more dragons, but only stateless ones
}
</script>
<style scoped>
/* And here we extend the component's visual styles via deep selector ">>>" */
.extended-component >>> .class-within-ext-comp {
}
</style>
You can do any of these three extension steps independently and know exactly what kind of behavior each one is affecting. Also: If the extendable component supports them, overriding via css modules is a nice alternative too.
I'm using something like the above now. However, it exposes the really tricky/annoying problem of CSS specificity. To use the piercing >>>
selector (or /deep/
in Sass), a parent selector is needed, which increases specificity for each level of extension.
This gets messy very fast, and god forbid you'd have to override some component styles from outside.
Thanks for sharing anyway, maybe someone can make use of that.
I like the idea of creating 'skins'. But my co-worker does not not. This is the idea:
<skin-button-danger> <button>Press me</button> </skin-button-danger>
...and in the skin-button-danger component you would use the piercing >>> selector to style the button. I like this method because it's very clear what you're doing. Any thoughts?
See my comment above yours. 🙂
I saw. I dont see how theyre so tricky. The idea with skins is that the base component never changes. Only the skins. And since the base component never changes, then after creating one skin, it can be easily duplicated, modified and applied as well.
And since your skin component is named, i personally think it makes your code more readable.
I agree with the readability, I think this is the best currently existing solution as well.
The specificity however becomes a problem when you're not going to just skin finished components, but for example compose components. This will end up in multiple nesting levels and therefore add layers and layers of specificity.
Yes i agree with the problem of multiple nested components.
I think the workaround involving the Shadow-Piercing descendant combinator, /deep/
(aka >>>
) for dynamic profile (in stylesheets) is problematic because
- it requires adding deep selector expressions for some (if not for all) styles of the subclassed component
- also
>>>
and/deep/
have been removed from the spec (see E) and are semantically invalid - and I'm not sure if sass does even support it, still (can't find it in the docs)
Another workaround could be to use sass and explicitly import
the stylesheets of the parent. Bit this would
- require to move the stylesheets to an extra file and corrupt the single file component methodology
- create duplicate css for every subclassed component (parent styles are duplicated every time)
I think the proposed solution is well-conceived. It would be awesome to see this implemented.
Hope to see some progression on this issue. It's really annoying not being able to extend simply a component to make variations of it 😉
For the moment, I make an external stylesheet and import it in all children components with <style src="path/to/base-button.css">
@yyx990803 Any chance of this being added before or after the release of vue 3.0?
That's how the hack works
The idea is to use the _scopeId of the parent component
const component = { extends: Plan } Object.defineProperty(component, '_scopeId', { get: () => Plan._scopeId, set: () => {} })
export default component
@loilo @lorenzo-w @Nanobrainiac