Skip to content

Use Dynamically Generated Classes as Mixins #1399

Open
scottgit opened this Issue Jul 3, 2013 · 21 comments

6 participants

@scottgit
scottgit commented Jul 3, 2013

This request is similar (though does not seem to be exactly the same) to the closed issues of #617 and #1133. It is also related to #1325 (both there and here seeks to "use" what Jon there calls "non-existent" classes, so the solving of one may solve the other). (If I missed a more exact statement of the same issue, forgive me for posting this new one.)

In all the issues use cases are sought, which I hope to provide here. The goal is to be able to essentially do the following (though it really becomes more powerful when in a loop structure):

Dynamically build a class name (which we can do now):

.myClassBuilder(@classname) {
   .@{classname} { some-properties: some values;}
}

.myClassBuilder(myName);

/* Outputs */

.myName { some-properties: some values;}

Then use that class name as a mixin (which currently gives a .myName is undefined error):

.my .big > .long .selector + .string {
    .myName;
}

/* Outputs */

.my.big > .long .selector + .string { some-properties: some values;}

Now as to reasons "why" to do it:

  1. Since LESS is designed to use classes as mixins, intuitively users expect it to work (this is evidenced by the links given in the following reasons).
  2. One use case is seen in this StackOverflow question trying to dynamically generate font sizing. Note how the user expected it to work. Now Martin's answer given on that question is a good workaround for that case, but the workaround is still quite cumbersome compared to what the user wanted to do.
  3. Another, perhaps more significant, use case is integrating with frameworks. In such a case, the framework may not be designed to make class properties accessible through a separate mixin like Martin advocated as a solution to the #2 problem. Further, the user of a framework like Bootstrap may not even know or understand the underlying code of Bootstrap itself, and not realize which classes are or are not dynamically generated. This is illustrated some in my answer on this StackOverflow question, which is the same one noted in issue #1325. In such a case again, the expectation is that .span3 would just "work" like any other class that can be used as a mixin (though there, it was really desired to be more "extended").

The key benefit is in those instances where loops are generating numerical differences not only in class names themselves, but properties within those classes. Being able to dynamically generate a number of classes based on whatever formula one wants, and then also being able to use those as a mixin would seem worthy of the effort to implement the feature. I hope this basic idea and the use cases here are enough to warrant a deeper look into this idea.

@lukeapage
Less member

I would be happy for it to work..

BUT

@a: "a";
.mixin() {
    @a: "b";
}
.mixin();
@{a} {
    prop: test;
    @b: test;
 }
 .b();
 .c {
    prop: @b;
 }

less is meant to work no matter the order of the statements.. e.g. you can use a variable that is defined later in the current scope.

that is why we do all the mixin calls before evaluating things that use variables.. but a selector uses variables.. so do the mixin calls twice and make sure we don't do the calls that have already been done? what if they change what the selector was?

The solution I have suggested (I think in the bugs you reference) is that people have to use extend for dynamic selectors.. that doesn't suffer from the above because extend happens after selectors have been calculated..

The next problem with the extend solution (and also calling dynamic mixins) is that we don't parse dynamic selectors.. so we have no idea ".class" is a class.. worst case scenario we would have to take something like this

@a: m;
@b: ~"-";
@c: ~".";
@d: class;
@{c}@{a}y@{b}@{d} {
}

render it to

.my-class

and then re-parse it into

{selector {element .class, combinator ""} }

BUT it would have to cope with selectors we don't recognise and also cope with someone who is purposefully trying to escape '&' because it is now some fancy css selector in 1 years time...

Ok, in the first instance we could just re-parse classes that are simple ^\.[a-Z-]+$ but that is (possibly) a path to inconsistency and pain

If you have any suggestions to work-around these problems (or perhaps a comment from @SomMeri as they are good at this kind of things) then please go ahead

@scottgit
scottgit commented Jul 3, 2013

@lukeapage: Thank you for the detailed explanation of the difficulties involved. I am a creative thinker, but by no means a master at programming, so whether I would be able to come up with a creative solution to the issues you state or not I don't know; but I will certainly meditate on them further. I don't entirely know how LESS works, but your explanation has shed some light on that.

@SomMeri
Less member
SomMeri commented Jul 5, 2013

@lukepage Similar problem already exists in variable evaluation for mixins. Less solves it by making the order of the statements matter in special cases.

Similar problem description: Each mixin call both returns variables into local scope and reads variables from caller scope. Next example defines two such mixins. Each of them creates one variable and uses variable defined in the other one.

.mixin1() {
    @in1: "in mixin1";
    prop1: @in2; 
}
.mixin2() {
    @in2: "in mixin2";
    prop2: @in1;
}

I will call them from the same selector:

#selector {
    .mixin1();
    .mixin2();
}

What happens is that both @in1 and @in2 variables are returned into the selectors scope. However:

  • .mixin1 does not see @in2 defined in .mixin2,
  • .mixin2 does see @in1 defined in .mixin1.

So, the order of statements does matter in this case. Full sample case:

.mixin1() {
    @in1: "in mixin1";
    prop1: @in2; 
}
.mixin2() {
    @in2: "in mixin2";
    prop2: @in1;
}

.outer {
  @in1: "in outer";
  @in2: "in outer";
  #selector {
    .mixin1();
    .mixin2();
    selector-prop1: "in mixin1";
    selector-prop2: "in mixin2"; 
  } 
}

Generated css:

.outer #selector {
  prop1: "in outer"; // mixin1 ignores variable defined in mixin2
  prop2: "in mixin1";
  selector-prop1: "in mixin1"; 
  selector-prop2: "in mixin2"; // variable defined in mixin2 was returned
}

The selector-scope problem @lukepage describes could work the same way as variable evaluation for mixins. E.g. the selector would use the same variable value as its body see (assuming it does not redefine it). It would ignore returned from calls after its definition.

So this:

#outer { // enclosure around original case
  @a: "a"; 
  .mixin() {
    @a: ".b";
  }
  .mixin();
  @{a} { // this would compile int .b
    prop: test;
    @b: test;
   }
   .b();
   .c {
    prop: @b;
   }
}

Would be equivalent to this:

#outer {
  @a: "a";
  .mixin() {
    @a: ".b";
  }
  .mixin();
  .b() { //  @{a} was originally here
    prop: test;
    @b: test;
  }
  .b();
  .c {
   prop: @b;
  }
}

And both would compile into following:

#outer {
  prop: test;
}
#outer .c {
  prop: test;
}
@SomMeri
Less member
SomMeri commented Jul 5, 2013

Another interesting question -should the following work?

Sample input:

@variable: ~".one, .two";

@{variable} {
  declaration: value;
}

#caller {
  .one();
}

Similar case without variables work:

.one, .two {
  declaration: value;
}

#caller {
  .one();
}
@SomMeri
Less member
SomMeri commented Jul 6, 2013

One consequence of my previous suggestion is that following would fail:

.mixin() {
  @variable: .value;
  .value();
}

#space {
  .mixin();
  @{variable} {
    size: 2;
  }
}

while very similar construction would work (most likely):

#space {
  @variable: .value;
  .value();
  @{variable} {
    size: 2;
  }
}

Which is not good, I would expect those two to behave the same way. I will try to think about it more and collect various cases here. They may serve as test cases later on if we decide to do this.

@seven-phases-max
Less member

Yes, unfortunately there're a few scope visibility problems that make "interpolated mixins" matching to fail in certain cases. However as @SomMeri already mentioned those issues are not unique to "intepolation" feature. Another example for collection:

@a: a;

#x {
    .@{a} {
        value: 1;
    }

    .a(); // Error

    & {
        .a(); // OK
    }
}

This one is very suprising (so simple and still fails?), but notice the following example with no interpolation:

.ns {
    .mixin() {
        value: 1;
    }
}

#y {
    .x {.ns};

    .x.mixin(); // Error

    & {
        .x.mixin(); // OK
    }
}

Update: I made further investigation of the example above (the "interpolated" one): interesting, it can be fixed by forcing the variable evaluation right at the "searching for match" point (magically it does not even break the "lazy loading" stuff). But it's hard to predict what other side-effects such forced evaluation may bring in... For example the following code will create a sort of "ghost" mixin:

@a: x;

div {
    .@{a} {
        value: @a;
        @a: y;
    }

    .x; // OK
    .y; // OK
}

result:

div {
  value: y;
}
div .y {
  value: y;
}
@donaldpipowitch

+1
I would like to use interpolated selectors as a mixin. Example:

@prefix: test;

.@{prefix}-hello {
  color: red;
}

.world {
  .@{prefix}-hello;
}
@seven-phases-max
Less member

Btw.,

.world {
  .@{prefix}-hello;
}

this is even yet another feature (something like this but with additional interpolation).

@seven-phases-max
Less member

@donaldpipowitch Sorry, I did not realize that it's not evident that this is partially supported since 1.6.0. For your example the syntax will be:

@prefix: test;

.@{prefix}-hello {
    color: red;
}

.world {
    .test-hello;
}
@donaldpipowitch

@seven-phases-max Thank you for the update. I noticed that this is possible, but I need to know that @prefix is test to use it as a mixin. But this is unlikely, because the purpose of @prefix is to change the value test.

@seven-phases-max
Less member

But this is unlikely, because the purpose of @prefix is to change the value test.

I guess so. So yep, it's more about #965 (combined with this one).
It's all depending on actual use-cases, for example if the use-case would be as simple as above we could write it as just:

@prefix: test;

.hello(@prefix) {
    color: red;
}

.@{prefix}-hello {
    .hello(@prefix);
}

.world {
    .hello(@prefix);
}

So usually it's helpful to mention some real-world use-cases instead of just minimal syntax usage.

@donaldpipowitch

Say I use a library like Font Awesome (https://github.com/FortAwesome/Font-Awesome) which lets me choose the prefix for generated classes with @fa-css-prefix: fa;. An now I would like to use one of the classes as a mixin from within another class, but I don't know if @fa-css-prefix is still fa.

@seven-phases-max
Less member

Say I use a library like Font Awesome

See http://stackoverflow.com/questions/21178092 (mixins are not quite the best way to deal with the FA).

but I don't know if @fa-css-prefix is still fa.

Well, have you seen any project that really changes the value of @fa-css-prefix? Technically if you change @fa-css-prefix you also need to change all .fa stuff in your HTML and if you don't use .fa in the HTML then you don't care what @fa-css-prefix is and then you don't need it to be anything but .fa :). I mean this whole Font Awesome story is not quite good use-case example simply because their only intention of using that @fa-css-prefix as selector interpolator was just to make the code of the project itself more structured and clean, the possibility to change the root prefix is more like a side-effect there or a sort of. In other words, this is just how Font Awesome (its Less version) is written, of course #1399 + #965 will help eventually, but considering that the prev. FA version did support those styles as mixins and the new one does not (and they knew that), they decided to give up this use-case intentionally (that's why I would not use it as a typical use-case for this feature, for instance I suspect that if we have #1177 implemented, the Less version of the FA might be much more elegant (covering all possible FA use-cases w/o #1399 and #965 at all)).

@donaldpipowitch

Lets say I'm the maintainer of "Awesome Library". My library creates classes, which should be used by a user. I prefix all my classes with @al-css-prefix: al;, but my users can choose their own prefix. But lets say I'm using some of my own classes as a mixin or extend for some other own classes - but I don't know which prefix my user choose, so I can't use them.

@Soviut
Soviut commented Jan 30, 2014

What about using a namespace?

@donaldpipowitch

You mean .namespace .class? I don't like that, because it slower, but what really bugs me is that refactoring and maintenance becomes harder. If I have a universal prefix I can use a simple text search to find all files which use this class. (I've upgraded a WordPress template from Bootstrap 2 to 3. It was very difficult to find all places with Bootstrap classes. If I could just search bs- in my Git repo this process would have been much easier.)

@Soviut
Soviut commented Jan 30, 2014

No, actual LESS namespaces. Go read about them on http://www.lesscss.org/

You can use the #id syntax to encapsulate classes and mixins.

@donaldpipowitch

I know them, but they don't solve my problem or do I miss something? I don't want something encapsulated in Less, I want prefixed classes in my CSS output - without Less knowing the prefix beforehand.

@Soviut
Soviut commented Jan 30, 2014

Ah, okay. Nevermind. I thought you were trying to replicate namespacing when a system for it already existed.

@seven-phases-max
Less member

But lets say I'm using some of my own classes as a mixin or extend for some other own classes - but I don't know which prefix my user choose, so I can't use them.

This is a flaw approach. As a library writer you need to demarcate library's front-end and back-end (Imagine a JavaScript library maintainer saying "My library provides some functions that user can rename, now how can I use those renamed functions within my own library?"). In other words, despite the fact that preprocessors allow to use CSS classes as mixins (which is a nice syntax sugar bonus for the end-users), a library writer should avoid re-using its front-end entities as its own back-end entities (at least when those front-end entities are meant to be modified by user). E.g.:
Don't:

.class-1 {
    // styles;
}

.class-2 {
    .class-1();
}

.class-3 {
    .class-1();
}

Do:

// back-end:
.mixin() {
    // styles;
}

// front-end:
.class-1 {
    .mixin();
}

.class-2 {
    .mixin();
}

.class-3 {
    .mixin();
}

Regardless of all those features supported or not supported by Less, this is just the rule#1 to keep a library maintenance easier and future-proof.


So yet again not denying importance of #1399/#965 combo I would say it's not a good idea for a library to rely on those features (as a prefix/namespace resolver for a mixin) even when/if they are implemented.
In context of the FA, notice that it will still be impossible to use classes of "icons.less" as mixins... So...

OK, I think the main idea is clear: if you need to use/provide mixins then do use/provide mixins (parametric). (Yes, these mixins won't be re-nameable just as variables like @fa-var-apple are).
So if I'd be the maintainer of "Awesome Library" my "icons.less" would look like that (just one of possible variants, I'm not really a maintainer so I did not have much time to think of polishing it to the best. Btw, also notice how I use a "namespace" there):

.@{fa-css-prefix} {
    // classes
    .icon-class(glass);
    .icon-class(music);
    .icon-class(search);
    .icon-class(envelope-o);
    // etc.

    // mixin an end-user can use too
    .icon(@name) {
        @var: "fa-var-@{name}";
        &:before {content: @@var}
    }

    // utility mixin
    .icon-class(@name) {
        &-@{name} {.icon(@name)}
    }
}

Above I did put .icon mixin into the same re-nameable namespace just for short (a user still can use it that way since he knows what value .@{fa-css-prefix} has), but as I mentioned earlier it's actually may be better to put it into a separate non-re-nameable one (which still can be just .fa with no conflicts).
Strictly speaking we don't even need explicit .@{fa-css-prefix} {} thing, if we won't use any "backward nesting" tricks (i.e. some-class & {...}) the whole library can be put into a namespace at any level just by:

.whatever-name {
    @import "library";
}

No need to repeat .@{fa-css-prefix} about 440 times as current FA implementation does.

@donaldpipowitch

Thank you for the insight! Very helpful. Maybe my idea was broken by design. I just try to find best practices for Less authoring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.