-
Notifications
You must be signed in to change notification settings - Fork 47
Less Language Namespaces
Namespaces encapsulate a group of mixins under common name. They have two purposes:
- avoid naming conflicts in bigger projects,
- encapsulate/isolate a group of mixins from outside world.
First chapter explains how to define and use namespaces, what is legal and what is not legal. Second chapter deal with scoping. Less language has very permissive scoping philosophy and its approach may be surprising for those used to strongly typed or restrictive languages.
Any mixin without parameters or rulesets whose name consists of only classes .id
and ids #name
can be used as a namespace. All mixins nested inside a namespace belong to that namespace. You have two options if you want to access a mixin defined inside a namespace:
- an explicit
namespace > mixin(param1, ..., paramn)
syntax, - a slightly shorter
namespace mixin(param1, ..., paramn)
syntax.
The symbol greater >
is optional.
Declare namespace and use its mixins:
//declare namespace with two mixins
#ns {
.square(@param) {
width: @param;
height: @param;
}
.color(@color) {
color: @color;
}
}
.box {
//use the namespace
#ns > .square(10px);
#ns .color(#ff00ff);
}
Compiles to:
.box {
width: 10px;
height: 10px;
color: #ff00ff;
}
It is legal to nest a namespace inside another one. In addition, mixins inside the namespace are matched as usually, e.g. all matching mixins are going to be used. Following less is equivalent to the previous one:
//declare namespace with one mixin
#outer {
#ns {
.square(@param) {
width: @param;
}
.square(@param) {
height: @param;
}
}
}
.box {
//use the namespace
#outer > #ns > .square(10px);
}
Compiles to:
.box {
width: 10px;
height: 10px;
}
Top level namespace references are legal as long as the referenced mixin contains only nested rulesets and no declarations:
#namespace {
//mixin without declarations
.mixin() {
nested {
color: blue;
}
}
}
//top level namespace reference
#namespace .mixin();
compiles into:
nested {
color: blue;
}
A namespace is valid withing the whole scope where it has been defined and can be used before it has been defined. If multiple namespaces match the reference, all of them are going to be used.
Namespace is defined after being used and split into two parts:
.box {
//use the namespace
#ns > .square(10px);
}
// namespace is split into two parts - part 1
#ns {
.square(@param) {
width: @param;
}
}
// namespace is split into two parts - part 2
#ns {
.square(@param) {
height: @param;
}
}
compiles into:
.box {
width: 10px;
height: 10px;
}
If namespace have a guard, mixins defined by it are used only if guard condition returns true. Namespace guard is evaluated exactly the same way as guard on mixin, so next two mixins work the same way:
#sp_1 when (default()) {
.mixin() { /* */ }
}
#sp_1 {
.mixin() when (default()) { /* */ }
}
The default function is assumed to have the same value for all nested namespaces and mixin. Following mixin is never evaluated, one of its guards is guaranteed to be false
#sp_1 when (default()) {
#sp_2 when (default()) {
.mixin() when not(default()) { /* */ }
}
}
Mixins with with mandatory parameters can not be used as a namespaces. Mixins can be used as namespaces only if all their parameters are optional.
Next less demonstrates parametrized and guarded mixins used as namespaces:
#namespace(@variable) { // mandatory parameter - can not be used as namespace
.mixin() { mandatory: parameter; }
}
@go: true;
#namespace(...) when (@go){ // ok
.mixin() { works: works; }
}
#namespace(@variable: default) when (@go){ // ok - parameter is optional
.mixin() { optional: optional; }
}
#namespace(...) when not(@go){ // false guard - can not be used as namespace
.mixin() { guard: false; }
}
.use {
#namespace > .mixin();
}
compiles into:
.use {
works: works;
optional: optional;
}
The last thing we need to deal with is scoping. You have to understand two things in order to properly use namespaces:
- Which variables and mixins are available inside the namespace.
- If multiple scopes contains namespaces with conflicting names, which one is going to be used.
Each issue is addressed in separate section.
Any mixin declared inside the namespace can use mixins and variables declared in both its declaration and caller scope. The declaration scope takes precedence over the caller scope.
Create a bunch of overriding variables within each scope and see which one wins:
#outer {
#ns {
.square(@priority1, @priority2) {
priority1: @priority1;
priority2: @priority2;
priority3: @priority3;
priority4: @priority4;
priority5: @priority5;
priority6: @priority6;
@priority1: "inside mixin";
}
}
@priority1: "declaration scope";
@priority2: "declaration scope";
@priority3: "declaration scope";
}
.box {
.box {
//call the mixin
#outer > #ns > .square("argument", "argument");
@priority1: "caller scope";
@priority2: "caller scope";
@priority3: "caller scope";
@priority4: "caller scope";
@priority5: "caller scope";
}
@priority1: "caller scope - deeper";
@priority2: "caller scope - deeper";
@priority3: "caller scope - deeper";
@priority4: "caller scope - deeper";
@priority6: "caller scope - deeper";
}
@priority1: "declaration scope - deeper";
@priority2: "declaration scope - deeper";
@priority3: "declaration scope - deeper";
@priority4: "declaration scope - deeper";
Compiles into:
.box {
priority1: "inside mixin";
priority2: "argument";
priority3: "declaration scope";
priority4: "declaration scope - deeper";
priority5: "caller scope";
priority6: "caller scope - deeper";
}
Note: Less.js has one known bug in handling of lazy-loaded namespaces https://github.com/cloudhead/less.js/issues/996 .
Namespaces references are relative to current scope. Less looks for matching namespace within current scope first. If a mixin is found, the search is stopped. The search continues into parent scope only if the last searched scope contains no matching element.
Example:
#ns {
.mixin() {
declaration: "global namespace";
}
}
.box {
#ns {
.mixin() {
declaration: "local namespace";
}
}
//use the namespace
#ns > .mixin();
}
Result:
.box {
declaration: "local namespace";
}
You can "import" nested mixins into namespace by calling their owner mixin. Since nested mixins act as return values, all nested mixins are copied into namespace and available from there:
.unlock(@value) { // outer mixin
.doSomething() { // nested mixin
declaration: @value;
}
}
#namespace() {
.unlock(5); // unlock doSomething mixin
}
#use-namespace {
#namespace > .doSomething(); // it works also with namespaces
}
compiles into:
#use-namespace {
declaration: 5;
}
Less4j cuts the evaluation if it detects dependency cycle. Dependency cycle happens when two namespaces reference each other. For example, #space2
references mixins from #abc2
which references mixins from #space2
:
#space2 {
.test2() {
.nested() {
available: not;
}
}
// use and import mixins from #abc2
#abc2 > .dependency();
}
#abc2 {
.dependency() {
voila: voila;
.nested();
}
// use and import mixins from #space2
#space2 > .test2();
}
The #abc2 > .dependency()
in previous example will not see the .nested
mixin, cause the #space2 > .test2()
call leads to cycle. Evaluation will end with error:
Errors produced by compilation of testCase
ERROR 12:6 Could not find mixin named ".nested".
Cycle detection ignores variable states, evaluation is cut whenever namespace compilation requires itself to be compiled. It stops even if the cycling would not be infinite due to guards around mixins.
Less.js requires namespaces to be defined before being used. The above situation represents error too, but on line 7:
SyntaxError: .nested is undefined in test.less on line 7, column 3:
6 }
7 #abc2 > .dependency();
8 }