-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Overriding dependent variables on a per-scope basis? #2435
Comments
For the reference the following does work on its own: .panel() {
color: @primary;
}
.my-theme {
@primary: green;
article {
.panel(); // -> color: green;
}
} However if you also have P.S. So considering it's a scope priority problem (and not really a lazy-loading problem) @import "foo";
.my-theme article {
#foo.some-button(); // using some nice mixin from the Foo library
// set some parameter for our own .panel mixin and call it:
@primary: -> green;
.panel();
} Now can you guess what can be wrong with this code before I reveal the sources of the "foo.less"? |
Yep - I'm aware - that's what my hacky solution relies on to work at all. Variable interdependence is what makes a big mess of it. The global precedence gets in the way a bit but is easily dealt with by putting vars in their own scope first before inclusion - the variable eval, not so much. |
Btw., just in case, I thought of it a bit more and I guess I forgot to mention that the goal is also possible to achieve by putting your mixins like @primary: red;
@secondary: tint(@primary, 50%);
.whatever() {
.panel() {
color: @primary;
background: @secondary;
}
}
.my-theme article {
@primary: green;
.whatever.panel();
} (Though "parametric namespaces" are a bit more undocumented/unspecified feature to feel too safe). |
Ohkaaay, so why did that work at all? I get that it's "undocumented" but, err, what is that a consequence of exactly? Everything I've read/seen so far would suggest I'm also a bit confused about the edit in your 1st reply - it is a lazy-loading problem from where I'm standing - unless the "scope priority problem" you speak of has to do with the undocumented "parametric namespace" thing you mention above? As to @primary: red;
@secondary: ->(@amount:50%) {tint(@primary, @amount)};
.panel() {
color: @primary;
background: @secondary(); // note explicit "call" brackets
}
// so to clarify - it's not a variable-specific construct, this should also work:
.some-mixin(->() { @primary}); // i.e. passing a it directly
I don't actually see the problem with the That said, requiring explicit eval via I'll also mention that, strictly speaking, I don't think the lambdas values terribly need to be able to take parameters (e.g. allowing |
No, actually nothing is evaluated immediately. It may only look so because the first @primary: red;
@secondary: tint(@primary, 50%);
.lazy-eval {
color: @secondary;
@primary: green;
} The result is greenish.
You did not read #1316 I linked above, did you? :) (yes, it's a language dark corner, we agreed there it's an inconsistency but no idea how it should be changed or can it be changed at all came out yet (there're yet more detailed related discussion about such "parametric namespace" evaluation stuff at #2212)). So I actually use this trick myself in my projects for similar use-cases (even if it's abuse - well, the world is not perfect anyway :)
Nope. See my first example in the first comment, every ancestor scope variable is perfectly lazy-loaded and the problem only is that in many cases (and for many use-cases) you (and me btw.) would prefer
So here's the key: the mixin definition scope hierarchy has higher precedence than mixin expansion one, thus finally resulting in So this is the problem of different scope priority expectations (as everything is lazy-loaded as it should just not in the order we would sometimes want it to).
This way it's not lambda this is a classical pure function defined with awkward syntax (and it's missing return statement :) (which is another big story: #538). And yet again the problem is that you expect your function to evaluate a caller scope variables before a parent scope variables while nothing in its definition (I mean the feature definition not "lambda" definition) dictates this. And you've proposed the whole new feature with the whole new syntax (it's does not really matter if it's lambda or function or whatever) only to force |
Alright took me a few tries but I think I get it now. Variable evaluation really is "fully lazy" and you are of course absolutely right that it's just a scope precedence problem. There is indeed no need to introduce "lambdas" or anything like that. For future reference to anyone coming in from Google, the initial example can be made to work as follows: // core library:
@primary: red;
@bg: #fff;
@fg: #000;
@color-bg: desaturate(mix(@primary, @bg, 50%), 50%); // paler white-ish red
#library() {
// fun fact: variables declared here are completely invisible to anything inside .panel()
.panel() {
background: @bg;
color: @fg;
blockquote {
background: @color-bg;
}
a {
color: @primary;
}
}
}
// these are the tricky use cases I'd like to support:
.my-theme {
@primary: green;
article {
#library.panel(); // should have a pale green background
}
article.serious {
@color-bg: #ccc;
#library.panel(); // keeps the green links, but not colored backgrounds
}
.inverted {
@bg: #000;
@fg: #fff;
#library.panel(); // an "inverted" block - color background is a dark green now
}
} And it works very specifically because of this:
Is this a consequence of implementation by the way? It's really hard to think about this intuitively (without consulting that call order explicitly every time), and it may well be easier to follow what the elevator actually does instead. I'm a bit concerned about how I can properly document the behavior of our |
I won't comment everything (this would be too much, and too primary-opinion based anyway...) Just a few thoughts: Thinking of it more, this whole approach in your recent example starts to look sort of flawed... it's like a "I make some theme then make a mixin hardcoded to use that theme and then searching for ways to patch individual properties of those things on a per-selector basis (or in other words: "providing highest possible abstraction while wishing to retain the lowest possible granularity to control it", this never takes off) ... Well, wishing for a least verbose code is understandable, but propbably the ven more important problem with all this is that nothing in the code of Regardless of above, notice that your last example in the first post can do all that with just a minor modification: .theme(@base: red) {
@primary: @base;
@bg: #fff;
@fg: #000;
@color-bg: desaturate(mix(@primary, @bg, 50%), 50%);
}
.panel() {
background: @bg;
color: @fg;
blockquote {
background: @color-bg;
}
a {
color: @primary;
}
}
.my-green-domain {
.theme(blue);
article {
.panel();
}
article.serious {
@color-bg: #ccc;
.panel();
}
.aside {
@bg: #000;
@fg: #fff;
.panel();
}
.or-maybe {
@color-bg: fadeout(@primary, 60%);
.panel();
}
} This obviously requires some extra duplication in Getting back to "providing highest possible abstraction while wishing to retain the lowest possible granularity to control it" stuff, here're just a few pretty random but related comments/examples/questions inspired by code above. Those can be summarized as something like "OK, it all makes sense but are there any use-case that's really worth getting to deep trying to improve it?", i.e. the code above already looks more like some kind of a DSL-language and while there's always room for improvement in that context (more abstraction, more syntactic sugar, more more) it's probably time to remember that such abstraction may also hurt by cluttering the final goal/result of all that code... So examples: [1]: .my-green-domain {
article {
.panel();
}
article.serious {
@color-bg: #ccc;
.panel();
}
} Now (assuming some real project) I go to browser's style-sheet debugger and... Oh, I guess I'll better rewrite the same as: .my-green-domain {
article {
.panel();
}
article.serious blockquote {
background: #ccc;
}
} Both variants have the same problem of requiring to know internals of the . [2]: .aside {
@bg: #000;
@fg: #fff;
.panel();
} It looks like it's a perfect case for Etc. (actually I have a suspition that for almost any individual sub-selector of this example adopted for some more real-world use-case - a better solution would be in more adoptable In summary (just repeating some thoughts from above in other words): while all these scoping problems are there (and there's always room for some improvement), the particular use-case example (of course assuming it's simplified and real use-cases may vary in details quite wide) does not look like something that makes those "scope improvements" to get some "not lowest" priority. |
Thanks for all the detailed answers just by the by, it's all been very helpful. So, you are quite right that there is a real danger here of defeating our own abstractions by allowing for too much granularity, especially when plain CSS already has a perfectly workable mechanism for such "spot" overrides. The sample use cases however are intentionally stripped down so they are easier to follow. The actual problem I am trying to solve is that for our projects everything needs to be themable through and through. We have a number of types of sites (think "store", "blog", "forums" - etc.) and separate instances of each are deployed for a number of separate brands. Brands all have their own branding style guides that have to be consistent throughout. Furthermore, the sites include a number of shared modular components that have their own non-trivial styling needs (video players, share widgets, that sort of thing.). Something I very specifically want to avoid is a config-var granularity explosion for each site-type and module combination. To make things worse, these aren't "functional" productivity apps where all components can and should be nice and uniform - it's all rich and interactive and the designers love to alternate color schemes on the same page and even within the same area just for visual effect. It's impractical to come up with alternate and inverted variations for every component and hook them in each site-brand combination explicitly. So, the approach I am going to try is to have components only ever be aware of a single "local" set of config vars - What I don't want to have is So the desired end result is that there are a limited number of configurable variables which implementers do need to be familiar with (hence why there need to be a limited amount of them). The level of granularity available for configuring a given component is always restricted to those variables (exceptions can be made if absolutely needed and dealt with on a case by case basis - could be regular CSS overrides, could be exposing an explicit mixin parm, but ideally we can just be smart about our abstraction levels). It's a rough equivalent to OOP where just how re-usable a library is often comes down to how well thought-out and flexible its public interfaces are. You can absolutely bury yourself trying to over-do it with re-usability and abstraction so that's definitely something I'm being very mindful of at the moment. On that note, it would certainly be more explicit and easy to understand to just pass that entire "interface" of config vars as actual parameters to the modular mixins every time you use them, especially if that interface isn't too large. The main thing you lose is being able to define inter-dependent defaults in the params, which may or may not actually be that important in actual use. I may revisit that approach, though my intuition is that the functionality is needed. For example, most of the time Anyway, sorry about the wall of text, no small part of this was writing it all out for my own sake to see if actually looks like an obviously bad idea at a glance. |
I would have to spend an hour at least to fully understand this I think - so @seven-phases-max would you be able to tag or close as appropriate now you've reached a kind-of conclusion? |
Yes, in summary this is discussion around #1316. I kept it initially open since the first post has a feature-request for a special syntax/functionality as a workaround. My bad - I forgot to add tags, sorry. |
I'd like to be able to do something like this:
I think it would be phenomenally useful because it allows individual components to keep a large set of configuration variables without having to worry about the context it's included in. As things stand, every component can only define its configuration in absolute terms with respect to the entire page. For example, we can have a form component, and a nav sidebar component, but they will have their own config vars like
@nav-bg
and@form-input-bg
. If we want to include a form inside the nav, it either has to match all other forms on the page or require its own exceedingly specific config, like@nav-form-input-bg
With the above approach, every component only cares about having "a background" and "a foreground". The form component would only ever need to be written to use
@bg
and@fg
and could be used in endless varieties without modification, just by changing the scope it's included in. Moreover, other components included in the same scope would also automatically share the same basic schemes without needing to configure each one repeatedly.I get that this is basically what parameterization is for, and although it is a perfectly workable solution when you only have a handful of configs, passing around a complete set of theming configs for each invocation is not. Parameter defaults also can't depend on each other - only existing variables in the enclosing scope .
Even with the fairly trivial example above, there is quite a lot of repetition.:
Imagine how quickly things would get out of hand with say, 30 or so config variables (which is a very modest amount of config, frankly, once you add typography, alignment direction, spacing etc etc) and more than just one all-encompassing mixin. Having to restate all dependent variables when trying to override one of the "bases" is also going to be time-consuming and error-prone.
I can sort of make it work with
less
as-is, by separating out dependent variables by their "derivation level" and including them adjacent to each-other in the same scope. It is hacky as all hell but, it works:Hideous.
Are there better ways of doing this sort of thing with
less
as-is? Am I missing some key concept maybe or something? Or can it be covered in a not-quite-obvious way by some seemingly unrelated feature? I'm definitely open to re-thinking this.That aside though - I wonder if this sort of use case could be supported just by adding support for something like extra lazy variables? I know variables are already lazy, but they could actually be quite a bit lazier. We only really need to resolve them when we're about to emit the actual, final CSS - i.e., when interpolated in a property value (or sometimes property name or selector). Simply deriving one variable from another doesn't, strictly speaking, need to resolve either of them.
So, here's what it would all look like using a fictional lazy variable declaration syntax (arrows, just as an example - I'm sure a more "less-y" syntax could be devised) which prevents the RHS from evaluating until they are about to be emitted to the compiled output:
It would basically allow a declarative flavor of variables Ii guess lambdas sort of?) that aren't constants.
Actually, now that I think about it I guess even just straight-up lambdas that have to be explicitly evaluated would work fine - it just introduces the small pain of having to keep track of which config vars have to be evaluated and which don't. Or maybe there can be an eval operator that will silently allow constants to pass through it. Or I could just declare all config vars as lambdas even if they simply return a literal and call it a day. Or a combination of all of the above, introducing a lambda value that can be explicitly evaluated but is also implicitly evaluated when used as a property value, and a tolerant eval operator.
Thoughts?
The text was updated successfully, but these errors were encountered: