ember-component-css icon indicating copy to clipboard operation
ember-component-css copied to clipboard

Request: guide for extensible component CSS

Open jamesarosen opened this issue 7 years ago • 4 comments

Background

Design and document for inheritance or else prohibit it. -Joshua Bloch, Effective Java

There are many cases where I have a component with some internal structure:

{{!-- my-component.hbs, outer HTML --}}
<div class='{{styleNamespace}}'>
  <div class='{{styleNamespace}}__sub-component'></div>
</div>

The SCSS for that might look like this:

// my-component.scss
&__sub-component {
  background-color: white;
  color: rebeccapurple;
}

Those class names are generated and not available to a calling component. That is, if the author of my-component didn't anticipate a use-case (bolder, grayer, rotated, or capitalized), then there's no way for consumers of my-component to extend it to get the effect they want.

There are a few standard practices for adding customization affordances:

Passed-In Class

The author of my-component accepts a class and adds it to the sub-component:

{{!-- my-component.hbs, outer HTML --}}
<div class='{{styleNamespace}}'>
  <div class='{{styleNamespace}}__sub-component {{subComponentClass}}'></div>
</div>
<div class='some-special-context'>
  {{my-component subComponentClass='sub-component-override'}}
</div>

This lets the caller add any arbitrary customizations:

// caller.scss
.some-special-context {
  .sub-component-override {
    background-color: azure;
    font-weight: bold;
    border-radius: 3px;
    transform: rotate(10deg);
  }
}

One benefit of this practice is that it lets callers pass in functional CSS classes:

{{my-component subComponentClass='font--large flex--1 color--cornflowerblue'}}

CSS Variables

NB: this currently doesn't work with ember-component-css. See #48 and #282.

The author of my-component defines (some of) its styles using CSS variables. This allows consumers to customize specific aspects of the sub-component, but not add arbitrary CSS.

// my-component.scss
:root {
  --my-component-sub-component-background: white;
  --my-component-sub-component-color: rebeccapurple;
}

&__sub-component {
  background-color: var(--my-component-sub-component-background);
  color: var(--my-component-sub-component-color);
}

The caller can customize those variables, but not anything else:

// caller.scss
.some-special-context {
  --my-component-sub-component-background: coral;
  --my-component-sub-component-color: darkolivegreen;
}

.some-special-context appears within the DOM between :root and &__sub-component so its variables override those from :root. Note that it wouldn't work to define those variables on & in my-component.scss because that element is within .some-special-context.

Static Classes

The author of my-component adds classes that don't depend on {{styleNamespace}} to each of the sub-components. These classes are only for customization and not used by the component itself.

{{!-- my-component.hbs, outer HTML --}}
<div class='{{styleNamespace}} my-component'>
  <div class='{{styleNamespace}}__sub-component my-component__sub-component'></div>
</div>
// my-component.scss
&__sub-component {
  background-color: white;
  color: rebeccapurple;
}

// .my-component and .my-component__sub-component don't appear in this file

Calling contexts target the "for-specialization" classes with regular CSS rules.

Static Attributes

In order to more clearly show that this is a hook for extensibility, we can turn the classes into attributes. For example,

{{!-- foo.hbs, outer HTML --}}
<div class='{{styleNamespace}}'>
  <div class='{{styleNamespace}}__sub-component' data-subcomponent='foo:bar'></div>
</div>
// calling CSS:
& {
  [data-subcomponent="foo:bar"] {
    color: chartreuse;
    background-color: magenta;
  }
}

Summary / Request

I'd love to hear about people's experiences with these different techniques. Do you have other ideas? What's a good medium for describing best practices for extensible component-css?

jamesarosen avatar May 01 '18 16:05 jamesarosen

I think i need a like bit of clearification. So are you saying that some addon author gives you access to “my-component” you have no way to customize those styles directly? You would have to do something with in a parent component or at the root level.. Am i reading that right? @jamesarosen

webark avatar May 02 '18 08:05 webark

If that is your use case. One thing that I do when dealing with customizing addon component styles, is just create a “my-component/styles.EXT” in my app directory, and customize the styles i want. This will get the same namespace that the addon has, and due to the app styles getting included after the vendor styles on the page, will overwrite the addon styles just due to css using the last declared thing.

It’s not as.. direct, but the times i have used it is just when tweaking one or two things do it hasn’t been an issue..

But maybe this is irrelevant due to your use case being different.

webark avatar May 02 '18 08:05 webark

I think i need a like bit of clearification. So are you saying that some addon author gives you access to “my-component” you have no way to customize those styles directly? You would have to do something with in a parent component or at the root level.. Am i reading that right?

Yes. Imagine the following scenario: ember-power-select uses ember-component-css to define its styles.

Its generated markup looks like

<div class='__0f5e3a'>
  Select…
  <div class='__0f5e3a__icon'></div>
<div>

And its CSS looks like

// .ember-power-select-trigger
.__0f5e3a {
  padding: 0 2rem 0 .5rem!important;
  background-color: transparent;
  border: 2px solid #fff2f2!important;
  border-radius: 4px;
  line-height: 1.6;
  white-space: nowrap;
  outline: 0;
}

// .ember-power-select-status-icon
.__0f5e3a__icon {
  border-width: 12px 7px 0;
  border-color: #fff transparent transparent;
}

Now in my app, I want to style those elements. The first one is easy since it's a top-level element:

{{ember-power-select class='my-custom-power-select'}}
.my-custom-power-select {
  background-color: blue;
  border-radius: 6px;
  color: white;
}

But I have no way of targeting the icon within.

just create a “my-component/styles.EXT”

Are you saying I can do

// my-app/app/styles/components/ember-power-select.scss
&__icon {
  ...
}

and it will be guaranteed to get the same class (__0f5e3a__icon) as the addon's version, which got compiled into vendor.css?

jamesarosen avatar May 02 '18 14:05 jamesarosen

@jamesarosen yes. The 0f5e3a is a md5 hash of the name of the component so it will be consistent across vendor and your app css. you should be able to do that.

webark avatar May 02 '18 15:05 webark