The history of the architectural error, its consequences, and three rules, thanks to which you can keep the source code in order and reduce the cost of making changes.
In 2014, the company began to redesign the project and as a basis for the layout we put on Bootstrap 3.0.1, which was fresh at the time. We did not use it as a separate third-party library, but closely integrated it with our own code: we edited the variables for our design and compiled the customized Bootstrap from LESS sources on our own. The project was enriched with its own modules, which used bootstrap variables and added their new variables to the settings file.
At that moment, I thought it was the right approach.
After all, the code is immediately compiled with the necessary values without redefining the styles. CSS clean, compact and repetitive. Components stylized using global settings.
After a year or so, the redesign was over, the project went into production, and we took on technical debt. When trying to update Bootstrap to version 3.6.x, it turned out that it would not be easy to merge the new variables.less with our settings file. In Boostrap renamed or removed some of the variables, added new ones. Bootstrap's own components were updated without any problems, but our components fell during compilation due to these changes.
We analyzed the situation and formulated the problem.
Too related code.
The components themselves were in separate files. This created the illusion that the components are independent. But the components used variables declared in the external variables.less file, and without this file did not work. It was impossible to just take and move the component to another project. I had to pull the file with the settings, which eventually turned into a trash.
Too many global variables.
Bootstrap had ≈400 variables. We turned off the unused components of Bootstrap, but left the variables in the config in case you need them again. We also added a hundred or one and a half of our variables. All the names do not remember, it is difficult to quickly find the ones you need. Even with the rules of naming and comments, navigating in the config of 500+ variables is hard.
The names of global variables are out of control.
The same variable could be used in different files and it was long and difficult to track all its occurrences in the code. When changing the value of a variable in one component, there was a risk of breaking other components. The developers of hardcodes created new variables with similar names and values and no longer followed the naming logic.
I came up with three rules that helped overcome our problems:
The variable is used only in the file in which it is declared.
Correctly create all the necessary variables in the file of the component itself and not use variables from other files. Then the components will become independent, they can be connected and moved between projects without breakdowns during compilation. Each component has its own set of variables that are prohibited from being used in other components and called in other files. When variables do not go beyond the limits of a single file, it is easy to see how they are used and what they affect.
All variables inside the component are local.
Since each component has its own variables, let them be local. This will relieve naming problems: components can use variables with the same name and different values - they will not conflict with each other.
Global variables are used only inside the configuration file.
Thanks to the first two rules, we will greatly reduce the number of global variables, but they are still needed. Global variables are declared in the main project file or in a file of type config.less. Rule No. 1 also applies to them - variables are not used outside of their file. This means that you cannot use global variables inside component files. But there is a way without breaking the first rule to throw the value of a global variable into the component. How to do this we will look at examples below.
The third rule is redundant. Observing the first two, you automatically observe the third. But in real life there is a very big temptation to go along the simplest path, not to create a local variable, but to quickly use the global one. The third rule reminds that doing so is harmful, that it creates unnecessary dependencies and leads to code binding.
Let's apply the rules in practice.
Imagine the simplest component to style the page. According to rule # 1, we will create variables inside the component file.
Rule 1
The variable is used only in the file in which it is declared.
/* page.css */ .page { padding: 40px; color: #000; background-color: #fff; }
It was. Sample component code.
// page.less v0.1 @padding: 40px; @txt-color: #000; @bg-color: #fff; .page { padding: @padding; color: @txt-color; background-color: @bg-color; }
It became. Variables are declared in the global scope and they have too common names. This is bad.
In the example above, we declared variables in the global scope and because of this they are subject to name conflicts. If a variable with the same name appears in the neighboring component, the components break each other.
The scope is the “space” between the curly brackets of the selector: {
and }
. Variables declared inside braces work only inside these brackets and inside child brackets, but they cannot be used outside.
If there are no brackets around, then this is the uppermost level - the global scope .
For a variable from a child scope, the priority is higher than for a variable of the same name from the parent scope. Global variables have the lowest priority when the names match.
According to rule №2, we will make the variables local - move them inside the selector.
Rule 2
All variables inside the component are local.
// page.less v0.2 .page { @padding: 40px; @txt-color: #000; @bg-color: #fff; padding: @padding; color: @txt-color; background-color: @bg-color; }
Variables are declared inside the selector and do not create name conflicts, because now they are local.
Now the conflict of names with other components will not be. We solved the first and second problems: we got an isolated snippet without external dependencies. But I do not know a way to override the local variables of the component in this form from the outside. Therefore, in order to customize the components of the global variables of the project, came up with another form of recording.
In LESS you can use mixins as functions. If you create variables inside a mixin and then call a mixin inside a selector, then the variables will be available in the scope of that selector.
Read about mixins as functions in the LESS documentation.
We put the declaration of variables inside the mixin .page-settings()
, and then call it inside the .page
selector:
// page.less v0.3 .page-settings() { @padding: 40px; @txt-color: #000; @bg-color: #fff; } .page { .page-settings(); padding: @padding; color: @txt-color; background-color: @bg-color; }
Mixin delivers variables to the visibility range of the selector.
Variables are localized inside the global mixin. When we called the mixin in the code, the variables became available in the scope of the .page
selector, but still remained local.
Such a mixin does not generate CSS code, its only task is to deliver variables to the desired scope. For example, if you call this mixin in the global scope, then the variables will become global. But this is the same as immediately declaring variables globally.
Instead of several global variables with common names, we created one global mixin. There should not be two components of the same name in the project, which means that the name of the mixin will be unique.
In LESS, there is a “lazy evaluation” of variables: it is not necessary to declare a LESS variable before using it; you can declare it later. At the time of compilation, the LESS parser will find the definition of the variable and render the value of this variable in CSS.
See examples of “Lazy Evaluation” and overriding default variables in the LESS documentation.
If you redefine a variable, then in all places of its use, the value from the most recent definition will take effect, since it is prioritized in the order of the source code. In this sense, variables behave like constants.
So, variables can be declared both before and after use, and mixins are a kind of variable. If you create two mixin with the same name and different content, they combine their insides. And if inside the mixins there are variables with the same name, then a redefinition occurs. The priority is higher for the last mixin.
Consider three files:
// projectname.less @import 'normalize.css'; @import 'typography.less'; @import 'page.less'; // ... @import 'config.less';
Main file We import components and config. Config - the last.
// page.less v0.3 .page-settings() { @padding: 40px; @txt-color: #000; @bg-color: #fff; } .page { .page-settings(); padding: @padding; color: @txt-color; background-color: @bg-color; }
Component. All variables are local and stored in mixin.
// config.less @glob-text-color: white; @glob-bg-color: darkblue; // .page-settings() { @txt-color: @glob-text-color; @bg-color: @glob-bg-color; }
Config project. Override component settings using the mixin settings.
All the most interesting happens in the config. We created global variables and used them in the same file to customize the component.
Mixin .page-settings()
declared twice. The first time inside the page.less
file with default values, the second time in the config.less
file with new values. At the compilation stage, mixins are glued together, new variables override default and CSS is rendered with new values from the configuration file.
Rule 3
Global variables are used only inside the configuration file.
Please note that config.less
is the last one on the list. It is necessary that the mixin declaration in the config has a higher priority than the original declaration in the file of the component itself. The settings will not be applied if you put config.less
before the component, because the mixins are also subject to the rule “the last definition is the strongest”.
In this way, we threw the values of global variables inside the component file, without changing the source code of the component. In this case, all three rules were observed:
config.less
file;It is impossible that the name of a global variable matches the name of a local variable - we get recursion and CSS will not compile. In order not to be mistaken, it is better to record all global variables with a prefix.
// @txt-color: white; .page-settings() { // @txt-color: @txt-color; }
Wrong. Recursive definition of a variable causes a compilation error.
// — @glob-txt-color: white; .page-settings() { // @txt-color: @glob-txt-color; }
Right. Global variables have their glob- prefix, which eliminates the coincidence of names.
SASS is different from LESS and is more like a scripting programming language: there is no “lazy evaluation” and the variable must be declared before it is used in the code. If we define a variable and use it in code, and then redefine it and use it again, then in the first call we will get the original value in CSS, and in the second call we will get a new value. The mixin trick, as in LESS, will not work. But there are other solutions.
Sets of variables for setting the component are conveniently stored in map-objects. This is an array of key: value pairs. The map-get method extracts a specific value from an array, and the map-merge method combines two arrays into one, complementing or rewriting the original array.
Read about Maps in the SASS documentation.
The simplest component without the ability to override it from the outside will look like this:
// page.scss v0.1 $page-settings: ( padding: 40px, bg-color: #fff, text-color: #000, ); .page { padding: map-get($page-settings, padding); background-color: map-get($page-settings, bg-color); color: map-get($page-settings, text-color); }
Settings are stored in the map object and are called in code using map-get
To configure a component from another file, you need to provide the ability to merge the external config with the original settings. Consider three files:
// projectname.scss @import: 'config'; @import: 'normalize'; @import: 'typography'; @import: 'page'; // ...
Main file
First we import the config, then the components.
// config.scss $glob-text-color: white; $glob-bg-color: darkblue; // $page-settings: ( bg-color: $glob-bg-color, text-color: $glob-text-color, );
Settings.
Create global variables and override component settings.
// page.scss v0.2 $page-override: ( ); // [1] @if variable-exists(page-settings) { $page-override: $page-settings; // [2] } $page-settings: map-merge(( padding: 40px, bg-color: #fff, text-color: #000, ), $page-override); // [3] .page { padding: map-get($page-settings, padding); background-color: map-get($page-settings, bg-color); color: map-get($page-settings, text-color); }
Component.
Added a check: do not there already exist settings to override the component?
[1]
- In the component, we first declared a variable with an empty $page-override
array.
[2]
- Then they checked if the $page-settings
variable already exists. And if it was already declared in the config, then assigned its value to the variable $page-override
.
[3]
- And then the initial config and $page-override
into the $page-settings
variable were $page-settings
.
If the $page-settings
array was previously declared in the global config, then $page-override
will overwrite the settings during the merge. Otherwise, the $page-override
variable will have an empty array, and the original values will remain in the settings.
I do not know all the subtleties of SASS, maybe there is a way to implement this technique in a more beautiful way.
As a result, we get all the same advantages, but unlike LESS, we have to redefine all settings in advance, before using them in code.
No matter what you write on - on LESS, SASS, CSS with custom properties or Javascript - there should be as few global variables as possible.
With CSS preprocessors, use three rules to help avoid chaos:
To get global settings inside the component, collect variables in mixin (LESS) or map-object (SASS).
Override the settings in the right place: in LESS - after inclusive, in SASS - before inclusive.
I formulated this methodology in December 2015 for LESS and since then I have been applying it to work and personal projects.
For a year and a half, several public npm-packages appeared. Look at the source code for a better understanding of how this works in real situations.
bettertext.css - typography for sites. Configurable using 11 variables, the remaining 40 are calculated by the formulas. Calculations are performed in a separate mixin so that it is possible to override the formulas. The component has no class, all styles are applied to tags. To create a local scope, I placed all the selectors by tags in a variable — in LESS this is called a “detached ruleset”.
Read about the detached ruleset in the LESS documentation.
links.less - styles for links with focus, animation and pale underline. The component besides the mixin with settings has additional global mixins for coloring links inside your own selectors.
flxgrid.css is a generator of HTML grids on flexes. It is configured using 5 variables, generates classes for an adaptive grid with any breakpoint and any number of columns. In the computation component, the service mixins are hidden inside the local scope. Only mixin with settings is globally visible.
space.less is a tool for managing indents in layout. Designed to be paired with the flxgrid.css grid. Their adaptability is configured in the same way, but space.less uses its own mixin settings and its own local variables - in the code space.less is in no way associated with flxgrid.css.
If I now needed to use the new project Bootstrap 3.xx - the one on LESS - I would turn all imported Bootstrap modules into a variable (in a “detached ruleset”), and all the settings from the variables.less
file in the bootsrtap-settings
. Boostrap global variables would cease to be global and could not be used inside their own code. I would customize the settings of Bootstrap as needed, causing the bootsrtap-settings
in the project config, as well as in the examples above. Then, with the updates of Bootstrap, only the mixin with the customized settings would have to be fixed.
Original source - http://paulradzkov.com/2017/local_variables/
Source: https://habr.com/ru/post/332382/
All Articles