Less.js: Allow guards to work with undefined variables

Created on 4 Jul 2013  ·  46Comments  ·  Source: less/less.js

.guard () when (@variable) {
  a {
    color: red;
  }
}

Variable isn't defined, therefore no output.

@variable: true;

.guard () when (@variable) {
  a {
    color: red;
  }
}

Variable is definied, therefore output:

a {
  color: red;
}
feature request low priority support as plugin

Most helpful comment

Has there been any movement on this request? I've been anxious for an undefined variable check for some time now. At the moment, I have a work around that is far from ideal...

p {
  & when not (@body-text-color = null) {
    color: @body-text-color;
  }
}

but, for this to work, the variable must exist and be defined as null by default. This would be far more effective/flexible if I could just instead check if the variable even exists in the first place.

All 46 comments

+1
This could also be used to set base colors on libraries.
Imagine the following case:
You have a less file for a specific app, e.g. a website's main menu.
You import this less file in the main less file, where your base colors are defined as variables.
Now if you use those base colors in the main menu's less file, they depend on the main file.
If you could check the variables for existence, you could fall back to module-defined colors.
Here's an example of how i imagine the menu less file:

@menu-base-color: white;
@menu-base-color: @base-color when (@base-color);
...or...
@menu-base-color: @base-color when isset(@base-color);

+1, we already have istext, isnumber having isdefined and isundefined will help greatly in theming with less.

I.e. use value or userValue if isdefined

I'm running into this issue a lot when trying to set up theming conventions for Semantic UI with LESS.

+1 on this - would be really great.

+1 on this for me too

Not a big deal to add is-defined function (I expect it to appear in one of the first plugins as soon as v2 is released). Though to be honest the sad thing is that those who want this feature really miss the fact that both use-cases above are solvable (in many cases even with more compact code) w/o any feature like that.

@InitArt's use-case:

// library.less
@variable: false;

a when (@variable) {
    color: red;
}

// ......................................
// user.less
@import "library.less"
@variable: true;

@nemesis13's use-case:

// library.less
@base-color: green;
@menu-base-color: @base-color;

.menu {
    background-color: @menu-base-color;
}

// ......................................
// user.less
@import "library.less"
@base-color: white;
// or:
@menu-base-color: black;
// or whatever...

I.e. in summary: instead of testing if a variable is defined just provide a default value for that variable since variable values are freely overridable.

Can anyone answer why max's example is not do-able for them? I can only
assume it's because you don't know if the variable is defined above or
below the import ? Or is it something else?
We are not going to consider something that does not have use-cases .
The downside of doing this is people mistyping a variable won't see an
error. We could work around this with a warning but then if you did it on
purpose you wouldn't want a warning.. that's my only problem with this
suggestion.

This is useful especially when defining themes in order to reduce the size of the resulting CSS. When creating themes in general one would like to enable setting a lot of properties. However, users may want to set only some of them leaving the rest of the properties unset. Of course setting them to default values would work in general, however, this bloats the final CSS.

Consider the following base theme (template):

.my-class {
    color: @text-color;
    background-color: @background-color;
    border-color: @border-color;
}

The user of the theme may want to set just the text color leaving the rest of the properties unset. Of course it is possible to set the values to "initial", "transparent", "inherit", etc., however, this increases the size of the final CSS. Themes tend to have hundreds of such properties so this the size may increase considerably.

@JechoJekov

I think you're missing the point of this feature request, notice that for your use case you still will need to put guard for every of these properties (hence the rationale I suggested two posts above still applies). So I don't think your use-case adds anything new to #1400 (basically it looks like you're suggesting a different feature like "skip property if all of its values are set with undefined variables" or something like that but it's another big story).

@seven-phases-max

Perhaps it would be best if it is possible to set a variable to an "undefined" or "unset" value. Setting a property to such a value would mean that the property is not rendered in the final CSS. This simplifies the syntax and makes it possible to validate that the variable is declared.

@JechoJekov

Perhaps it would be best if it is possible to set a variable to an "undefined" or "unset" value.

As I mentioned this is another feature that needs its own ticket (with its own discussion).

Just some clarification for roadmap: This is marked "ReadyForImplementation", but there doesn't seem to be consensus in this thread, and it looks like @seven-phases-max suggested a decent alternative. @lukeapage @seven-phases-max, should ready for implementation be removed?

@matthew-dean No idea. I suppose here "ReadyForImplementation" is still sort of valid, meaning something like "this feature seems to be not really neseccary but if someone comes up with a PR implementing such thing there will be no resistance" or a sort of :)

How's that? (Under Consideration)

To me, "Ready For Implementation" means that there's general (but not necessarily unanimous) consensus that it _should_ be addressed or implemented. I think "Under Consideration" meets your definition.

Still running into this issue when using scoping

@foo: 'bar';
& { 
  // not sure if @foo is defined
}

Would love ability to use isDefined

While something like is-defined makes sense (as a syntactic sugar), it is not really required. Just _always_ define @foo (with whatever default value you need), after all if you're using a variable within your styles - you _can_ predict you need to define it, e.g.:

// in a far far away galaxy of your library default variables:
@foo: undefined;

// the code where you need it
& when not(@foo = undefined) { 
    // @foo is defined by user    
}

// some user code
@foo: bar;

Though I guess I already wrote this in examples above (I'm just trying to make sure to voice the fact that the lack of this feature can't be a show-stopper for anything. If you know any use-case where it is the one, please share).

I'm trying to set up theme previews in the browser.

This means I have to use less.js and overwrite the default @theme variable with run-time settings.

Ala

window.less.modifyVars({
  theme: 'someOtherValue'
})

In my LESS i'm importing a global theme.less file that includes

@theme: undefined;

.setTheme {
  @theme : 'default';
}
.setTheme;

I've also tried just

@theme: 'default';

In the first case, the outputted value is undefined in second case it is always default regardless of what is set in modifyVars

Click theme dropdown here for an example.

The current work around (which is on live site) is to manually grab the variable import path, and parse it using regexp then import all those variables with modifyVars, as you can guess this is pretty messy

@seven-phases-max I've narrowed it down to a very specific test case with @import causing issues.

In both cases assume:

Javascript

window.less.modifyVars({
  theme: 'changedValue'
});

In first case in theme.less, outer is correctly set to changedValue

component.less

@import 'theme.less';

theme.less

@theme: 'default';
.outer {
  value: @theme; // value is set to changedValue
}

In this failed case, in theme.less @theme is set to default and not changedValue

component.less

@import 'theme.less';

theme.less

@theme: 'default';
@import "@{theme}"; // theme is set to default not changedValue

In case of "var in mixin", the snippet turns into:

// defaults:
.setTheme {
   @theme: undefined;
}

// custom:
.setTheme {
  @theme: 'default';
}
.setTheme;

Though for modifyVars it should always result 'someOtherValue' for either snippet you tried (because modifyVars works simply by appending plain @theme: 'someOtherValue'; line right at the end of the compiled source code). So if modifyVars is not working the problem is most likely elsewhere.
I did look at the button page code but it's too complex to quickly say what could be possibly wrong. At quick glance I can't see (or maybe just can't understand) how the Less code is recompiled and resulting CSS is reapplied after modifyVars (in simple snippets it's done via less.refreshStyles following modifyVars).


Btw., just in case, are you sure you're not planning to use is-default as something like:

.setTheme when not(is-defined(@theme)) {
  @theme: 'default';
}
.setTheme;

? If so, the problem is that such code will directly violate lazy-evaluation principle (is-defined should return false if the variable not defined, but since it _will_ be defined in that scope immediately, is-defined also has to return true -> unsolvable recursion. (For more details on why conditional imperative-like redefinitions can't safely be allowed in Less see #2072).
I.e. it could actually work as expected (unless is-defined is coded to explicitly detect and bail-out of such recursion) but only as kind of unspecified/undefined behaviour, w/o consistency with normal variable visibility rules thus creating even more mess.

@jlukic

@theme: 'default';
@import "@{theme}"; // theme is set to default not changedValue

Yes, this was one of my suspicions too (Unfortunately I can't test this right now, neither I can guess how this could happen, but yes, it's possible because variable use in imports is real black magic. I'll create a dedicated issue report if this is the problem).

I'm fairly certain it has something to do with that wonderful black magic.

I've been working with very simple less files to debug, however I haven't had a chance to create a test-case repo yet.

In my tests, the failure is specific to using {@variable} values in import, but not in css properties.

Really enjoying theming with less, would love to get past these issues. Here's a fun theming example with LESS i just showed off at Meteor DevShop last week (click the paintbucket icon in top right)

@jlukic I tested it and modifyVars works for vars in imports as expected both with lessc and with less.js (see codepen demo, it's a bit tricky code there since it had to import from the gist scary url, but I suppose it's equal to what your pages are supposed to perform... So it actually seems like the problem is elsewhere).

Thanks for taking the time to produce the test case for me. This is pretty clear cut. I'm not sure what's going on for me.. I'll have to investigate

Has there been any movement on this request? I've been anxious for an undefined variable check for some time now. At the moment, I have a work around that is far from ideal...

p {
  & when not (@body-text-color = null) {
    color: @body-text-color;
  }
}

but, for this to work, the variable must exist and be defined as null by default. This would be far more effective/flexible if I could just instead check if the variable even exists in the first place.

+1

So in summary: implementation of an is-defined(name) (and/or get-value-of(var-name, fallback-value-if-not-defined)) function within a plugin needs less than 15 lines of code. So just do that if you're brave enough.

+1 to this. Is there any progress on this as a plugin? I mean, @seven-phases-max you know better the code than most contributors, I guess. If it requires around 15 lines of code, why hasn't it been done yet?

If it requires around 15 lines of code, why hasn't it been done yet?

Because nobody is really interested? Every feature is cool and "extremely useful" when somebody else does this for you... But it magically gets not so important when it's about your own time ;)

you know better the code than most contributors

This does not mean I have to put anything of my own interest after anything of someone else interest, sorry (Especially if that's something encouraging what I personally treat as misuse/bad-code-style).

To not sound empty, for @ashenden's use-case: here's the proper way to make any of your ruleset to be customizable by "unknowns" (obviously assuming a case where normal CSS overriding is not applicable for some reason) without the flawed idea of "blind-move-every-CSS-value-to-a-variable-plus-use-something-that-you-dont-use" (I won't get into details why it's actually broken as it's a story for a long blog-post).

There's concept usually named "hooks", so the following "can't make my mind up festival":

// .............................................
// base code:

p {
    & when not (is-defined(@body-text-color)) {
        color: @body-text-color;
    }
    & when not (is-defined(@body-text-color)) {
        background-color: @body-back-color;
    }
}

span {
    & when not (is-defined(@body-text-color)) {
        color: @body-text-color;
    }
    & when not (is-defined(@body-text-color)) {
        background-color: @body-back-color;
    }
}

// .............................................
// customization:

@body-back-color: blue; // how about gradient?

should be replaced with:

// .............................................
// base code:

p {
    .body-text();
    .body-back();
    // ^ it's actually better to group all this into a singe entity, e.g. .p()
    // so that as you don't know WHAT or HOW to be customized
    // you don't pre-enforce any limitations and/or hardcoded approach
    // for something YOU do not even write
}

span {
    .body-text();
    .body-back();
}

.body-text() {}
.body-back() {}

// .............................................
// customization:

.body-back() {background-color: blue}

Count the lines, count the possibilities, count the limitations.
I.e. really, if you don't use some CSS properties please just leave them alone for those who will.

Guys, here's where I ran into this, please take a look at this example and tell me if it's a valid case and cannot be fixed without is-undefined.

I want to load themes in a library like module like that:

In my core app I define variable @theme: 'yellow' and then import library less.

And here's what library less could look like in this case:

@import 'themes/@{theme}';

body {
background: @primaryColor;
}

This way I could have yellow.less and default.less with @primaryColor: yellow and @primaryColor: red.

In the end, I can write my library using semantic variable names like primaryColor and provide a set of themes with those variables defined. And library user would just define theme name in his app and import library styles.

Ok, nevermind, looks like I got it, I just need a importing mixin like this:

@theme: 'default';

.imports(@theme) when (@theme = 'yellow') {
    @import "themes/yellow";
}
.imports(@theme) when (@theme = 'default') {
    @import "themes/default";
}
.imports(@theme);

@waterplea You can simplify your code to:

@theme: default;

.imports(yellow) {@import "themes/yellow";}
.imports(default) {@import "themes/default";}
.imports(@theme);

also note removed redudant quotes.

Though to be honest I can't see how @theme: yellow; (+ all the mixins code) is better then just the explicit @import "themes/yellow"; line. I.e. first of all are you aware of the overriding? I.e. you don't need to hide default @import "themes/default";, if your library user needs to apply yellow stuff - she just imports your yellow theme (anywhere after your main library file, and yet again it's the same one line) and voilà - your library uses variable values specified in the yellow file.

I ended up with an optional import of a file that overrides theme or switches to a preset theme. In a project that uses the library we then configure webpack to use alias to that file as a node module if the project needs a different theme. What I wrote before or what you wrote wouldn't work in our case as we are building a UI kit for Angular and it is highly modular. We can import just one button or select control and it has to be aware of the theme we are using with all of its styles encapsulation. The solution is easy to set up so I'm happy with it.

what you wrote wouldn't work in our case as we are building a UI kit for Angular and it is highly modular.

What I wrote would work for any level of modularity. I've seen this infinite times before - it's one of the most sad Less (mis)understanding problem - people just miss the fundamental Less lazy-evaluation principle and, instead of using the natural Less overriding, construct libraries with heavy file-injecting-based customization patterns inspired by habits from languages with directly opposite variable semantics (like PHP, Sass etc.). But never mind, it's normal for a developer to be conservative and keep sticking to a once-tried-and-succeeded design-pattern even if that design-pattern is totally ineffective in a new environment/platform (me is not an exception). It must be something more than just some guy continuously screaming "Guys, you do it wrong!" for this to change :)

I would love to get it right, I'm not claiming I understand everything :) Care to explain your suggestion on my example? Here's what we got:

Here's a minimum structural example:

Library:

  • import.less
    @import 'theme-default';
    @import 'mixins';
  • mixins.less
    some mixins here like resetting default browser button styles
  • theme-default.less
    @primary: red;
    @secondary: blue;
  • theme-secondary.less
    @primary: green;
    @secondary: yellow;
  • buttonComponent
    @import 'import';
    .button { color: @primary; }

Project 1 (must use default theme):

  • component
    @import 'import';
    body { background: @secondary; }

Project 2 (must use secondary theme):

  • component
    @import 'import';
    body { background: @secondary; }

Our library is added as a node module and within projects we import just the button component from the library. Since it's an Angular module for that button with it's own less file it is compilated separately from the rest of the Project 1 or Project 2.

I've been thinking about this for a while and I couldn't come up with a way to organize less files to allow Project 1 or Project 2 to set what theme file to use when compiling both components and Projects own code. I don't see a way to override styles for button component from within Project's code.

So instead, basically, here's how I wrote import.less:
@import 'theme-default';
@import 'mixins';
@import (optional) '~custom-theme';

And within Projects, if I want to override theme variables for both Projects code and library components I alias this file as module using webpack. This file could have explicit overriding of say @primary or just a switch to secondary theme:
@import '~myLibrary/styles/theme-secondary';

So I know about overriding, problem is — user of the library has no way of getting in between "anywhere after your main library file" and the library component code. I get it that what you say would work for frameworks without styles encapsulation, but for Angular it would either not work or I misunderstood something in your solution.

I see. Then we're actually speaking of the same thing (just in different words). As as soon as buttonComponent (or any other component) is compiled separately, "the project customization master" remains to be the import.less file and you did exactly what I suggested. (while Project 1 and Project 2 turn to be just another "anything-else" (or "all-in-one"?) components of the same level as buttonComponent and not quite a "projects").

I.e. this way I'd say you "did it right" to my taste.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale Ping.

Technically a plugin implementing the feature is available since 2015. But personally I never tested it so please don't blame me if something goes wrong (it's just FYI).

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

So... I skimmed back through this thread and I can't really determine if this is mostly cases of people mis-understanding Less scope or not.

I don't really see the need to check if vars are defined. Vars should be defined if they're going to be consumed.

@import "library";  // contains @library-color: blue;

@library-color: red;

.box {
  color: @library-color;
}

I don't understand the need to conditionally set a property based on the value _changing_. If the property is set to a variable, just change the variable's value.

That is, if library.less has this:

@library-color: blue;
.box {
  color: @library-color;
}

All someone would need to do to set its output:

@import "library";
@library-color: red;

This would output:

.box {
  color: red;
}

Does the OP and others in that thread understand that behavior?

I think the use case of "does var exist" is kind of a nice way to do flags. That is, I don't think there's a conceptually straight-forward "falsey". Or rather, it's unusual that true is the only "truthy" value. In other words, I think it's a matter of education, rather than a behavior issue.

I think that we should consider closing this issue, since this is usually fixed with one line declaring the default variable value. Although with Less v3.5 moving toward a more permissive deal, maybe variables inside guards are evaluated more permissively?

My vote is no, though. @the-variable: false; seems to be all OP needs to add.


To any who have questions about how to solve a specific problem they feel is related to this, feel free to post on the less gitter and @ me.

Okay closing, thanks @calvinjuarez.

What's also happened since this was open is the if() function. So you can do color: if((@variable), green, red);

@matthew-dean Thank you so much for writing in with this tip. I saw the addition of if() in the 3.0 release notes, but didn't make the connection to this issue, which I had a unique case for (but not the time for making a reduced version of). Again, very much appreciated. Well done, LESS team.

@kbav No problem! Another tip on top of that tip: when if() was first introduced, it required parens around conditions because it was basically "embedding" a when guard (as in, the part after when in when (@variable)). However, that's been fixed as of Less 3.6, so the above example you can write without parentheses (if compiling with the latest version):

color: if(@variable, green, red);
Was this page helpful?
0 / 5 - 0 ratings