Skip to content

Instantly share code, notes, and snippets.

@apfelbox
Created March 11, 2015 10:56
Show Gist options
  • Select an option

  • Save apfelbox/26605e0d3d2adafd55d9 to your computer and use it in GitHub Desktop.

Select an option

Save apfelbox/26605e0d3d2adafd55d9 to your computer and use it in GitHub Desktop.

Revisions

  1. apfelbox renamed this gist Mar 11, 2015. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. apfelbox created this gist Mar 11, 2015.
    219 changes: 219 additions & 0 deletions Article
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,219 @@
    The double ampersand -- or as A list apart called it ["lobotomized owl selector"](http://alistapart.com/article/axiomatic-css-and-lobotomized-owls) -- is a CSS rule that looks like the following:

    ```css
    * + * {
    /* some declarations */
    }
    ```

    You can also use it with a specific selector:
    ```css
    p + p {
    margin-top: 1.5rem;
    }
    ```

    This article now looks at implementing this functionality in Sass using a mixin.


    # The simple implementation
    ```scss
    @mixin double-ampersand {
    & + & {
    @content;
    }
    }
    ```

    Usage is like this:
    ```scss
    p {
    @include double-ampersand {
    margin-top: 1.5rem;
    }
    }
    ```

    ## The problem
    The issue with this mixin will be apparent as soon as you use nested selectors. In a nested selector the CSS should look like this:

    ```css
    .content p + p {
    margin-top: 1.5rem;
    }
    ```

    If you now go to your Sass file and write:
    ```scss
    .content p {
    @include double-ampersand {
    margin-top: 1.5rem;
    }
    }
    ```

    it will not produce the desired output. Instead it will produce

    ```css
    .content p + .content p {
    margin-top: 1.5rem;
    }
    ```

    # Improved mixin
    The improved mixin will only duplicate the last selector.
    So let's first look at the desired output and afterwards discuss the implementation


    It should support simple selectors, ...

    ```scss
    p {
    @include better-double-ampersand {
    margin-top: 1.5rem;
    }
    }

    // ... should produce ...

    p + p {
    margin-top: 1.5rem;
    }
    ```

    ... multiple selectors, ...


    ```scss
    a,
    p {
    @include better-double-ampersand {
    margin-top: 1.5rem;
    }
    }

    // ... should produce ...

    a + a,
    p + p {
    margin-top: 1.5rem;
    }
    ```

    ... nested selectors, ...


    ```scss
    .content p {
    @include better-double-ampersand {
    margin-top: 1.5rem;
    }
    }

    // ... should produce ...

    .content p + p {
    margin-top: 1.5rem;
    }
    ```

    ... multiple nested selectors, ...

    ```scss
    .test p,
    .content p {
    @include better-double-ampersand {
    margin-top: 1.5rem;
    }
    }

    // ... should produce ...

    .test p + p,
    .content p + p {
    margin-top: 1.5rem;
    }
    ```

    ... and multiple nested selectors with different last selectors.

    ```scss
    .test p,
    .content a {
    @include better-double-ampersand {
    margin-top: 1.5rem;
    }
    }

    // ... should produce ...

    .test p + p,
    .content a + a {
    margin-top: 1.5rem;
    }
    ```

    So, here goes. This is the commented code that solves all requests (it is a bit verbose, sorry about that).
    It tries to use the existing `&` behaviour and only breaks out of it, if it would produce invalid selectors.

    ```scss
    // This function will return the last item in a list
    @function last ($list) {
    @return nth($list, length($list));
    }

    // This function implements the improved double ampersand algoithm
    @mixin better-double-ampersand {
    // at first we need to reference the list of selectors for which
    // this mixin is called (we will call that "caller selectors" from now on)
    //
    // For
    // p .test, a { @include better-double-ampersand { /* ... */ }; }
    // this will be
    // (p .test, a)
    $caller-selectors: &;

    // We need to track whether the last selector for all caller selectors
    // is the same. If it isn't we need to perform some special handling
    $has-same-last-caller-selector: true;

    // For checking whether all last selectors are the same. Store the first one
    // and compare all other last selectors to it.
    $previous-last-selector: last(nth($caller-selectors, 1));

    // A list of prepared separators. If the last selectors are not the same,
    // we need to create our own block with the prepared selectors.
    $prepared-selectors: ();

    // Loop through all caller selectors to
    // - check for the last
    // - generate prepared selectors
    @each $selector in $caller-selectors {
    $last: last($selector);
    @if ($previous-last-selector != $last) {
    $has-same-last-caller-selector: false;
    }

    // generate prepared selector
    $prepared-selectors: append($prepared-selectors, #{$selector} + #{$last}, comma);
    }


    @if ($has-same-last-caller-selector) {
    // if all selectors have the same last selector
    // we can just use the regular `&` functionality
    & + #{$previous-last-selector} {
    @content;
    }
    } @else {
    // If not all selectors are the same, we need to render a completely
    // own block
    @at-root #{$prepared-selectors} {
    @content;
    }
    }
    }
    ```

    Please note that this code currently doesn't work in libsass. :(
    You can inline the `last()` function, if you want to keep it all inside of one mixin.