tabatkins / specs

:pencil: Spec proposals that aren't yet approved by the relevant Working Group.
179 stars 22 forks source link

Can an @apply rule be used inside a custom property bag declaration by the same name? #43

Open philipwalton opened 9 years ago

philipwalton commented 9 years ago

For example, imagine inside <my-element> you have the following style declaration:

:host {
  /* Sets some defaults */
  --my-element-styles: {
    color: red;

    /* But also applies any inherited declarations. */
    @apply --my-element-styles;
  }
}

I've been experimenting with the @apply feature in Polymer and, when composing elements, I quickly found myself wanting to have the composing element set some custom property values yet still allow for parent elements to override them. As far as I can tell, that's not supported (at least not explicitly) in this proposal (Polymer fails when trying this).

Conceptually related, it's not clear to me whether custom property bags operate as a merge (like they do on elements) or as an override.

For example, consider the following HTML structure and CSS rules:

<div class="parent">
  <div class="child">
    <div class="grandchild">
      text...
    </div>
  </div>
</div>
.parent {
  --styles: {
    font-family: monospace;
  }
}
.child {
  --styles: {
    color: red;
  }
}
.grandchild {
  @apply --styles;
}

Is the text red and monospaced, or is it just red because --styles was overriden in .child?

What I suspect is that the declarion is overridden, and if so, that could be an argument in favor of custom pseudo-elements as they could more gracefully (and predictably) handle merging of properties via the cascade.

tabatkins commented 9 years ago

Within a custom property's value, only var() values are expanded. Everything is kept literal, waiting for processing by something else (substitution into another property via var(), processing via JS, etc).

So using @apply in a custom property bag does nothing at declaration time. Upon use (via an @apply in another rule), the value of the custom property is subbed into the style rule and processed. At that point, the inner @apply is processed, triggering another substitution. Both the outer and inner @applys use the value of the given custom property on the invoking element.

If you want to build a custom property bag out of multiple other custom property bags at declaration time, use var():

.parent {
  --foo: { 
    color: red; 
    var(--bar); 
  };
  --bar: { 
    background-color: white; 
  };
}
.child {
  @apply --foo;
  /* gets "color: red; background-color: white;" */
}

Merging in a child (like your last example) can be accomplished, but it's a little tricky until we get the ability to refer to the parent's variable value. You'd just write something like:

.parent {
  --styles: { font-family: monospace; };
}
.child {
  --styles: { var(parent --styles); color: red; };
}
.grandchild {
  @apply --styles;
  /* gets "font-family: monospace; color: red; } */
}

This is the same problem as building up an "accumulating" variable for normal use - you need something like --indent: calc(20px + var(parent --indent)); to make it clean.

To do it today, you instead need to change the variable name as you go down the tree, such as appending a generation counter to the var name, so you're not self-referencing. This is tricky and error-prone, obviously. I'm waiting until variables finish getting implemented and used before doing Variables Level 2 with this ability.

tabatkins commented 9 years ago

THAT SAID, I might change the @apply behavior so that it gets substituted in at declaration time, like var() does. The behavioral difference isn't intentional; there's just as much call for something like --shadow: var(--shadow-color-at-use-time) 2px 2px;, so Variables level 2 will have some ability to declare that a var() is substituted "late", at use-time, rather than at declaration time. Having @apply work the same way - substituted at declaration time by default, and at use-time by request - is probably best for authors. And it means you don't have to remember the weirdness of using a var() in a declaration context, like my examples above do - they'd instead both be written with @apply, which I think feels much more natural, as it keeps the "var() is for values, @apply is for declarations" concept consistent.