Code.Art.Web

Code.Art.Web

Profile Picture

Lorefnon

Reducing BEM boilerplate through HAML extensions

Abstract

A hack which makes the HAML parser BEM aware and helps reduce some of the repetitive boilerplate associated with specifying BEM compliant class names in templates.


BEM is a nice pattern with the primary caveat being having to repetitively specify long class name prefixes in html.

In Sass this problem is significantly alleviated because the parent operator & allows for extending of parent class name:

.Button {
  &__label {
    &--primary {
      font-weight: bold
    }
  }
  &__icon {
    opacity: 0.5;
  }
}

compiles to:

.Button__label--primary {
  font-weight: bold;
}
.Button__icon {
  opacity: 0.5;
}

But usage in HTML is quite verbose:

<label class="Button__label Button__label--primary">
</label>

Fortunately, if we are using HAML, then we can extend the HAML parser to reduce some of this verbosity. HAML has a method Haml::Parser.parse_class_and_id to expand expressions like .hello.world#some-id. We can monkey-patch this method to make it bem aware.

Haml::Parser.class_eval do
  class << self

    CLASS_KEY = 'class'.freeze
    ID_KEY = 'id'.freeze

    def parse_class_and_id(list)
      attributes = {}
      return attributes if list.empty?

      list.scan(/([#.])([-:_a-zA-Z0-9]+)/) do |type, property|
        case type
        when '.'
          if attributes[CLASS_KEY]
            attributes[CLASS_KEY] += " "
          else
            attributes[CLASS_KEY] = ""
          end
          if match = property.match(/^bem:(.*)$/)
            property = expand_bem_class match[1]
          end
          attributes[CLASS_KEY] += property
        when '#'
          attributes[ID_KEY] = property
        end
      end
      attributes
    end

    def expand_bem_class(property)
      convert_to_classes(parse_bem_shorthand(property)).join(" ")
    end

    def parse_bem_shorthand(property)
      breakdown = [{}]
      previous_char = nil
      current_part = :block
      property.each_char do |char|
        breakdown.last[current_part] ||= ''
        if char == '_'
          if previous_char == '_'
            previous_char = nil
            unless current_part == :block
              breakdown.push(block: breakdown.last[:block])
            end
            current_part = :element
          else
            previous_char = char
          end
        elsif char == '-'
          if previous_char == '-'
            previous_char = nil
            unless current_part == :block || current_part == :element
              breakdown.push(
                block: breakdown.last[:block],
                element: breakdown.last[:element]
              )
            end
            current_part = :modifier
          else
            previous_char = char
          end
        else
          if previous_char == '_' || previous_char == '-'
            breakdown.last[current_part] += previous_char
          end
          breakdown.last[current_part] += char
        end
      end
      breakdown
    end

    def convert_to_classes(bem_breakdown)
      classes = []
      bem_breakdown.each do |item|
        current_class = ''
        if item[:block].blank?
          raise "BEM Block missing"
        else
          current_class += item[:block]
        end
        unless item[:element].blank?
          current_class += "__#{item[:element]}"
        end
        classes.push(current_class)
        unless item[:modifier].blank?
          current_class += "--#{item[:modifier]}"
          classes.push(current_class)
        end
      end
      classes.uniq
    end

  end

end

Now classes prefixed with bem: will receive special treatment:

.bem:Header__row--secondary--dark
  %a Hello

will compile to:

<div class="Header__row Header__row--secondary Header__row--dark">
</div>

Note that we were able to chain multiple modifiers in single expression. We can also use multiple elements in single expression:

.bem:Header__row--secondary--dark__bar--clear
  %a Hello

will compile to:

<div class="Header__row Header__row--secondary Header__row--dark Header__bar Header__bar--clear">
</div>

Of course this works with arbitrary selectors:

%header.bem:Header__row--secondary--dark
  %a Hello

will compile to:

<header class="Header__row Header__row--secondary Header__row--dark">
</header>

What about cases when we want to use ruby helpers ? We can define two helper methods that expose this functionality:

def expand_bem_class(name)
  Haml::Parser.expand_bem_class(name)
end

def expand_bem_classes(name)
  name.split(".").map{|name| expand_bem_class name }.join(" ")
end

Of course, the usual caveats of tinkering with internals of vendor libraries apply. If tommorrow HAML changes the compiler API, then the monkey patch would have to updated to accomodate for that.

comments powered by Disqus
Separator line
Separator line
Lorefnon

Full stack web developer and polyglot programmer with strong interest in dynamic languages, web application development and user experience design.


Strong believer in agile methodologies, behaviour driven development and efficacy of open source technologies.


© 2013 - 2015 Gaurab Paul


Code licensed under the The MIT License. Content and Artwork licensed under CC BY-NC-SA.


The opinions expressed herein are my personal viewpoints and may not be taken as professional recommendations from any of my previous or current employers.


Site is powered by Jekyll and graciously hosted by Github