Something we talk about a lot, here at Clearleft, is the concept of the component library.

Graham Smith
Graham Smith
one week ago

It’s a key deliverable to a number of our clients, and to help ensure high quality, we even developed a tool to manage them, fractal. But it wasn’t until the recent refresh of our brand and website that we had a chance to use it on our own project and, as we like to say, eat our own dog food.

As I tweeted out the other day, we have opened up the library we built for this very site at fractal.clearleft.com, for all to see, learn from, etc…

Now, a pattern library in itself is a lovely thing. However, we know that its true power lies in being directly integrated into the system for which it is intended. In our case the CMS, Craft. And we have done just that. It is an imperfect solution, but good enough for now. We’ll be aiming to refine it in the coming weeks and months, but in the meantime I thought it would be useful to share our method as it currently stands.

What we’re looking to accomplish is have our CMS read in the pattern files directly and apply the data from its database to construct each page. Our site and our component library are two separate systems in two separate repositories.

The Challenges

  1. Craft is built in PHP and uses Twig as its templating language. The main issue we faced here was PHP security prevents us from including files from outside the site’s root directory. So we needed a way to easily import the patterns into the site so that they were accessible.

  2. In Fractal, including components uses a special naming system to reference other components, e.g: {% include '@card' %}. The @ symbol is effectively shorthand for ‘ID’, allowing component template files to be included into others without hardcoded paths, allowing them to be moved around the directory structure without breaking everything. But we would need a way for Craft/Twig to figure out what component file is required by any given ID.

Getting the Library into Craft

Even though the component library and the website, in our case, live on the same server, we can’t reference component files either directly, or by setting up a components folder in Craft and symlinking. Instead, due to Fractal being a Node app, we are able to import it into other apps/sites using NPM. So, in the clearleft.com package.json file we have:

{
	"name": "clearleft.com",
	"version": "0.1.0",
	"description": "Clearleft site v5",
	"main": "index.js",
	"dependencies": {
		"clearleft-fractal": "github:clearleft/clearfractal"
	},
	...
}

And so by running npm install the component library is pulled into the node_modules folder in the site. Then, keeping the library up to date is a simple case of running npm update or, to speed up development, you can use npm link to create an npm symlink, thereby skipping the manual update step.

Understanding the Fractal Paths

With the components now in the node_modules folder of the site, we could include patterns into Craft templates with something along the lines of:

{% include ‘../../node_modules/clearleft-fractal/components/your-directory-structure-here/card.twig’ %}

But that’s horribly fragile and still does not handle referencing other components using the @ symbol. So what we need to do is get Craft to ‘understand’ that symbol and figure out where that component lives.

At its core, Fractal is a file and directory management system onto which you can layer more complex functionality. Knowing that, we can use Fractal’s underlying mechanics to spit out a components-map.json file which contains a mapping of all the patterns IDs to their relative paths. Here is the Fractal function which creates the file:

/*
 * Handle => filesystem path mapping export.
 */

function exportPaths() {
	const map = {};
	for (let item of fractal.components.flatten()) {
		map[`@${item.handle}`] = path.relative(process.cwd(), item.viewPath);
	}
	fs.writeFileSync('components-map.json', JSON.stringify(map, null, 2), 'utf8');
}

That lives in the fractal.js file. To make it useful, we create a new Fractal command, pathmap to run it:

fractal.components.on('updated', function(){
    exportPaths();
});

fractal.cli.command('pathmap', function(opts, done){
    exportPaths();
    done();
});

Which we can run with fractal pathmap. The above code also sets fractal up to automatically export the file when a component changes.

Here is a snippet of the resulting path map when run from the root of the website:

{
  "@background-colours": "node_modules/clearleft-fractal/components/01-core/background-colours/background-colours.twig",
  "@headings": "node_modules/clearleft-fractal/components/01-core/headings/headings.twig",
  "@prose": "node_modules/clearleft-fractal/components/01-core/prose/prose.twig",
  "@author": "node_modules/clearleft-fractal/components/02-units/author/author.twig",
  "@card": "node_modules/clearleft-fractal/components/02-units/card/card.twig",
	...
}

Once we had this components-map.json file we created a Fractal plugin for Craft which loaded in a custom class, FractalTemplateLoader which extends Twig allowing us override some of the functionality. In this case, the pathfinding. So whenever Twig now looks up a file, a function, _findTemplate() kicks in:

private function _findTemplate($name)
{
    if (strpos($name, '@') === 0)
        {
            $mappingPath = defined('FRACTAL_COMPONENTS_MAP') ? FRACTAL_COMPONENTS_MAP : CRAFT_BASE_PATH . '../components-map.json';
            if (IOHelper::isReadable($mappingPath))
            {
                $mappings = json_decode(IOHelper::getFileContents($mappingPath));
                if ($mappings->$name) {
                    if (strpos($mappings->$name, '/') !== 0) {
                        $template = realpath(CRAFT_BASE_PATH . '../') . '/' . $mappings->$name;
                    } else {
                        $template = $mappings->$name;
                    }
                }
            }
		...
}		

Above is the most important part of the code. It’s checking to see if the path starts with the @ character. If it does, we check for the components-map.json file, decode it, then lookup the required pattern to find its relative path.

Provided the file is found okay, it is served up and from that the template files are populated with all the necessary Twig code.

The Assets

The final order of business is getting the assets, which are primarily developed and stored in the pattern library, into the site proper. What we do is use clearleft.com’s gulpfile to run the component library’s gulpfile from within the node_modules folder and store the output in the site’s public folder instead.

That about covers the meat of the problem. Thanks for reading!