Search code examples
csssassmixinsbem

BEM generator mixin want to match MDL structure


So I have the following mixins to generate my BEM classes:

$es: '__';
$ms: '--';

@function to-string($selector) {
    $selector: inspect($selector); //cast to string
    $selector: str-slice($selector, 2, -2); //remove brackets
    @return $selector;
}

@function contains-modifier($selector) {
    $selector: to-string($selector);
    @if str-index($selector, $ms) {
        @return true;
    } @else {
        @return false;
    }
}

@function get-block($selector) {
    $selector: to-string($selector);
    $modifier-start: str-index($selector, $ms) - 1;
    @return str-slice($selector, 0, $modifier-start);
}

@mixin blck($block) {
    .#{$block} {
        @content;
    }
}

@mixin elem($element) {
    $selector: &;
    @if contains-modifier($selector) {
        $block: get-block($selector);
        @at-root {
            #{$selector} {
                #{$block+$es+$element} {
                    @content;
                }
            }
        }
    } @else {
        @at-root {
            #{$selector+$es+$element} {
                @content;
            }
        }
    }
}

@mixin modf($modifier) {
    @at-root {
      #{&}#{$ms+$modifier} {
            @content;
        }
    }
}

@include blck(block) {
    background: red;
    @include elem(child){
        color: blue;
    };
    @include modf(modifier) {
        background: blue;
        @include elem(child) {
            color: red;
        }
    }
}

Now this actually generates perfect BEM style code but I want to to match the MDL code structure which means I want to more specificity nest my modifiers form this

.block
.block--modifer

to

.bock.block--modifier

The reason for this as said before is to match MDL an example of this formatting can be seen here: https://github.com/google/material-design-lite/blob/master/src/card/_card.scss

Now I can almost get the desired effect by changing this line:

@mixin modf($modifier) {
    @at-root {
      #{&}#{$ms+$modifier} {
            @content;
        }
    }
}

To this:

@mixin modf($modifier) {
    @at-root {
      #{&}#{&}#{$ms+$modifier} {
            @content;
        }
    }
}

But that changes the CSS output from this:

.block {
  background: red;
}
.block__child {
  color: blue;
}
.block--modifier {
  background: blue;
}
.block--modifier .block__child {
  color: red;
}

To this:

.block {
  background: red;
}
.block__child {
  color: blue;
}
.block.block--modifier {
  background: blue;
}
.block.block--modifier .block.block__child {
  color: red;
}

Now as you can see this fixes the modifier specificity but breaks the modifier child.

The desired output is as follows:

.block.block--modifier .block__child {
  color: red;
}

You can see it all in action here: http://codepen.io/crashy/pen/wGWPvr


Solution

  • Here is something quick i came up with by forking your pen:

    1. Add the #{$selector} in modf mixin.

    2. Modify the get-block function so that it returns the actual base class. At first we slice the string up to the point of the modifier (--) and then we check if there are more than one classes in the concatenated string. If there are more than one, we get the first.

    Link from codepen: http://codepen.io/MKallivokas/pen/bpBYEg

    $es: '__'; // Element
    $ms: '--'; // Modifier
    $ns: 'ns'; // Name space (Set to null if un-wanted)
    
    @if($ns) {
      $ns: $ns + '-';
    }
    
    @function to-string($selector) {
        $selector: inspect($selector); //cast to string
        $selector: str-slice($selector, 2, -2); //remove brackets
        @return $selector;
    }
    
    @function contains-modifier($selector) {
        $selector: to-string($selector);
        @if str-index($selector, $ms) {
            @return true;
        } @else {
            @return false;
        }
    }
    
    @function get-block($selector) {
        // do what get-block was doing in order to
        // remove the modifier's part from the string
        $selector: to-string($selector);
        $modifier-start: str-index($selector, $ms) - 1;
        $selector: str-slice($selector, 0, $modifier-start);
        // remove the first dot
        $selector: str-slice($selector, 2, str-length($selector));
        // check if there is another dot
        $modifier-start: str-index($selector, '.') - 1;
        @if $modifier-start >= 0 {
          // if there's another dot we slice the string up to that point
          $selector: str-slice($selector, 0, $modifier-start);
        }
        // we insert the dot that we removed to the start
        $selector: str-insert($selector, '.', 1);
        @return $selector;
    }
    
    @mixin blck($block) {
      .#{$ns}#{$block} {
            @content;
        }
    }
    
    @mixin elem($element) {
        $selector: &;
        @if contains-modifier($selector) {
            $block: get-block($selector);
            @at-root {
                #{$selector} {
                    #{$block+$es+$element} {
                        @content;
                    }
                }
            }
        } @else {
            @at-root {
                #{$selector+$es+$element} {
                    @content;
                }
            }
        }
    }
    
    @mixin modf($modifier) {
      $selector: &;
      @at-root {
        #{$selector}#{$selector+$ms+$modifier} {
          @content;
        }
      }
    }
    
    @include blck(block) {
        background: red;
        @include elem(child){
            color: blue;
        };
        @include modf(modifier) {
            background: blue;
            @include elem(child) {
                color: red;
            }
        }
    }
    

    I don't know if it suits exactly your needs or covers every case of what you want to do but i hope it helps.