Cross Component Fragility

There are multiple ways how you can split up your code in components. Splitting up code might seem simple from the start, but often you will need to maintain some sort of link between these components.

There are loads of ways of how you could link components together, but the way how you do this can bring issues or complexity with it. So let's go over what your options are.

Using an image #

When you are using an image inside a component, you are encouraged to put that image inside of it’s component folder. 

Let’s take an example, if you are using a smile.png to theme the node--article.html.twig, you should put that image inside the most-specific folder that you have at that moment. So that would mean your component would look like this:

node
node--article
Twig Created with Sketch.
node--article.html.twig
smile.png

If you have the imageoptim feature enabled, the Gulp-setup will automatically generate an optimised version of that PNG in a sibling “dist” folder:

node
node--article
dist
smile.png
Twig Created with Sketch.
node--article.html.twig
smile.png

However, if you want to load in that image through Twig, CSS or JavaScript, the rule is: the component-level is the default. So if that image belongs to only 1 component, it’s a very simple rule to follow.

So for the sake of showcasing simplicity, let’s see how that would look if we load it in with CSS:

node
node--article
dist
css3-full Created with Sketch.
node--article.css

.node--article {
  background: url('smile.png');
}

smile.png
YAML Created with Sketch.
libraries.yml
Twig Created with Sketch.
node--article.html.twig
sass Created with Sketch.
node--article.scss
.node--article {
  background: url('smile.png');
}

smile.png

So in many cases this is the wanted end-result. Super-simple Sass and CSS code that links to an image within the same component.

Above component is very easy to understand and maintain. But what do we do when we create another component that needs that same smile.png?

Follow along for a series of possibilities, as we slowly improve this edge case until we get to a point that the approach is both maintainable, scalable while staying DRY (Don't Repeat Yourself).

One image for multiple components #

A) Maybe just duplicate the image?

The easiest approach would be to simply duplicate the image in to the next component. This might go against the DRY principle, but in this way we do keep independent components.

Assuming we need this image also in the node--blog component, our folder structure would look like this:

node
node--article
Twig Created with Sketch.
node--article.html.twig
sass Created with Sketch.
node--article.html.twig
smile.png
node--blog
Twig Created with Sketch.
node--blog.html.twig
sass Created with Sketch.
node--blog.scss
smile.png

(Leaving out the dist folders and libraries.yml files for simplicity sake)

As a big pro to this approach: both components are incredibly simple to understand.

However, duplicating your images like this, has implications for the browsers that will load in the assets of these components. Each image would be seen as an entirely different image. Therefore the browser will have to download and cache each smile.png individually, which would mean you bloat your page-size quite a bit.

If those 2 components are never to be loaded on the same page, you could potentially consider this approach, but in most cases, this is not ideal. So what other options do you have?

B) Find the most specific component.

What if we find the component that both images have in common? In our example that would mean the node component.

Then our structure would look like this:
 

node
node
dist
smile.png
Twig Created with Sketch.
node.html.twig
smile.png
node--article
dist
css3-full Created with Sketch.
node--article.css
Twig Created with Sketch.
node--article.html.twig
sass Created with Sketch.
node--article.scss
.node--article {
  background-image: url('../../node/dist/smile.png');
}

node--blog
dist
css3-full Created with Sketch.
node--blog.css
Twig Created with Sketch.
node--blog.html.twig
sass Created with Sketch.
node--blog.scss
.node--blog {
  background-image: url('../../node/dist/smile.png');
}

While we now no longer have a duplicated image, we now have duplicated a fragile looking url towards that image: Going up 2 levels and then in to another component's dist folder: "../../node/dist/smile.png".

Not only will this lack of simplicity annoy you, it is also very fragile. The fragility arrives from letting the node--article component rely on a different component being in an exact location.

This also means that if someone else opens up the node-component, we would have no indication of which other component is relying on that image. Nor would there be any indication for other developers to not rename the node-component.

Let's illustrate another scenario that would break this component in unpredictable ways: let's abstract view mode components for the node--blog component:

node
node
dist
smile.png
Twig Created with Sketch.
node.html.twig
smile.png
node--article
Twig Created with Sketch.
node--article.html.twig
sass Created with Sketch.
node--article.scss
node--blog
node--blog--full
Twig Created with Sketch.
node--blog--full.html.twig
sass Created with Sketch.
node--blog--full.scss
node--blog--teaser
Twig Created with Sketch.
node--blog--teaser.html.twig
sass Created with Sketch.
node--blog--teaser.scss

Which would mean you would now need to remember that moving a component around, you would need to change it’s Sass. 

Refactoring is key for any frontend project, so making component not-independent, and making them rely on each other in such a fragile way, is just not an acceptable solution.

For future reference, we will call this problem cross-component fragility later on in this blogpost.

C) A CSS-only component

Not having the image duplicated in 2 components feels good, but we want to avoid any cross-component fragilities.

So let's explore the idea of a self-independent CSS-only component. Let’s call this component: “smileys”. Then the inner folder structure of this component would look like this:
 

smileys
dist
css3-full Created with Sketch.
smileys.css
smile.png
YAML Created with Sketch.
libraries.yml
smileys:
  version: VERSION
  css:
    component:
      dist/smileys.css: {}

sass Created with Sketch.
smileys.scss
.smiley--happy {
  background: url('smile.png');
}

smile.png

A benefit of this approach is that our Sass now looks clean again, and contained within the component where the image is located.

If we now want to use this image within another component, we can use the smiley--happy class in any html, after we've attached the smileys library.

node
node--blog
node--blog--full
Twig Created with Sketch.
node--blog--full.html.twig
{% extends 'node.html.twig' %}

{% block before %}
  {{ attach_library("compony/node--blog--full") }}
{% endblock %}

{% block content %}
  {{ attach_library("compony/smileys") }}
  <div class="my-messages smiley--happy">
    ...
  </div>
{% endblock %}

This approach would still create a dependency from the node--blog--full component to the smileys component, but at least you can duplicate, rename, move & reuse the code from node--blog--full.

When you refactor the smileys component, you would realise that it's a CSS-only component (as there is no HTML in it's folder), so you would do a search within the themes-directory on the name of the library “compony/smileys”. And you could check where the smileys component is being used.

So this would be an awesome solution, but what if that image is an SVG, and you want to have it inline in your component? Then a class-based approach wouldn’t really work, right? For that use case, we could consider an HTML-only component!

D) HTML-only component

What if smile.png wasn't a png but an SVG? Then there is a good chance that you want to have it inline. We can achieve this by again, abstracting the part that we want to re-use, but this time as a HTML-only component, instead of a CSS-only component.

So let’s see how that would look, if we also name it “smileys”:

smileys
dist
svg Created with Sketch.
smile.svg
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 368 368"><path d="M261.336 226.04c-3.296-2.952-8.36-2.664-11.296.624C233.352 245.312 209.288 256 184 256c-25.28 0-49.352-10.688-66.04-29.336-2.952-3.288-8-3.576-11.296-.624-3.296 2.944-3.568 8-.624 11.296C125.76 259.368 154.176 272 184 272c29.832 0 58.248-12.64 77.96-34.664 2.944-3.296 2.664-8.352-.624-11.296z"/><path d="M184 0C82.544 0 0 82.544 0 184s82.544 184 184 184 184-82.544 184-184S285.456 0 184 0zm0 352c-92.64 0-168-75.36-168-168S91.36 16 184 16s168 75.36 168 168-75.36 168-168 168z"/><path d="M248 128c-22.056 0-40 17.944-40 40 0 4.416 3.584 8 8 8s8-3.584 8-8c0-13.232 10.768-24 24-24s24 10.768 24 24c0 4.416 3.584 8 8 8s8-3.584 8-8c0-22.056-17.944-40-40-40zm-104 40c0 4.416 3.584 8 8 8s8-3.584 8-8c0-22.056-17.944-40-40-40s-40 17.944-40 40c0 4.416 3.584 8 8 8s8-3.584 8-8c0-13.232 10.768-24 24-24s24 10.768 24 24z"/></svg>

Twig Created with Sketch.
smileys--smile.html.twig
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 368 368"><path d="M261.336 226.04c-3.296-2.952-8.36-2.664-11.296.624C233.352 245.312 209.288 256 184 256c-25.28 0-49.352-10.688-66.04-29.336-2.952-3.288-8-3.576-11.296-.624-3.296 2.944-3.568 8-.624 11.296C125.76 259.368 154.176 272 184 272c29.832 0 58.248-12.64 77.96-34.664 2.944-3.296 2.664-8.352-.624-11.296z"/><path d="M184 0C82.544 0 0 82.544 0 184s82.544 184 184 184 184-82.544 184-184S285.456 0 184 0zm0 352c-92.64 0-168-75.36-168-168S91.36 16 184 16s168 75.36 168 168-75.36 168-168 168z"/><path d="M248 128c-22.056 0-40 17.944-40 40 0 4.416 3.584 8 8 8s8-3.584 8-8c0-13.232 10.768-24 24-24s24 10.768 24 24c0 4.416 3.584 8 8 8s8-3.584 8-8c0-22.056-17.944-40-40-40zm-104 40c0 4.416 3.584 8 8 8s8-3.584 8-8c0-22.056-17.944-40-40-40s-40 17.944-40 40c0 4.416 3.584 8 8 8s8-3.584 8-8c0-13.232 10.768-24 24-24s24 10.768 24 24z"/></svg>

svg Created with Sketch.
smile.svg
<?xml version="1.0" encoding="iso-8859-1"?>
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 368 368" style="enable-background:new 0 0 368 368;" xml:space="preserve">
  <path d="M261.336,226.04c-3.296-2.952-8.36-2.664-11.296,0.624C233.352,245.312,209.288,256,184,256 c-25.28,0-49.352-10.688-66.04-29.336c-2.952-3.288-8-3.576-11.296-0.624c-3.296,2.944-3.568,8-0.624,11.296 C125.76,259.368,154.176,272,184,272c29.832,0,58.248-12.64,77.96-34.664C264.904,234.04,264.624,228.984,261.336,226.04z"/>
  <path d="M184,0C82.544,0,0,82.544,0,184s82.544,184,184,184s184-82.544,184-184S285.456,0,184,0z M184,352 c-92.64,0-168-75.36-168-168S91.36,16,184,16s168,75.36,168,168S276.64,352,184,352z"/>
  <path d="M248,128c-22.056,0-40,17.944-40,40c0,4.416,3.584,8,8,8c4.416,0,8-3.584,8-8c0-13.232,10.768-24,24-24s24,10.768,24,24 c0,4.416,3.584,8,8,8c4.416,0,8-3.584,8-8C288,145.944,270.056,128,248,128z"/>
  <path d="M144,168c0,4.416,3.584,8,8,8s8-3.584,8-8c0-22.056-17.944-40-40-40c-22.056,0-40,17.944-40,40c0,4.416,3.584,8,8,8 s8-3.584,8-8c0-13.232,10.768-24,24-24S144,154.768,144,168z"/>		
</svg>

node
node--blog
node--blog--full
Twig Created with Sketch.
node--blog--full.html.twig
{% extends 'node.html.twig' %}

{% block content %}
  <div class="my-messages">
    {% include smileys--smile.html.twig %}
  </div>
{% endblock %}

The abstraction we've made as a HTML-only component, we can fetch to include in to our component. When you refactor the smileys component, you would realise that it's a HTML-only component, so you would look in your codebase for where the exact name of the template is being used either in code (or in theme-hooks).

Drupal is smart enough to find any .html.twig file located anywhere in your Drupal installation, which means we again don’t create any cross component fragility.

Great, but now that we've inlined our SVG, and we render this SVG multiple times on a page, it might be nice to make the browser cache it. Inline HTML won't be cached by the browser, so therefor, it would work best if we link to the image from the HTML using a <img> tag.

smileys
dist
svg Created with Sketch.
smile.svg
Twig Created with Sketch.
smileys--smile.html.twig
<img src="{{ _self|split('/', -1)|join('/') }}/dist/smile.svg" />

svg Created with Sketch.
smile.svg

Note that we could also have linked to that smile.svg by typing out the full path:
 

Twig Created with Sketch.
smileys--smile.html.twig

<img src="/themes/custom/compony/components/smileys/dist/smile.svg" />

But that would make that path very fragile.

Conclusion #

Compony is all about splitting up the theming layer in to different components. The main goal with this approach is that in the future we can easily refactor component by component. 

Using components is a good start to keep each part of your theming readable, however, you can still end up with big frontend blobs if you ignore cross-component fragility.

Hypothetically you could split up every component in to thousands of other components. But please remember this line of thought: “complexity arrives from abstracting too soon”. If abstracting parts of a component into individual components doesn’t have a clear benefit, then simply don’t abstract. Let the component grow in complexity, and refactor it into separate components only when it makes more sense.

Special thanks to thamas for coining the idea of this blogpost. Upvote what upcoming blogpost you want next, or propose a new one!