Lately, I've been working on the theming system of Kendo UI for Angular. The system has turned out rather nicely, providing a hefty set of features that we could not ship previously. One of the elements that allowed this is the (surprisingly simple) dependency infrastructure.

For some context, here is what our previous system lacked:

  • dependencies between components were hidden
  • no way to build the CSS for a list of components
  • reuse of styles in derived themes was hard to achieve

This post shows how the new styling system was implemented and how it solved the problems we faced.

The exports mixin

To create a simple dependency management system for CSS, only one thing is needed - a mechanism that allows files to be imported just once, before any of the files that require it. Ideally, this will be handled by SASS itself -- and it will, via the @import-once option scheduled for v4. Waiting for a new version is not a very practical solution, though. Fear not! One can define a mixin that achieves a similar result -- the exports mixin:

// mixins/_exports.scss
$_imported: () !default;

/// Module exports mixin
/// This mixin makes sure a module is imported only once.
/// @param {String} $name - Name of exported module
@mixin exports($name) {
    @if (index($_imported, $name) == null) {
        $_imported: append($_imported, $name) !global;
        @content;
    }
}

The mixin works in a very subtle fashion. When a file is included for the first time, the mixin stores an identifier ($name) in the $_imported map, and outputs the file. Later, if the same key is found when importing a file, the mixin does nothing - the file is already in the output.

Here is how it is used:

// @import 'mixins/exports'; is required in every file

// file: forms-layout.scss
@include exports("forms-layout") {
    // define common form element styles
}

// file: numeric-textbox.scss
@import 'forms-layout';
@include exports("numeric-textbox") {
    // define numeric textbox styles
}

// file: dropdownlist.scss
@import 'forms-layout';
@include exports("dropdownlist") {
    // define dropdownlist styles
}

// file: grid.scss
@import 'dropdownlist';
@import 'numeric-textbox';
@include exports("grid") {
    // define grid styles
}

With the above setup, the grid component will import the file forms-layout.scss twice, through the numeric-textbox.scss and through dropdownlist.scss. However, the exports mixin makes sure the forms styles are included just once.

What exports solves

Apart from outputting the styles just once, the mixin provides more important benefits that improve the usability of the styles.

Allows custom theme builds

If an application uses just a few components, its theme can be built by listing the components independently:

@import 'dropdownlist';
@import 'dialog';

Since each component lists its dependencies, the resulting styles will include both components, and their style dependencies.

Ensures correct style order

As long as shared styles are extracted in separate modules, all styles will be included in the correct order. This is of great benefit to consumers of the styles - they don't need to know the dependencies of a module before they use it. Even if a module is included multiple times, the output will include it before any of its dependencies.

Allows easy reuse

If anyone desires to change the styling of the numeric textbox without changing other parts of the system, it can be done by defining a module with the same name:

@include exports("numeric-textbox") {
    // completely different numeric styles
}

@import "grid";

Since the name is the same, the grid dependency of the numeric textbox is resolved, and only the new content is included.

Next steps

I have been quite happy with the system, since it solves code organization problems as well as providing features for the consumers of the styles. It is still early to tell if it will stand against the test of time, but early results seem very promising.

If you decide to use this approach, I would love to hear your story -- share it in the comments below!

Get new posts delivered to your inbox

Back to all posts