Skip to content

Instantly share code, notes, and snippets.

@kilmc
Last active February 9, 2020 19:58
Show Gist options
  • Select an option

  • Save kilmc/600c49694406b368eedecda829e46ed8 to your computer and use it in GitHub Desktop.

Select an option

Save kilmc/600c49694406b368eedecda829e46ed8 to your computer and use it in GitHub Desktop.
Battery Docs

Battery

How Battery works

The core of Battery is the propertyConfig. These objects configure Battery to recognize valid class names from your Atomic CSS library and then convert them into full CSS classes. Here's an example of a propertyConfig for background-color.

const backgroundColor = {
  property: "background-color",
  propertyIdentifier: "bg",
  valueSeparator: "-",
  values: {
    black: "#000000",
    white: "#FFFFFF",
    transparent: "transparent"
  }
};

The code example below is an extremely simplified version how Battery converts the contents of a propertyConfig into CSS.

Object.entries(backgroundColor.values).forEach(([valueIdentifier, value]) => {
  const { propertyIdentifier, valueSeparator, property } = backgroundColor;

  const className = `${propertyIdentifier}${valueSeparator}${valueIdentifier}`;
  const cssClass = `.${className} { ${property}: ${value} }`;
  console.log(cssClass);
});
// Output

.bg-black { background-color: #000000 }
.bg-white { background-color: #FFFFFF }
.bg-transparent { background-color: transparent }

More examples

There are several other properties in our library that use colors. Below we'll create propertyConfigs for each of them.

const textColor = {
  property: "color",
  propertyIdentifier: "text",
  valueSeparator: "-",
  values: {
    black: "#000000",
    white: "#FFFFFF",
    transparent: "transparent"
  }
};
const fillColor = {
  property: "fill",
  propertyIdentifier: "fill",
  valueSeparator: "-",
  values: {
    black: "#000000",
    white: "#FFFFFF",
    transparent: "transparent"
  }
};

With these propertyConfigs Battery can now recognize and generate the following classes:

const input = [
  "bg-black",
  "bg-white",
  "bg-transparent",
  "text-black",
  "text-white",
  "text-transparent",
  "fill-black",
  "fill-white",
  "fill-transparent"
];
// Output

.bg-black { background-color: #000000 }
.bg-white { background-color: #FFFFFF }
.bg-transparent { background-color: transparent }
.text-black { color: #000000 }
.text-white { color: #FFFFFF }
.text-transparent { color: transparent }
.fill-black { fill: #000000 }
.fill-white { fill: #FFFFFF }
.fill-transparent { fill: transparent }

Duplication, extraction and plugins

The propertyConfigs for background-color, color and fill above work, but they introduce a lot of duplication. In the next section we'll learn how to DRY this code up by using Battery's plugins.

Lookup plugins

In this section we'll cover the lookup type of plugin. This is one of two plugin types that affect a classes value.

LookupPlugins allow us to create a mapping between value names and the outputted CSS values when there is no 1-1 relationship between them; there's no way to infer that black means #000000 so we need to associate them with one another.

const colorPlugin = {
  // 'type' defines the plugin type (who'da thought?)
  type: "lookup",
  // 'name' allows us to refer to this plugin in propertyConfigs
  name: "color",
  // The 'values' object is the map between the valueIdentifier
  // in a class name (e.g. the 'white' in 'bg-white') and the
  // hex value '#FFFFFF'.
  values: {
    black: "#000000",
    white: "#FFFFFF",
    transparent: "transparent"
  }
};

Using the plugin

We can now use this plugin in our configs by removing the values object of the propertyConfig and replacing it with a plugin.

// Without plugin
const backgroundColor = {
  property: "background-color",
  propertyIdentifier: "bg",
  valueSeparator: "-",
  values: {
    black: "#000000",
    white: "#FFFFFF",
    transparent: "transparent"
  }
};
// With plugin
const backgroundColor = {
  property: "background-color",
  propertyIdentifier: "bg",
  pluginSeparator: "-",
  plugin: "color"
};

We can repeat this process for the other two propertyConfigs to get everything in sync and fully DRY'd up.

const textColor = {
  property: "color",
  propertyIdentifier: "text",
  pluginSeparator: "-",
  plugin: "color"
};

const fillColor = {
  property: "fill",
  propertyIdentifier: "fill",
  pluginSeparator: "-",
  plugin: "color"
};

Note: Above we've replaced the valueSeparator with pluginSeparator. This is because Battery allows you to use a plugin in addition to manually added values on your propertyConfig. We will see an example of how this can come in handy in a later section.

Adding new values

The other big benefit of having a plugin is that if you want to add a new color to your Atomic Library, you can do so by editing the values object in the colorPlugin. This will make those new values available to all propertyConfigs that use colorPlugin.

const colorPlugin = {
  type: "lookup",
  name: "color",
  values: {
    black: "#000000",
    white: "#FFFFFF",
    transparent: "transparent",
    red: "#ff0000",
    green: "#00ff00",
    blue: "#0000ff"
  }
};

Now color, background-color and fill properties all have access to the red, green and blue values.

Pattern plugins

The second plugin type that affects CSS values is the pattern type. This is best suited to class names where there is more of a 1-1 relationship between the valueIdentifier and the outputted CSS value.

Below we're going to create a plugin that will allow Battery to recognize and generate classes that use positive and negative integers.

const integerPlugin = {
  // 'type' defines the plugin type
  type: "pattern",
  // 'name' allows us to refer to this plugin in propertyConfigs
  name: "integer",
  // `valueIdentifier` is a regex defines what values are valid for this plugin
  valueIdentifier: /-?\d{1,4}/
};

The regex in valueIdentifier is used as a way to limit the values that we want to accept to any numbers between -9999 and 9999.

Using the plugin

const zIndex = {
  property: "z-index",
  propertyIdentifier: "z",
  plugin: "integer"
};

const order = {
  property: "order",
  propertyIdentifier: "order",
  plugin: "integer"
};

const flexGrow = {
  property: "flex-grow",
  propertyIdentifier: "grow",
  plugin: "integer"
};

In the following examples, the integerPlugin is matching on the integers found in the class names: 1 in z1, 2 in order2, 4 in grow4 and -9999 in z-9999.

const input = ["z1", "order2", "grow4", "z-9999"];
.z1 { z-index: 1 }
.order2 { order: 2 }
.grow4 { grow: 4 }
.z-9999 { z-index: -9999 }

As you can see in the example above, the value you write after your propertyIdentifier (z,order, grow etc.) gets passed straight through to the value part of the CSS declaration.

Now we've covered both the lookup and the pattern type of plugin. Next we will move onto plugin modifiers which are a way to supercharge both lookup and pattern type plugins.

Giving plugins superpowers

Let's take a look at another set of properties that use similar values. Specifically properties that use length units like px, em, %, vh and vw.

Requirements

For this example we're going to start with the width CSS property. We want to add two types of length units: percentages, and spacer units. In the end Battery will be able to convert the following classes into CSS.

const input = ['w100p','w50p','w4x','w1x'];
// Output

.w100p { width: 100% }
.w50p { width: 50% }
.w4x { width: 2rem }
.w1x { width: 0.5rem }

Your first modifier

The base of this plugin is going to be quite similar to the integerPlugin that we made previously. Essentially we want to match class names where the valueIdentifier is 1–3 consecutive digits.

const lengthUnitPlugin = {
  type: "pattern",
  name: "lengthUnit",
  valueIdentifier: /\d{1,3}/
};

For us to be able to differentiate between percent and spacer length units we need to add modifiers. Let's first add support for percent length units.

const lengthUnitPlugin = {
  type: "pattern",
  name: "lengthUnit",
  valueIdentifier: /\d{1,3}/,
  modifiers: [
    {
      name: "percent",
      // 'name' allows us to refer to this modifier in propertyConfigs
      identifier: "p",
      // 'identifier' is what Battery uses to distinguish one modfier from the other
      modifierFn: value => `${value}%`
      // 'modifierFn' is called on the value extracted from the class name to convert it into the desired CSS value.
    }
  ]
};

Adding this modifier means Battery will only match class names where there is a p at the end of the valueIdentifier.

const valueIdentifierRegex = /(\d{1,3})(p)/;

Doing the conversion

Now that we've gotten our plugin setup let's create the propertyConfig for width so that Battery can recognize and convert classes like w100p and w50p into CSS.

const width = {
  property: "width",
  propertyIdentifier: "w",
  plugin: "lengthUnit"
};

If we take w100p as a sample class to convert to CSS, Battery knows the following pieces of information:

  • w is the propertyIdentifier for the css property width
  • width uses the lengthUnit plugin
  • The value 100 matches the (\d{1,3}) regex in the lengthUnit plugin
  • The p matches the percent modifier

With all that information, Battery will pass the value 100 to the modifierFn associated with percent and the CSS value 100% will be returned. That gets set as the value of the width declaration and the final output becomes.

.w100p { width: 100% }

Adding a second modifier

We've added support for percent but what about spacer. Let's add a second modifier so that we can make this plugin even more useful.

const lengthUnitPlugin = {
  type: "pattern",
  name: "lengthUnit",
  valueIdentifier: /\d{1,3}/,
  modifiers: [
    {
      name: "percent",
      identifier: "p",
      modifierFn: value => `${value}%`
    },
    {
      name: "spacer",
      identifier: "x",
      modifierFn: value => `${parseInt(value) * 0.5}rem`
    }
  ]
};

Battery will now accept either p or x at the end of the digits.

const valueIdentifierRegex = /(\d{1,3})(p|x)/;

In the same process as above, Battery takes in the class name w4x and figures out all the same information except, it will now recognize the x at the end as being associated with the spacer modifier and use the modifierFn associated with that to convert 4 into 2rem.

Handling shorthand CSS properties

We've looked at how values that are common to multiple CSS properties can be shared via plugins. In this section we're going to take a look at another area that is often pretty duplicative which is shorthand properties.

A propertyConfig with subProperties

We're going to start by setting up our basic propertyConfig for margin.

const margin = {
  property: "margin",
  propertyIdentifier: "m",
  plugin: "lengthUnit"
};

Based on the lengtUnit plugin from previous sections we can now access classes like m2x or m10p. But what if we want to access classes for margin-top, margin-right, margin-bottom and margin-left?

One way to do this would be to duplicate the propertyConfig for each of those.

const marginTop = {
  property: "margin-top",
  propertyIdentifier: "mt",
  plugin: "lengthUnit"
};

const marginRight = {
  property: "margin-right",
  propertyIdentifier: "mr",
  plugin: "lengthUnit"
};

const marginBottom = {
  property: "margin-bottom",
  propertyIdentifier: "mb",
  plugin: "lengthUnit"
};

const marginLeft = {
  property: "margin-left",
  propertyIdentifier: "ml",
  plugin: "lengthUnit"
};

While this is valid, it introduces the same problems we discussed when we extracted the color values into the colorPlugin where there's a chance for things to get out of sync and maintainence becoming more difficult.

Introducing subProperties

In CSS margin is classified as a "shorthand" property. It rolls up the values of margin-top, margin-right, margin-bottom and margin-left.

In Battery we invert the idea of shorthand and instead consider top,right,bottom and left as "sub properties". The idea is that you set up your propertyConfig for the shorthand and then make all of those same settings available to the subProperties.

const margin = {
  property: "margin",
  propertyIdentifier: "m",
  subProperties: {
    top: "t",
    right: "r",
    bottom: "b",
    left: "l"
  },
  plugin: "lengthUnit"
};

In the background Battery is creating propertyConfigs for each of the subProperties. These look identical to the examples we showed above where we manually wrote each of those configs out.

Given the addition of the subProperties config, here are some examples of class names that are now valid and can be converted into CSS.

const input = ["mb2x", "mt50p", "mr1x", "m3x", "ml100p"];
.mb2x { margin-bottom: 1rem }
.mt50p { margin-top: 50% }
.mr1x { margin-right: 0.5rem }
.m3x { margin: 1.5rem }
.ml100p { margin-left: 100% }

Finally, like previous examples, if we want to add a value the margin propertyConfig it will automatically become available to all of the subProperties. In the following example we add auto via the values object.

const margin = {
  property: "margin",
  propertyIdentifier: "m",
  subProperties: {
    top: "t",
    right: "r",
    bottom: "b",
    left: "l"
  },
  valueSeparator: '-',
  values: {
    auto: 'auto'
  }
  plugin: "lengthUnit"
};

We now have access to the following class names:

const input = ["m-auto", "mt-auto", "mr-auto", "mb-auto", "ml-auto"];
.m-auto { margin: auto }
.mt-auto { margin-top: auto }
.mr-auto { margin-right: auto }
.mb-auto { margin-bottom: auto }
.ml-auto { margin-left: auto }

Selector plugins

In previous sections we talked about plugins that affect the value part of a CSS class. Now we're going to look at plugins that allow us to modify the CSS selector.

Note: selector plugins are limited by their position in the class name. The identifier for a selector plugin has to be either at the start or the end of the class name.

For example, if we wanted to add support for the :hover pseudo selector we could have our class name become something like hover-bg-black or bg-black-hover. However, we could not do bg-hover-black.

Adding support for :hover

Let's create a plugin that would allow us to prefix any class in our library with hover- to apply that style when the element was hovered.

const pseudoPlugin = {
  type: "selector",
  name: "pseudo",
  // `affixType` specifies where on the class name to expect the identifier
  affixType: "prefix",
  modifiers: [
    {
      name: "hover",
      identifier: "hover",
      separator: "-",
      modifierFn: selector => `${selector}:hover`
    }
  ]
};

Unlike lookup and pattern, selector plugins can apply to all class names in your library. By including the pseudoPlugin in your BatteryConfig the hover- prefix is now available for any class name.

Taking this sample input, here's the CSS that gets generated:

const input = ["hover-bg-black", "hover-text-white", "hover-z100"];
// Output

.hover-bg-black:hover { background-color: #000000 }
.hover-text-white:hover { color: #FFFFFF }
.hover-z100:hover { z-index: 100 }

Extending

Now that we have support for :hover we can pretty easily add support for :focus.

const pseudoPlugin = {
  type: "selector",
  name: "pseudo",
  affixType: "prefix",
  modifiers: [
    {
      name: "hover",
      identifier: "hover",
      separator: "-",
      modifierFn: selector => `${selector}:hover`
    },
    {
      name: "focus",
      identifier: "focus",
      separator: "-",
      modifierFn: selector => `${selector}:focus`
    }
  ]
};
const input = ["focus-bg-black", "focus-text-white", "focus-z100"];
// Output

.focus-bg-black:focus { background-color: #000000 }
.focus-text-white:focus { color: #FFFFFF }
.focus-z100:focus { z-index: 100 }

Atrule plugins

The last plugin type is the at-rule. The most common use is for media quries but the plugin archetecture is flexible enough to cater for other uses.

Similarly to the selector plugin, at-rule plugins apply to all classes in your library once the plugin is included in your BatteryConfig.

A breakpoint plugin

We want to add responsive classes to our Atomic Library. We're going to start by adding a min-width breakpoint at 600px.

const breakpointPlugin = {
  name: "breakpoint",
  type: "at-rule",
  // 'atrule' specifies which type of atrule this plugin should target
  atrule: "media",
  affixType: "suffix",
  modifiers: [
    {
      name: "responsiveSmall",
      identifier: "sm",
      separator: "-",
      // 'condition' sets the condition of the atrule
      condition: "(min-width: 600px)"
    }
  ]
};

Adding more breakpoints can by done by adding modifiers.

[
  // ...
  {
    name: "responsiveMedium",
    identifier: "md",
    separator: "-",
    condition: "(min-width: 900px)"
  },
  {
    name: "responsiveLarge",
    identifier: "lg",
    separator: "-",
    condition: "(min-width: 1050px)"
  }
];

With this plugin added we can now use the following class names in our Atomic Library.

const input = ["w100p", "w50p-sm", "d-block-md", "bg-white-lg"];
// Output

.w100p { width: 100% }

@media (min-width: 600px) {
  .w50p-sm { width: 50% }
}

@media (min-width: 900px) {
  .d-block-md { display: block }
}

@media (min-width: 1050px) {
  .bg-white-lg { background-color: #FFFFFF }
}

Note: Modifiers in at-rule plugins are order dependent. If you are using a mobile-first breakpoint system, make sure to define the modifiers in order of smalles screensize to largest to ensure your classes cascade correctly.

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