Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Where extended placeholder selectors go wrong in libsass

When working between the Ruby version of Sass and libsass, we are constantly reminded that these are two very separate implementations of a specification. Things are bound to go wrong. And here is a very real example.

The github thread

If you are interested in following along and/or are able to assist with this difficult functionality that is so desperately needed in the community, please follow this thread.

What goes wrong?

When writing an placeholder selector in Ruby Sass, you could so the following code example. Here I created a parent selector, but using the % syntax the whole selector never appears in the output CSS until the placeholder selector is extended with a standard selector.

%new-parent {
  border-width: 1px;
  border-style: solid;
  .background-color {
    background-color: orange;
  }
  .add-border {
    border: 1px solid red;
  }
}

Using the placeholder selector with a standard selector:

.outer-box {
  @extend %new-parent;
}

we get the following processed CSS with Ruby Sass:

.outer-box {
  border-width: 1px;
  border-style: solid;
}
.outer-box .background-color {
  background-color: orange;
}
.outer-box .add-border {
  border: 1px solid red;
}

Using the same Sass example from above, but using the libsass library to process the CSS, I get the following:

.outer-box {
  border-width: 1px;
  border-style: solid; }
  %new-parent .background-color {
    background-color: orange; }
  %new-parent .add-border {
    border: 1px solid red; }

Without digging too far into details, what we see happening here is that the extension of the parent selector is not being carried forward into the child selectors and simply outputting the silent selector's name. And of course, this is not valid CSS and will render nothing.

Not is all lost!

This is not to say that extends are totally broken in libsass. If I were to create another complex selector with nested selectors, instead this time all the selectors are placeholders, there is no issue with the output.

Notice in the following how all selectors are designated with the % symbol which renders them invisible from processing.

%default-parent {
  border-width: 1px;
  border-style: solid;
  %set-background-color {
    background-color: orange;
  }
  %set-border {
    border: 1px solid red;
  }
}

To allow for all the placeholder selectors to be processed into CSS, I simply need to extend them to a standard selector as illustrated in the following example:

.parent {
  @extend %default-parent;
  .block {
    @extend %set-background-color;
    @extend %set-border;
  }
  .another-block {
    @extend %set-border;
  }
}

This technique will produce the following CSS:

.parent {
  border-width: 1px;
  border-style: solid; }
  .parent .block {
    background-color: orange; }
  .parent .block, .parent .another-block {
    border: 1px solid red; }

This is actually ok

Really, when you think about it, this is still very useful. The technique of rendering pre-written CSS selectors within a more complex parent selector is an easy way to reproduce UI code, but are you really reusing properties correctly? If you need to reuse a complex selector's properties, but a child selector has a different name for one reason or another, this will break.

The technique of making every child selector within a parent selector a placeholder allows for greater flexibility in application. Not only with being able to change the name of the standard selector which will be extended to, but the ability to NOT extend a CSS rule.

In the following example, I am calling the %default-parent placeholder selector, but not extending the nested %set-border placeholders. The following output represents this decision clearly.

.parent {
  border-width: 1px;
  border-style: solid; }
  .parent .block {
    background-color: orange; }

There is one more issue

I recently discovered that even going with the 'all placeholder selector' model, there is one more issue, pseudo elements. In the following example notice how all the selectors within are placeholders and within %tab-ui-pattern is &:last-child.

%nav-ui-pattern {
  width: 100%;
  border-bottom: 1px solid black;
  margin-bottom: em(20);
  %tab-ui-pattern {
    display: inline-block;
    padding: em(20) em(40);
    margin-right: em(20);
    border: {
      width: 1px 1px 0 1px;
      style: solid;
      color: black;
      radius: em(5) em(5) 0 0;
    }
    &:last-child {
      margin: 0;
    }
  }
}

If I were to use this pattern in the following way:

.primary-header-navigation {
  @extend %nav-ui-pattern;
  a {
    @extend %tab-ui-pattern;
  }
}

Currently in libsass, I will get the following output. Notice were we see %nav-ui-pattern %tab-ui-pattern:last-child, but are expecting .primary-header-navigation a:last-child.

.primary-header-navigation {
  width: 100%;
  border-bottom: 1px solid black;
  margin-bottom: 1.25em; }
  .primary-header-navigation a {
    display: inline-block;
    padding: 1.25em 2.5em;
    margin-right: 1.25em;
    border-width: 1px 1px 0 1px;
    border-style: solid;
    border-color: black;
    border-radius: 0.3125em 0.3125em 0 0; }
    %nav-ui-pattern %tab-ui-pattern:last-child {
      margin: 0; }

This last part is addressable using Mixins, but that is far from ideal.

In closing

So in the end, extends in libsass perform much as you would expect them to with single selectors. I am not saying that it's implementation is perfect, the libsass team has stated that this needs to be addressed. But now at least you have an example where this will fail and how it will fail.

Check it out for yourself at SassMeister

// ----
// libsass (v0.7.0)
// ----
// Where extends go wrong in libsass
// In the following placeholder selector, all the selectors are created as
// such using the % syntax
%default-parent {
border-width: 1px;
border-style: solid;
%set-background-color {
background-color: orange;
}
%set-border {
border: 1px solid red;
}
}
// For all parts to be properly extended, all the nested selectors will need to
// be extended referencing the placeholder from the previous example
.parent {
@extend %default-parent;
.block {
@extend %set-background-color;
@extend %set-border;
}
.another-block {
@extend %set-border;
}
}
// What's wrong is, if you were to create a placeholder that contains
// real selectors within that you intended to appear without having to
// re-assign, your output will not be as expected as
// illustrated in the following example:
%new-parent {
border-width: 1px;
border-style: solid;
.background-color {
background-color: orange;
}
.border {
border: 1px solid red;
&:last-child {
border-color: orange;
}
}
}
// You will notice in the processed CSS that the silent placeholder selector
// is not replaced with the non-silent selector with the nested selectors
.outer-box {
@extend %new-parent;
}
// What continues to be an issue, much like the previous example, even if
// create a new module using only silent placeholders, using pseudo CSS
// options like :last-child suffer the same ill fate
%nav-ui-pattern {
width: 100%;
border-bottom: 1px solid black;
margin-bottom: em(20);
%tab-ui-pattern {
display: inline-block;
padding: em(20) em(40);
margin-right: em(20);
border: {
width: 1px 1px 0 1px;
style: solid;
color: black;
radius: em(5) em(5) 0 0;
}
&:last-child {
margin: 0;
}
}
}
.primary-header-navigation {
@extend %nav-ui-pattern;
a {
@extend %tab-ui-pattern;
}
}

michaek commented Sep 16, 2014

Hi, Dale. I just ran your example .scss against the work my team is currently doing on libsass, and the output is now identical to the output from Ruby Sass. Just wanted to let you know there's light on the horizon!

michaek commented Oct 8, 2014

And, as you probably know, libsass 3.0.0 is in rc now, with support for @extend!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment