Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interpolation of variable, mixin and function names (a.k.a. "Dynamic" variables/mixins/functions) #2702

Closed
WalkerCodeRanger opened this issue Sep 23, 2015 · 31 comments

Comments

@WalkerCodeRanger
Copy link

Using less 2.5.1 on http://less2css.org I can use variable variable Names to access a variable.

@var-name: "foo";
@foo: 45px;
.selector
{
    width: @@var-name;
}

Outputs:

.selector {
  width: 45px;
}

However when I try to declare the varible the same way, it gives an error:

@var-name: "foo";
@@var-name: 45px;

.selector
{
    width: @@var-name;
}

Gives:

ParseError: Unrecognised input on line 2, column 1:

1 @var-name: "foo";
2 @@var-name: 45px;
3 

The real world use case for this is I would like to have a grid framework that supports a variable number of breakpoints the user can change the names of. So I need to take a list of screen size names and declare a bunch of variables based on those. i.e.

@screen-size-names: xs, s, m, l, xl;
@screen-breakpoints: 544px, 7568px, 991px, 1200px;

And then use mixins to declare variables like @screen-s-min, @screen-m-max etc.

@rjgotten
Copy link
Contributor

So I need to take a list of screen size names and declare a bunch of variables based on those.

No. You need to declare keyed dictionaries; which you can build with lists...

@WalkerCodeRanger
Copy link
Author

@rjgotten I don't know what you mean when you say "keyed dictionaries; which you can build with lists". Can you provide a reference to an example or an example less?

@seven-phases-max
Copy link
Member

Can you provide a reference to an example or an example less?

This depends on the code you need to use such "variables" in. In general yes, the need for such variable-variable-names hints that there's something wrong with the approach (just think of it: you define something with unknown name to represent a value you would refer to later by the same unknown name... It does not make any sense really). And yes, lists/arrays and more "structured" (whatever this could mean in general) programming are much more clean and straight-forward (compared to both hypothetical variable-name-variable-definition and variable-name-variable-access (which is just yet another closely related anti-pattern of "moving every thing into a dedicated global variable")).

A simplest example (just example, guessing you would use those variables for some grid generation) would be:

@devices:
    s   544px,
    m   768px,
    l   991px,
    xl 1200px,
    z  2400px;

.make-grid-colomns();
.make-grid-colomns(@i: length(@devices)) when (@i > 0) {
    .make-grid-colomns(@i - 1);
    @device: extract(@devices, @i);
    @name:   extract(@device, 1);
    @size:   extract(@device, 2);
    @media (min-width: @size) {
        column-@{name}-bla-bla {
            blee-blu: blo-bla;  
        }
    }
}

But there're too many variations/patterns and zillion of other tips and tricks to use in particular situations (for a concrete use-case it's better to ask at SO or so).

@WalkerCodeRanger
Copy link
Author

@seven-phases-max Thanks, I now understand what he meant by "keyed dictionaries" I didn't realize you could do that, though I guess it makes sense given that lists can be separated with commas or spaces.

I still think I need the functionality I filed this ticket for. Normally, I would agree that defining "something with unknown name to represent a value you would refer to later by the same unknown name... It does not make any sense really." If this were simply my own style sheet I would agree with you. However, I am trying to write a library something like bootstrap. The ability to define variable variables means that users of my library could change the number and names of the breakpoints and then have access to variables with those names. That will be much more straightforward for a user of my library than understanding how to pull things out of lists etc. So a hypothetical user of my library might do something like:

@devices:
    tiny   340px,
    small  768px,
    big   1200px;

@media @screen-tiny-only
{
    .some-class
    {
        color: #123;
    }
}

From their perspective, there are no unknown names. They know that the names should be tiny, small, and big. They directly use a variable with a reasonable name to them.

@seven-phases-max
Copy link
Member

@devices:
    tiny   340px,
    small  768px,
    big   1200px;

@media @screen-tiny-only ...

The main problem of this code is that if you add another screen size ("very-big") to the arrays above it will have no effect until you also explicitly write @media @screen-very-big-only for every case you need it, so it spoils the very idea of "dynamic" names (i.e. if you have to know it must be @screen-very-big-only then it's no point in having those values as a list(s)).

(And for a more complex use-case, one can always get use of something like @screen-very-big-only: ... at(@devices, very-big) ... or so).

@WalkerCodeRanger
Copy link
Author

That isn't a problem because I am not expecting people to add device sizes to an existing project. Rather I am assuming they will be defined project to project near the beginning of that project.

@seven-phases-max
Copy link
Member

That isn't a problem because I am not expecting people to add device sizes to an existing project.

Then there's no need for lists (that's the main point - if you use dedicated vars you don't need any lists, if you use a list of these values you don't need vars).

In other words, if you expect @screen-tiny-only to explicitly appear in either media query, there's no way a dynamic var definition (hidden and cluttered in mixins and/or some scary interpolations) can be better than the self-documented explicit @screen-tiny-only: value; definition.

@WalkerCodeRanger
Copy link
Author

I don't think you are understanding what I am doing. Let's imagine that Alice and Bob both download my library.

My Library

@devices:
    s   544px,
    m   768px,
    l   991px,
    xl 1200px;

// Mixins that dynamically declare @screen-s-min, @screen-m-max for each device.

Alice's Project

@import "library.less"

@media @screen-xl-only
{
    // ...
}

Bob's Project

@import "library.less"

@devices:
    tiny   340px,
    small  768px,
    big   1200px;

@media @screen-tiny-only
{
    // ...
}

Bob is able to change the device list for his project, then use natural variables. So I do need to declare variable variables. My library needs to be dynamic and then various users of it need to use explicit variables like @screen-xl-only and @screen-tiny-only.

@seven-phases-max
Copy link
Member

I don't think you are understanding what I am doing.

Well, no, actually I do understand it perfectly. As I mentioned above if you want a fully customizable set of media queries forget about dedicated global vars. Bob's project may look like:

@import "library.less";

@devices:
    tiny   340px,
    small  768px,
    big   1200px;

.media(tiny, {
    // ...
});

(just one of possible syntaxes).

What I can't actually understand in your example is that how the code in your library would know what identifiers Bob used for his devices? Does the library have any media dependent CSS at all? Or this conversion from a list to global vars is the only purpose of the library? And if it's not, could you also elaborate what else (media related) code/stuff the library has?

@matthew-dean
Copy link
Member

I have to concur with @seven-phases-max and @rjgotten. "Defining" variable variables doesn't make sense, because it is, by nature, a referential syntax. And they're suggesting other patterns to achieve the same thing, and I could think of a few more to solve the same problem.

For example, if Bob or Alice need to override the value of a global var, it would be as simple as:

@screen-xl-only: 849px;

If, however, Bob wanted to change the NAME of that var, it would be as simple as:

@screen-hugetastic-only: @screen-xl-only;

...and then he could use @screen-hugetastic-only to his heart's content. So I don't think you need lists or extract loops at all. I don't really use lists, because I find Less loops a kind of anti-pattern, but that's more my personal preference.

@WalkerCodeRanger
Copy link
Author

First, setting aside my use case for a moment, I think it is just a matter of consistency. If you document that @@something is a variable variable, then I would expect to be able to use it anyplace I would put a variable name. I can do that, except I can't put it on the left hand side of a declaration. That feels inconsistent to me.

Second, since there are some questions about the library I am thinking of creating I will try to provide some more detail. The plan is that it would be a mixin only library. Initially, it would support responsive grids and media queries. Over time I would hope it would grow to incorporate lots of other functionality like Bootstrap except only as mixins so users weren't forced into one way of organizing styles and constantly surprised by the millions of styles the framework applies. I'm sure some of these additional mixins would be affected by the media queries. I think it would be great to add features along the lines of Compass as well though I am not that familiar with it. I like less more than Sass and think that the lack of a really great mixin library is part of what is pushing people to Sass. When creating a library, I think end user syntax and clarity is very important to adoption. It makes sense for the library authors to jump through hoops and experience pain to create a good experience for users. I am open to alternate syntaxes, but so far haven't seen one I think is as clear as what could be done if I could declare variable variables. I readily admit I am not a Less guru, so there may be a good one I am not aware of yet. With all that said, something more concrete will probably help to clarify.

In the discussion below, I will describe the API of the library without going into all the mixins it would take to create that.

My library should support not just re-definable breakpoints, but a variable number of breakpoints since different sites work well with a different number of breakpoints. When changing the number of breakpoints, it makes sense to rename them.

@screen-size-names: xs, s, m, l, xl;
@screen-breakpoints: 544px, 768px, 991px, 1200px;

I am using two different lists because there is one more screen size name than there are breakpoints (breakpoints divide the screen sizes into ranges).

Based on the screen size names (possibly overridden by a customer), there would be mixins allowing the creation of what bootstrap calls a container. This defines in much greater detail the behaviour of the page width. An example usage might be:

@page-default-margin: 16px;

.my-page
{
    .page();
    .page-xs-fluid(@page-margin: 8px);
    .page-s-fluid();
    .page-m-fixed();
    .page-l-fixed();
    .page-xl-fixed();
}

This allows the user to control for each screen size whether the the page is fluid or fixed size and what the margins and padding should be. In general, there are two mixins per page size @size. They are .page-@{size}-fluid(@page-padding: @page-default-padding; @page-margin: @page-default-margin) and .page-@{size}-fixed(@page-padding: @page-default-padding; @page-margin: @page-default-margin).

When laying out elements on the page, there are a set of mixins that work similar to bootstrap grids (except they use inline-block instead of float). Rows are created using:

.grid-row(@gutter: @grid-default-gutter) { /* ... */
.grid-row-align(@align) { /* ... */ }
.grid-row-vertical-align(@align) { /* ... */ }

Then, for each screen size @size there are mixins:

.grid-cell-@{size}(@width; @gutter: @grid-default-gutter; @columns: @grid-default-columns) { /* ... */ }
.grid-cell-@{size}-offset(@width: @gutter: @grid-default-gutter; @columns: @grid-default-columns) { /* ... */ }
.grid-cell-@{size}-push(@width; @columns: @grid-default-columns) { /* ... */ }
.grid-cell-@{size}-pull(@width; @columns: @grid-default-columns) { /* ... */ }

This set of mixins allows users to have different numbers of columns and different gutter width for different elements.

Up to this point everything I have described can be done with less today.

Given that the user has used many mixins each named after the screen sizes that they have potentially customized, when they need to do some media query for a style they are applying, it seems natural to them that they should be able to use variables with the same names. i.e.

@media @screen-tiny-only
{
    .some-class
    {
        color: #123;
    }
}

There would be a set of such variables for each screen size @size. There would be @screen-@{size}-only, @screen-@{size} (that size and up), and @screen-@{size}-down. (I know that was not valid syntax but I thought it would be a clear way to express what I meant). There would be additional media query variables like @screen-high-res and other useful variables like @screen-@{size}-min and screen-@{size}-max (the min and max size for given screen size).

This last functionality is what I would use declaring variable variables for. In this context, I think it makes a lot of sense and provides the most intuitive syntax to the user.

If something I have said is unclear, I'm happy to answer questions. However, at this point I think I have pretty much stated my case for this feature. If you don't agree that is fine. I leave it up to whatever process the less community has for deciding what features make it into the language. However, to keep less relevant I would strongly encourage that features like this and other powerful features needed to create advanced mixin libraries be added. I don't expect typical users to use features like this. Just like in C# and Java how many developers don't ever create a generic class, but they are integral to libraries every developer uses.

@seven-phases-max
Copy link
Member

seven-phases-max commented Oct 1, 2015

.my-page
{
    .page();
    .page-xs-fluid(@page-margin: 8px);
    .page-s-fluid();
    .page-m-fixed();
    .page-l-fixed();
    .page-xl-fixed();
}

In Less I suggest this should be:

.my-page
{
    .page();
    .page(xs, fluid, @page-margin: 8px);
    .page(s, fluid);
    .page(m, fixed);
}

and so on (notice if Bob defines very-large device your library won't have .page-very-large mixins), and you don't need any global variables for this. So for me it yet again seems like it's just the Bootstrap way of implementing grid stuff and mixins is what misdirecting you (BS grid implementation way is a stone age approach forcing predefined set of devices via predefined sets of variables, mixins and so on).

So no, you did not convince me.

@matthew-dean
Copy link
Member

I was also going to suggest keyword guards on mixins as @seven-phases-max. And I would also echo that Bootstrap's way of doing things wasn't what I would call "best practices" for Less code. (I suspect much of the stuff they had to simplify in order to have cross-platform support with Sass.)

And, having tinkered with building a Less library (which maybe someday I'll finally finish), I can tell you that all of your user goals are possible with Less's existing feature set, just not perhaps in the exact method you're aiming at.

And, with Less's (unfortunately undocumented) inline, scoped plugins, Framework authors have unparalleled power to build features that don't cause conflicts with other frameworks nor end-user styles, and don't require any extra steps on the part of the user to "install" or "build", unlike extensions for Sass and PostCSS. (See: #2479) So there's a lot of power possible there.

@seven-phases-max
Copy link
Member

Yes, sure this does not mean that we don't need other features that could make "structured" code better (in particular #1848, #2433 and so on). But this "interpolate definitions" stuff you're looking for is a wrong move because it's nothing but an attempt to workaround the fundamental limitation of predefined set of entities (vars and mixins) by introducing yet another level of indirection thus essentially just stacking kludges on top of each other, while the problem should be solved directly via proper language entities with natural arbitrary data support (structures, arrays, mixins args and so on...) to stress the weirdness: .grid-cell-@{size}-offset-like mixins are as strange as round_{value}() function would be, you will wonder if you see that instead of normal round(value), will you? ;)

@seven-phases-max
Copy link
Member

seven-phases-max commented Oct 1, 2015

To not sound unfounded here's concrete example. Bootstrap .make-*-column-offset mixins are defined like this:

.make-sm-column-offset(@columns) {
    @media (min-width: @screen-sm-min) {
        margin-left: percentage((@columns / @grid-columns));
    }
}

and same code repeated for every device in their hardcoded set.

In a modern library this can be implemented as (here I'm using lists and less-plugin-lists since currently it's the easiest and the shortest way (for me at least), but there're other methods to do the same):

@devices:
    sm  768px,
    md  992px,
    lg 1200px;

// not showing media-free branch for xs (or whatever first device it may be) to keep code evident
.make-column-offset(@device, @columns) {
    @media (min-width: at(@devices, @device)) {
        margin-left: (@columns / @grid-columns * 100%);
    }
}

See? It's times less code (actually it will be even less verbose since you'll have shared .media-like mixin anyway) and no limits on the count of the breakpoints. And Bob can now use it the way you wanted it:

@import "library.less"

@devices:
    tiny   340px,
    small  768px,
    big   1200px;

.bobs-news-column {
    .make-column-offset(small, 2.42);
}

Additionally see this example.

@rjgotten
Copy link
Contributor

rjgotten commented Oct 1, 2015

In a modern library this can be implemented as

Yup. dictionary/map style lookup on a nested list and a generic mixin that takes the map's key as a parameter. That's what I'd suggest as well.

Given unlocking mixins you could also do nifty stuff like having one mixin set the 'breakpoint key' and unlocking a set of mixins for further manipulation that curry this parameter so your API consumers don't have to repeat it. E.g.

.grid-define(@breakpoint) {
  .grid-gutters(@width) {
    #grid-internal > .grid-gutters(@breakpoint, @width);
  }
}

#grid-internal {
  .grid-gutters(@breakpoint, @width) {
      @media screen and (min-width : at(@breakpoints, @breakpoint)) {
         & {
           margin-left  : (-.5 * @width);
           margin-right : (-.5 * @width);
         }
         & > &col {
           padding-left  : (.5 * @width);
           padding-right : (.5 * @width);
         }
      }
   }
}

used as:

.my-grid {
  .grid-define(small);
  .grid-gutters(10px);
}

Helps a lot if you find you have to define more stuff, like entire systems for shifting/swapping/pushing/pulling columns to/from left or right in various slot distributions.

@markentingh
Copy link

markentingh commented Nov 7, 2016

I actually have a really good example.

I am creating a CSS framework on top of LESS & eventually SASS.

My framework uses 600+ LESS variables exposed to the user specifically used for colors. So the user includes my framework.less in their own theme.less file, then they set up the default values for the 600+ variables in their own .defaults() function.

then it gets tricky.

Each section of their web page can use a different theme, so their header might use a dark theme, body might use a light theme, side bar might use a blue theme, so they'll have to set up 600+ variables for each section of their website, and then they call a .render() function to render the CSS for each section of their website using the provided variables.

This is how I want the workflow for my users to be like:

@imports 'framework.less';

.defaults(){ ... set up 600+ default color variables here ... }

.header{
   .defaults();
   .render();
}
.menu{
    .defaults();
    @bg:#d0d0d0; //slightly different background color from default theme
    @bg-hover:#ddd; //slightly different hover color from default theme
    .render();
}
.footer{
    .defaults();
    @bg:#000; //slightly different background color from default theme
    @bg-hover:#000; //slightly different hover color from default theme
    @font:#fff; //since bg is now black, font should be white
    .render();
}

Now this is where dynamic variable names come to play

If one section in their LESS file uses different colors for a button compared to the default colors, they'll have to change 15 different variable names, just for 1 button type (types include default, apply, cancel, disabled, special, etc). To change all 6 different button types, they'll have to change 90 different variables. Instead, I want them to execute a function that modifies the default variables values for a specific button type before they call the .render() function.

for example:

.sidebar{
    .defaults();
    @bg:#980444;
    .button('apply', #a0e066, #a6eeaa, #fff, #fff, ...etc);
    .button('cancel', #a0e066, #a6eeaa, #fff, #fff, ...etc);
    .button('disabled', #a0e066, #a6eeaa, #fff, #fff, ...etc);
    .render();
}

this allows them to focus on generating different themes for different sections of their web page without having hundreds of lines of LESS, just to change the colors of their theme.

Of course, the .button() function can't use dynamically generated variables at the moment, so instead I use an if statement and hardcoded the 6 different button types with 15 variable names for each type

.button(@name, @bg, @bg-hover, @bg-active, @bg-selected, @font, ...etc...){
    & when(@name = 'apply'){
        @button-apply-bg: @bg;
        @button-apply-bg-hover: @bg-hover;
        @button-apply-bg-active: @bg-active;
        @button-apply-bg-selected: @bg-selected;
        ...etc
    }
}

what I want to do instead is find a way to just change

@button-apply-bg:@bg

to

@tmp-bg:"button-@{name}-bg";
@{tmp-bg}:@bg;

In the future, I would want them to purely use functions to change the different parts of their theme before rendering the theme

for example:

.sidebar{
    .defaults();
    .section('body', #6666ff, #fff);
    .section('row', #6666ff, #fff);
    .section('column', #6666ff, #fff);
    .button('apply', #a0e066, #a6eeaa, #fff, #fff, ...etc);
    .button('cancel', #a0e066, #a6eeaa, #fff, #fff, ...etc);
    .button('disabled', #a0e066, #a6eeaa, #fff, #fff, ...etc);
    .ui('tabs', #6666ff, #fff, ...etc);
    .ui('menu', #6666ff, #fff, ...etc);
    .ui('scrollbars', #6666ff, #fff, ...etc);
    .render();
}

the key concept here is that the user sets up global variables then executes the .render() function to render all the elements of the theme in the correct order.

@seven-phases-max
Copy link
Member

seven-phases-max commented Nov 7, 2016

I'm not convinced. Yet again see my example there. Your example shows nothing but general fail of any frameworks using armies of global variables named like bla-bla-theme-bla-bla-component-bla-bla-actual-variable-name. So in this regard any "string-based concatenation dynamic..ish variables" idea becomes nothing but a kludge to overcome the problem of the initially flawed approach.

So yet again see less/less-meta#12 for the proper namespacing improvement proposal.
Also see quite related discussion on #2442.

(Doh! In other languages they considered global variables to be harmful yet in 1970s. I can understand how that bla-bla-bla-variable-name pattern became widespread in Less, but it's not an excuse to tolerate the incidental "simulating namespaces via variable name concatenation" kludge anymore and definitely not a good idea to push it further. So set on fire and burn burn burn!)

@matthew-dean
Copy link
Member

@markentingh I was going to suggest another option based on scoped, matching mixins. But after I wrote up my example, I ran into this bug: #2984.

Like @seven-phases-max said, we will probably close this issue not because variable referencing / overriding doesn't need improvement, but because a better syntax for addressing some of those things is already on the table.

@markentingh
Copy link

it's alright. Perhaps my whole approach to my framework is wrong to begin with. Keep up the good work, guys.

@seven-phases-max
Copy link
Member

seven-phases-max commented Apr 19, 2017

Renaming the ticket to accommodate both var/mixin cases since they almost always come together as the "Attempting to simulate namespaces and/or parameters via string based manipulation of identifiers" anti-pattern.

@seven-phases-max seven-phases-max changed the title Declare variable with variable name Interpolation of variable, mixin and function names (a.k.a. "Dynamic" variables/mixins/functions) Apr 19, 2017
@mchaov
Copy link

mchaov commented Aug 28, 2017

I think there is a merit here, although not for dynamic variables. See this example:

@color-transformation: lighten;
@some-var: @color-transformation(red, 10%);

@rjgotten
Copy link
Contributor

rjgotten commented Aug 28, 2017

@mchaov

Storing function names inside variables is highly ambiguous. There is no way to discern a keyword from a function name in the parsing phase and the presence of plugins and plugged in functions makes it highly problematic during the evaluation phase as well.

Your particular case will eventually be covered by mixins being able to return values and being able to be passed by reference, like detached rulesets. The pairing of those two features would give Less the ability to create lambdas.

A possible future syntax for that might be:

.some-mixin(@color-transform) {
  @some-var : @color-transform(red);
}

// ...

.color-transform(@color) : @return {
  @return : lighten(@color, 10%);
}
.some-other-mixin(.color-transform);

But it's all still being debated and conceptualized.

@stale
Copy link

stale bot commented Dec 26, 2017

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 stale bot added the stale label Dec 26, 2017
@csergiu
Copy link

csergiu commented Dec 27, 2017

If you don't give us the option to create functions like in Sass then at least let us do dynamic variable names, this is so frustrating, I'm trying to write a mixing to calculate my em size based on the font-size in px and I can't return a custom value to be used in multiple places, so for example if I have:

.something {
  .emSize(20px);
  .emSize2(20px);
  margin: @emSize auto @emSize2;
}

SO I have to create a duplicated mixin just because I can't have a custom returned variable name? At least give us the ability to create functions that automatically return a value, something like this:

.something {
  margin: emSize(20px) auto emSize(30px);
}

I also don't want to write the long version of the property just because it's against the coding style we're using.

@stale stale bot removed the stale label Dec 27, 2017
@seven-phases-max
Copy link
Member

If you don't give us the option to create functions like in Sass

You have it for more than 2.5 years by now.

SO I have to create a duplicated mixin just because I can't have a custom returned variable name?

What is the point of asking to support a dirty kludge instead of asking to support a proper solution instead?

@csergiu
Copy link

csergiu commented Dec 27, 2017

You have it for more than 2.5 years by now.

I was expecting it to be built-in and I didn't know about the plugin, my apologies. I would suggest adding this to the core as it's something that a lot of people are using.

@seven-phases-max
Copy link
Member

seven-phases-max commented Dec 27, 2017

For the particular use-case though, I believe the proper solution would be a an arbitrary-unit-conversion-plugin (autoconverting whatever units (incl. virtual) back and forth depending on its settings), so one would simply write something like: margin: 20epx auto 30epx - faster, shorter and times more readable.

@rjgotten
Copy link
Contributor

rjgotten commented Dec 27, 2017

Agree with @seven-phases-max
A custom function with a signature like em-scalar(value, base) seems what you need here, @csergiu.

Validate that value and base are Dimension nodes with conversion-compatible units -- e.g. mm and cm; pt and px; etc. -- then convert to the same unit; divide to obtain scalar value; change unit to em; and done.

@csergiu
Copy link

csergiu commented Dec 28, 2017

@rjgotten this is what I ended up with:

.function-em-size(@size: 16px, @base: 16px) {
  return: unit(@size/@base, em);
}

@matthew-dean
Copy link
Member

Closing since there a few solutions suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants