First, clone the project Hasty at libs/frontend-libs/hasty
and create a new branch of the origin/dev
branch. Before doing anything else, make sure you update the version in the package.json
file at the root of the project, to do that you can run:
npm version major|minor|patch|prerelease --no-git-tag-version
(it updates package-lock.json
file too)
npm run generate-version
, it interactively generates new version in package.json
and package-lock.json
files and optionally prepares entry to CHANGELOG.md
This will ensure that new version of styleguide is properly built and no previous version is overwritten. Hasty adheres to the Semantic Versioning specification, with major versions reserved for breaking changes, patch versions for various fixes, and minor versions for everything else.
To start the development server, run docker-compose up
to use a Docker container, or npm ci
and than npm run develop
to develop locally. Then, open http://localhost:8080
. All source files are found in the src
directory. To add a new piece of styleguide, you first have to decide what type it is. There are objects, elements, components, and utility classes. They are all described in the namespaces section. Then, add a new .less
file with the name of the new module into the appropriate folder in the src/css
directory. To show the new module in the styleguide, add a new .njk
file into a folder with the same name as before, but this time in the src/styleguide
directory. This is an example of part of the file that generates the button element:
Do not forget to write a unit test for any new React component. Create a directory `__tests__` inside the component folder and add test file with snapshots.
---
name: Button
desc: "Button can take either the form of a link or a clickable element. The basic variant should be used sparingly in a given view, as it represents a primary action, or a CTA. If more than one button is present at once, all secondary actions should use the simple modifier. In case the primary action is destructive and cannot be taken back (e.g. delete a review), the button must use the negative modifier class."
---
{% variant 'e-button' %}
<button class="e-button" type="button">Dolorum!</button>
{% endvariant %}
{% variant ['e-button', 'e-button--simple'] %}
<button class="e-button e-button--simple" type="button">Dolorum!</button>
{% endvariant %}
Don't forget to update CHANGELOG.md
by new version entry with your changes.
To publish a new version of styleguide, make sure you have the right version in package.json
. After merging your branch to the dev
branch, a new styleguide is automatically deployed to heureka.cz.stage.heu.cz/ui. To deploy a production version, merge the dev
branch into the master
branch. Apart from deploying the styleguide to heureka.cz/ui, this ensures that the latest version of styleguide is published to a private NPM registry npm.heu.cz. The styleguide is then available under the package name of @heureka/hasty
.
generate-version
script with option 4 (prerelease) locally to generate new alpha version in package.json and lock fileversion-branchName-alpha.alphaVersion
- 7.20.1-resolve-alpha-publish-alpha.0
)make build
task after your changesgenerate-version
npm install @heureka/hasty@version
in a project you want to use alpha version of Hasty (ex. @heureka/[email protected])
To release your contribution into production, it has to be reviewed and approved by at least two members of the front-end technical group. Afterwards, it can be merged into the dev
branch, which automatically deploys the new version to heureka.cz.stage.heu.cz/ui, where it can be checked once again. Then, create a merge request from the dev
branch to the master
branch and wait for another approval (one is enough). If approved, you can finally push the update to the production (both to heureka.cz/ui and the private NPM registry) by merging the branch.
There are many well known methods for building HTML and CSS components. We base ours on a set of recommendations called BEM, or Block, Element, Modifier, which rests on a simple, but hard rule. It forbids all selectors other than those targeting classes, like .foo
, and prohibits selector chaining. Both, of course, with specific exceptions. If we comply, we avoid some of the biggest problems with specificity.
The abbreviation BEM hides another principle. A component represents a so-called block, which consists of several elements and may use modifiers. To distinguish them, each of these have a different naming convention. If you create a component (or block) with the name (class) block
, then its inner element must be named something like block__element
. We recognize modifiers because of their block--modifier
class. Here is an example of specific component:
<nav class="breadcrumbs breadcrumbs--small">
<ul class="breadcrumbs__list">
<li class="breadcrumbs__item">
<a class="breadcrumbs__link" href="…">Heureka.cz</a>
</li>
<li class="breadcrumbs__item">
<a class="breadcrumbs__link" href="…">E-shops</a>
</li>
<li class="breadcrumbs__item">
<span class="breadcrumbs__link">Notino</span>
</li>
</ul>
</nav>
Using multiple __
to give an element its depth is a common mistake. Instead of the class breadcrumbs__link
, we would have breadcrumbs__list__item__link
. This is unnecessary — and ugly — and makes it harder to change the structure of a component.
We use the CSS preprocessor Less. Preprocessors give you the basic but important option to put each component in its own file, for example breadcrumbs.less
:
.breadcrumbs {
…
&--small { … }
&--large { … }
}
.breadcrumbs__list {
…
.breadcrumbs--small & { … }
}
.breadcrumbs__item {
…
@media (max-width: @lteLayout) {
…
}
}
.breadcrumbs__link {
…
&:not([href]) { … }
}
The element selectors in the example are not chained with the block and thus remain simple. They are also indented to reflect the HTML structure of the component. This is a helpful practice that — as opposed to the multiple use of __
— is easy to maintain.
In contrast, modifiers are inserted directly into a block using the &
parent selector. Modifiers usually change properties of blocks, but sometimes these changes apply directly to elements. That’s a good excuse to use chaining, like the .breadcrumbs--small & { … }
in the element breadcrumbs__list
. We treat @media
blocks similarly and put them right where they belong.
The last interesting thing to point out is the selector &:not([href]) { … }
. It goes against the principle of BEM. The correct way would be to create a new modifier, for example breadcrumbs__link--current
. It's fine, though. Not everything should conform exactly to the BEM method and common sense sometimes leads you a little astray.
If we want to adjust the appearance or behavior of a component depending on the context or its location in the HTML document, we often stumble. For example, two components named article and category, which represent different sections of the site and define their layout, are two different contexts. How to proceed if the breadcrumbs component should look or behave differently in each section?
We might think of using the selectors .article .breadcrumbs { … }
and .category .breadcrumbs { … }
. That, however, would be an unwise choice. We would quickly find out that it’s not at all clear where to put these — into breadcrumbs.less
or article.less
? Either way is bad, because both create an unnecessary link between the components. That, in turn, would complicate future modifications and lead to trouble with specificity.
Depending on the amount and type of adjustments, we can make a better choice. If the changes are superficial, such as background color or text size, we make a modifier. These are best named in general way, like breadcrumbs--small
or breadcrumbs--large
. If we know these are specific and will not be used elsewhere, it’s fine to use names such as breadcrumbs--article
and breadcrumbs--category
.
When it comes to major changes — at the level of structure or layout — we need to think a little more. The goal is to build components that are manageable and reusable. A component (block) is defined by its structure and inner layout, and therefore it knows if, for example, it’s a flex or grid container, or whether its elements are block or inline. It shouldn’t, however, reason or know about its location in the external layout. If, for example, we declare the breadcrumbs component as position: absolute
, it will unnecessarily limit its reusability. So, it’s better to leave such declarations to components that are designed to provide the overall page layout.
In our case, that’s the article and category components’ job. So, we create two new elements article__breadcrumbs
and category__breadcrumbs
, whose task is to place the breadcrumbs component within the section. Since the appearance and inner structure of the component are still handled by the breadcrumbs class, the result is a composition of classes: <nav class="breadcrumbs category__breadcrumbs">
. Unlike the .category .breadcrumbs { … }
solution, composition doesn’t create a strong link between components and avoids specificity.
The original purpose of BEM was to make developers’ work easier and help them code better. Because of its clear syntax, it enhances code readability and eases orientation. We immediately know what we’re dealing with, just by looking at the code. But can we make it even better?
Using the example of the breadcrumbs and category components, we see that the first one is a reusable component, while the other lays out a particular section. They’re entirely different beasts. If that’s not clear from the names, we’ll make it obvious and add a namespace to the classes. Breadcrumbs becomes c-breadcrumbs
and category takes the name of l-category
. The namespace c
(component) suggests that a class is a component — a closed module with a clear, specific use that is always the same. On the other hand, l
(layout) gives the hint that a class provides page layout.
We use three more namespaces. The first one, named e
(element), is similar to c
but used only on those components that don’t have an inner structure (therefore no BEM elements). A good example is the e-button
element, e.g. <button class="e-button">Send</button>
. These are the simplest components.
The namespace o
(object) is more complicated, but also more valuable in its meaning. It suggests a class that’s in charge of the local structure and layout. The objects o-block-list
and o-inline-list
are good examples. The o
in the title tells us that we’re dealing with a structural element, not a specific component like a bullet list (which would have the c-bullet-list
class). The o-block-list
lays out its elements (called o-block-list__item
) under each other, while o-inline-list
puts them side by side. Also, both have a bunch of modifiers (such as o-block-list--loose
), which are used to modify the spacing between elements. As objects can be used in many places and for different purposes, you can’t predict where they end up, which means objects shouldn’t be edited casually, because any change can have far-reaching consequences. It’s better (and safer) to add new modifiers.
The last namespace is the utility class with the prefix u
. These classes have only one specific purpose — which should be obvious from the name — such as u-align-left
or u-visually-hide
. However, they should be used sparingly. Unless we need the change only in one place or situation, it’s better to create a new component or modifier.