Skip to content

Allow incremental scoped styling for child components #1003

Open
@loilo

Description

@loilo

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 a 414 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 one extends 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 same data-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 own data-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 extends 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions