Mixin and Function Interpolation #626

Open
Snugug opened this Issue Jan 13, 2013 · 132 comments

Projects

None yet
@Snugug
Contributor
Snugug commented Jan 13, 2013

One of the largest stumbling blocks that contributors face right now is being able to cleanly build extendable systems using Sass. While the zip/index method currently proposed as a best practice works OK for variables, it simply does not work for mixins or functions. While we could build a lookup function/mixin for dealing with this, if we are looking to have 3rd party add-ons to a system we've made, the writing of that lookup function/mixin needs to be passed off to the end user, creating a terrible end user experience especially for inexperienced users. As system contributors, we need a way to develop APIs for our systems in a way that is contained from within our extensions. The only way I can see this being accomplished is through mixin and function interpolation.

We are currently running into this problem with attempting to create an Output API for the next generation of Susy, one of the most widely used Compass extensions available (between the two versions, it's something like the 2nd most installed Sass/Compass gem that's not Sass or Compass itself; right behind Bootstrap). While we can create a lookup function/mixin for people to contribute output styles, it leaves the burden on the end user to do it if there are output styles created in contrib. We are thus left with the following user experience, which IMO is terrible:

Inside Extension

$output-styles: isolation, float;

@mixin output-list($input, $output) {
  @if $output == 'isolation' {
    @include isolation($input);
  }
  @else if $output == 'float' {
    @include float($input);
  }
}

@mixin isolation($input) {…}
@mixin float($input) {…}

Inside non-core Output Style

$output-styles: append($output-styles, 'table');

@mixin table($input) {…}

Inside User's file

Would need to write to use non-core Output style, a bit too technical for most users

@debug $output-styles;

Read output styles from debug. Only really works from Command Line, not from GUI apps which many many people use.

DEBUG: "isolation", "float", "table"
@mixin output-list($input, $output) {
  @if $output == 'isolation' {
    @include isolation($input);
  }
  @else if $output == 'float' {
    @include float($input);
  }
@else if $output == 'table' {
    @include table($input);
  }
}

What we'd much much rather prefer is to do the following.

Inside Extension

$output-styles: 'isolation', 'float';

@mixin output-styles($input, $output) {
  @each $style in $output-styles {
    @if $output == $style {
      @mixin #{unquote($style)}($input);
    }
  }
}

@mixin isolation($input) {…}
@mixin float($input) {…}

Inside non-core Output Style

$output-styles: append($output-styles, 'table');

@mixin table($input) {…}

Inside User's file

NOTHING! They'd just need to use the system like they'd expect to be able to without any setup! Everyone's happy!

@chriseppstein
Member

I do think it's incongruous from a user's perspective why they can interpolate a placeholder selector for extend but not for mixin & function definitions and calls. It seems like we can add interpolation for @include and @mixin fairly easily. However, function calls are trickier and I think it may be better to have a generic call($function-name, $arglist...) function that can be used to call a function with the provided arguments.

@Snugug
Contributor
Snugug commented Jan 13, 2013

My primary use case is for mixins, so I'm happy with that solution. The generic call for function works for me too. Both would solve the issues I need solved.

@robwierzbowski
Contributor

For another real world example I'm writing a mixin right now that has a keyword list, and needs to call a related function if an argument passed in contains a keyword. With function interpolation I could do (pseudocode) if $keyword_list contains $var { #{var}() }. So much shorter, more maintainable, more extensible. The call()function would work great too, just a general +1 to the idea.

@Snugug
Contributor
Snugug commented Feb 11, 2013

Any movement/further thoughts on this? We're running into quite a few places where this functionality would be immensely useful

@nex3
Contributor
nex3 commented Feb 23, 2013

I'm reasonably open to this, although I doubt I'll have time to implement it in the near future.

@chriseppstein
Member

I'll take a swing at it. @nex3 do you prefer call or invoke (or something else) for a generic function calling function?

@nex3
Contributor
nex3 commented Feb 26, 2013

call matches JavaScript, which is a good enough criterion for me.

@lunelson
lunelson commented Mar 9, 2013

+1. Was looking for exactly this yesterday, in the same form as with #673. I was surprised to find that it didn't work.

If I understand it correctly, one can do

@media #{$breakpoint} {...}

but not

@include #{$mixin_name};

?

@lunelson

What about a mixin-including function? Something like:

include(mixin_name, args...);

?

@Snugug
Contributor
Snugug commented Mar 11, 2013

@lunelson Interpolating a @media string (which gets printed straight out to CSS) and interpolating a mixin call (which then needs to dynamically call another piece of code) are very different processes, so it shouldn't be too surprising that it didn't work (especially considering you can't interpolate variables or functions either).

As stated above by @chriseppstein, he thinks that they'll be able in do interpolation for mixins with the current @include syntax and he and @nex3 have agreed they like the call syntax for functions, I don't think we need to confuse the issue w/more proposed syntaxes.

@lunelson

Thanks @Snugug, I see your point and I missed @chriseppstein's mention concerning @include above. All good then, glad to hear it.

@joshuafcole

@chriseppstein how goes the work on the call/invoke methods? I ran into the need today and spent an hour trying to homebrew a solution in pure SASS before I realized it was impossible. I'd love to help, but unfortunately I have no ruby experience, and glancing over the apparently relevant files (interpolation.rb and functions.rb?), I realized this might not be the best project to jump in on headfirst.

If there's anything I can do to help, please let me know. In the interim, is their a recommended solution around this? Should I just build a lookup table for the functions I plan to use and a behemoth if tree?

Thanks!

@ghepting

What is the state of this call/invoke function calling function feature?

@nex3
Contributor
nex3 commented May 10, 2013

Low-priority.

@chriseppstein
Member

After thinking about this more, I don't really like the idea of adding interpolation support to mixin names. I dislike interpolation and only ever want to use it when there is no way of doing a more readable syntax. For example, I find this to be less readable than some of the options I demonstrate below:

@include #{$some-mixin}($arg1, $arg2);

Since, we have full control over what is a valid syntax for @include, I don't see a need to resort to a syntax hack. So I would like to consider some different syntax options. Som options that come to mind:

  1. @include $some-mixin with ($arg1, $arg2) -- Any expression that evaluates to a string could be used before the with keyword. I think a space after the with would be optional. However if the with clause is optional for mixins with no arguments (as opposed to with () -- which I dislike), this has a degenerative ambiguity where the expression is a simple identifier. So that leads me to think we need some sort of token early on in the mixin expression.
  2. @include mixin $some-mixin with ($arg1, $arg2) -- What i don't like about this is that it makes include seem like it could include things that aren't mixins, but it does allow the with clause to be omitted for mixins with no arguments. This syntax can be parsed with only a single token look-ahead to disambiguate it from an include of a mixin named mixin.

Thoughts?

@Snugug
Contributor
Snugug commented Jun 24, 2013

I like the former much more than the later, but the more I think about this holistically, the more I'm not entirely thrilled by either especially when compared to interpolation. How, for instance, would either of the options proposed work with #366, assuming that's still on the roadmap? Can the answer be implied interpolation?

What if a variable/expression is included for a mixin name (mixins currently strictly disallow for naming conventions that would look like a variable/expresion, and this would stay) that it would attempt to resolve the variable/expression? If it doesn't resolve to a string, throws an error. Once it's been resolved to a string, it uses that as its mixin name? From there it's just mixin as normal, probably passing through a check to see if the mixin exists (and if not, throwing an error).

Something as simple as the following:

$foo: 'bar';

@mixin baz {
  content: 'baz';
}

.baz {
  @include $foo;
}

This would also work with the syntax in #366:

.baz {
  ++$foo;
}
@cimmanon

Most of the desire to use interpolation on mixins/functions stems from the fact that they are not first-class entities (ie. they can't be passed as arguments to other mixins/functions). If mixins should become a first-class entity, we'll have to have some way of expressing that anyway, even if adding interpolation wasn't on the table.

Snugug's proposed @include $my-mixin syntax seems the simplest, most logical way to go and would be an easy concept for non-programmers to grasp.

@chriseppstein
Member

@Snugug @cimmanon The issue here is that using a variable is not forced. Any valid SassScript expression would be allowed, including no indirection at all. So any syntax we choose must allow for a bare identifier to be unambiguous. This is the case for @include $my-mixin, but this implies that @include $my-mixin($a, $b) or @include some-fn-that-returns-a-mixin-name($asdf)($a, $b) would be valid, but they make me a bit squeemish.

@Snugug
Contributor
Snugug commented Jun 24, 2013

@chriseppstein That's exactly what I'm implying. I would assume the same would be true for either of your proposed syntaxes, and that's how it would work with interpolation. For me, the most common use for this feature would be something like the following (and why I like interpolation):

$mixins: 'foo', 'bar', 'baz';

@mixin api($mixin-type) {
  @each $mixin in $mixins {
    @if $mixin-type == $mixin {
      @include namespace($mixin) {}
    }
  }
}

Or, with interpolation, would look something like the following:

$mixins: 'foo', 'bar', 'baz';

@mixin api($mixin-type) {
  @each $mixin in $mixins {
    @if $mixin-type == $mixin {
      @include #{$mixin}-namespace {}
    }
  }
}
@chriseppstein
Member

@Snugug the problem is that we need to know what parts of that expression are the SassScript that means the mixin name and what parts are the argument list so that we can parse them appropriately. It's very hard to suss out the last parenthesis group using either a regexp or recursive descent parsing. As such, I'm pretty sure we need to have a way to demarcate the argument list expressions from the name expression. (correct me if I'm wrong, @nex3)

@Snugug
Contributor
Snugug commented Jun 24, 2013

Ahh, I understand your point now. In that case, this brings me back to one of two things that I think are the best options for this; either magical interpolation but only allowing for variables, or interpolation. I understand that you're not a fan of interpolation, but it does make that distinction for us that you're looking to have, it's an established (if not liked) pattern, and it doesn't add additional complexities to the mixin syntax, allowing for the mixin syntax to take on other forms. Additionally, when it comes to #366 for instance, one of the wants is mixins to look like properties using that syntax, and having interpolation brackets would bake it look identical to property interpolation that we can do now.

@chriseppstein
Member

Regarding #366, that syntax is simply sugar to support a common use case, but as I envision it, it does not express the full power of the @include directive. As such, it's just a shortcut parsing scheme for generating an include directive and it could support interpolation like properties do, since the goal of that syntax is to be more property-like. So I don't consider this decision to be linked to that one.

Forcing the use of a variable is unnecessary and arbitrary -- the only places we do that are where the variable is an L-Value (being assigned to).

And I don't agree that interpolation is a better option than the one I specified above. It's a jumble of curly braces and hash signs and it's hard to read. I don't see a need to escape our own syntax. CSS is a verbose language and we do not need to be terse here. I think the include syntax for dynamic mixin names can and should more closely resemble the @for directive.

@Snugug
Contributor
Snugug commented Jun 24, 2013

Maybe where I'm getting caught up is the additions to the @include syntax proposed to allow for this to happen with, to me, don't feel right. Maybe the answer is a new directive so we have full control over it, something like @call? That way, this would work closer to how #812 works? So something like the following:

.foo {
  @call baz [with (arglist)];
}

.bar {
  @call qux [with (arglist)] {
    content: 'Qux';
  }
}

I'd even be happy with @call mixin baz… instead of just @call baz if that meant that there was a SassScript API of some sorts of creating new literals.

@chriseppstein
Member

I don't think a completely different directive is warranted to accomplish this. It's still just an include of a mixin. If I'm reading a file and I see @call and I don't know what it is and I have to go study it. If I see a slightly different way of using @include I can correctly infer what is going on without having been taught it.

@Snugug
Contributor
Snugug commented Jun 24, 2013

I'm not entirely sure I agree with the later part of your statement. If I saw @include mixin $foo as a green user, I'd be confused why I needed to tell the interface that's used for using mixins that I'm using a mixin.

@chriseppstein
Member

@Snugug right. As I stated originally "What i don't like about this is that it makes include seem like it could include things that aren't mixins". But I don't want to solve this with interpolation, nor a new directive. Some other keywords are fine though.

@Snugug
Contributor
Snugug commented Jun 24, 2013

Is there a way for us to solve both use cases presented with the current @include syntax? A semantic suffix works when there is an arglist, but not when there isn't. A semantic prefix works without an arglist, but makes it appar as if there could be more to @import. What about a flag of some sorts like is being used for @extend?

@include $foo-bar !flag [(arglist) {}]

Possible options for !flag could be:

  • !optional
  • !dynamic
  • !call
  • !eval
@cimmanon

@chriseppstein Unless the user is really confused, I highly doubt anyone is going to believe that @include can be used on anything other than a mixin. Other languages seem to do just fine without adding new language constructs for functions vs variables containing functions.

It's been quite a while since I've used PHP, but this should work (also note that passing functions around as arguments to other function is done with the function name as a string, or at least it used to be).

$my-function = 'is_string';
echo $my-function('foo');

JavaScript that we all know and love...

var foo = function(x) { return x * 2 }
console.log(foo(3));

I've been using Haskell in my day to day programming for the past year or so and there's no differentiation between "variable" and "function" (partially because there's no such thing as variables in Haskell), everything is an expression.

foo :: Bool
foo = True

bar :: Bool -> Bool
bar x = not x

If I try to pass an argument to an expression that takes no arguments (eg. foo False), then the compiler informs me that I'm doing it wrong.

@robwierzbowski
Contributor

@chriseppstein: Is your argument against interpolation solely readability? Each of the alternate suggestions puts a higher mental burden on me than reading an interpolated string.

If interpolation isn't going away for variables, i.e. #{$foo}-something, I would much rather see an existing language construct reused for an expected result than a new new directive added.

For me, #{$foo}(args) is the most understandable, readable option so far.

@chriseppstein
Member

@cimmanon I don't let PHP inspire any of my language design :)

In the javascript case, the variable foo is bound to a function reference. SassScript does not have first class references for mixins and functions. Are you proposing that we create such a construct? To this point, it hasn't seemed necessary and I still don't see a strong argument for it. It implies we have to introduce new definition constructs, etc. which I'm not a big fan of.

@robwierzbowski We've rejected interpolation for variables. Instead, we're adding a map data structure in 3.3. Regarding you claim that you find interpolation easier to read than a keyword, all I can say is that whenever I see a large block of code making heavy use of interpolation, I find it ugly and unreadable. Maybe this isolated use is not, in and of itself unreadable. But it adds to a general unreadability for code en-masse. It's obviously an aesthetic, so we're not going to reach agreement through persuasive arguments, but I appreciate your feedback.

@robwierzbowski
Contributor

@chriseppstein Thanks. If the common interpolation use case is being replaced with maps I rescind my argument.

@cimmanon

@chriseppstein Until I started using languages that offered first-class functions and currying, I didn't think I needed them either. Of course I'm not suggesting that you take inspiration from PHP, but for better or worse it is a language most web programmers are familiar with. As for the JavaScript example, I could have just as easily written var foo = parseInt or any other previously defined function.

Consider a project like Sassy Buttons. No disrespect intended towards the author, but all of those arguments make one imposing mixin.

@mixin button($style, $color, $mixins...) {
    $gradient: button-gradient($style, $color);
    $color1: button-highlight($gradient);
    $color2: button-lowlight($gradient);

    background: $color1;
    @import background-color(linear-gradient($gradient...));

    @each $m in $mixins {
        @include $m($color1, $color2);
    }
}

@mixin button-border($color1, $color2) {
    border: 1px solid $color1;
}

// other mixins here ...

.foo {
    @include button(shiny, red, button-border, button-hover, button-active, custom-button-shadow);
    @include border-radus(.25em);
    @include button-text(inset, white);
}

To get the same conciseness, you'd have to muck around with global variables.

@chriseppstein
Member

@cimmanon I've used plenty of languages with first class functions. I'm well aware of the benefits they provide.

I'm confused. The Sassy Buttons example seems like you're making a case for whether it should be possible to invoke a mixin by determining the name of the mixin in SassScript, but that is not the subject of this conversation, we've already agreed to that. The question is simply what syntax it should take.

@robwierzbowski
Contributor

Another syntax for thought: #812 adds a function that calls functions. To keep things consistant, we could have a mixin-like construct that calls mixins:

@include mixin($foo, args) {
}

$foo above could be any Sass expression (function, variable, string) without causing parenthesis clashes that necessitate with. Seems simple enough that I'm probably missing something that invalidates it.

@Snugug
Contributor
Snugug commented Jun 26, 2013

I actually agree with Rob here, was on my way to post the same suggestion.

@chriseppstein
Member

@robwierzbowski I had two concerns with that:

  1. it obviously precludes defining a mixin named mixin. I'm not super worried about this one. as the same issue exists for the call function. However, there's an interesting side note: you can use call to call a call you've defined yourself.
  2. Maybe it's mental baggage, but imagining a function that calls my function and returns the value it returns seems nice and tidy because the forms of the two are so similar. But with mixins, the presence of @include makes the word mixin seem like it's not the same thing as a mixin. But try as I might, I keep not finding any rational explanation for this feeling.

So maybe it's ok. Thoughts, @nex3?

@robwierzbowski
Contributor
  1. I thought about that for a minute too, but as you say it's the same with call().
  2. I agree, it could be a little odd at first. In my head it almost looks like a function calling a mixin. But I'm sure people would acclimate quickly, especially with the parity with call().
@lunelson

It's pedantic maybe, to point this out, but semantically the equivalent of call() would be include(), as both are verbs rather than mixin() which is a noun.

I suggest considering the following

$foo: foo;
@mixin foo($x,$y){...}

// normal include
@include foo($x, $y);

// interpolated include
@include($foo, $x, $y);
@chriseppstein
Member

@lunelson That's a very good point. And I like where that is going, except I don't like that this directive now looks like a function call, where all the others look like statements.

After all this, I still find the keyword based syntax to be the most readable and unambiguous:

@include mixin $foo with ($x, $y);

However, maybe there are better keywords. E.g. with could be passing, mixin could be named?

@lunelson

FWIW I think the keyword syntax as you just wrote—with those keywords—is fine. My suggestion is concise; but I allow it is perhaps syntactically confusing, being a directive with arguments, which has no precedent. I just felt it was the closest 'equivalent' of using call() vis a vis functions

@lunelson

However, I'd say perhaps allow straight interpolation anyway? i.e. @include #{$foo}($x,$y)—for the sake of dryness—even if the recommended and documented implementation uses keywords?

@Snugug
Contributor
Snugug commented Jun 26, 2013

The more I see @include mixin $foo [with (arglist)], I'm generally OK with it, but I'd have to agree with @lunelson that, while I'm OK with it, it doesn't seem as elegant a solution as interpolation (which I know you dislike). All of the suggestions presented, including the functions presented by @robwierzbowski and the flag syntax I proposed, while are good alternative syntaxes, do not provide the same instantly-understandable interpolation syntax that, especially for system developers like us, we're very use to needing to do it currently.

In the end, while I'm just generally going to be ecstatic with getting this feature in, maybe we don't have to push so hard for a new syntax? But that's me.

Also, very interested in @nex3's thoughts.

@codingdesigner

just did a quick catch-up on this. I'm sure I missed nuance.

@include mixin $foo with ($x, $y); seems like the clearest and easiest to learn syntax put forth. I'd be happy with that.

@scottkellum

I’m ok with this. Seems like mixin is implied by @include and the args are implied by standard mixin syntax but sounds like there are some technical limitations to that.

@robwierzbowski
Contributor

I'm down with @include mixin $foo with ($x, $y) as well. If the mixin part could be dropped, even better, but if not no problem.

@Snugug
Contributor
Snugug commented Jun 26, 2013

If everyone is generally OK with mixin/with, I am as well. I say go for it.

@chriseppstein
Member

We need a 👍 from @nex3. 🙏

@blackfalcon

Call me a little crazy, just putting this out there, feel free to put it right back.

But @include mixin $foo with ($x, $y) at first glance from a newb/tutorial perspective is a little confusing. As it currently stands, the name of a mixin is out there without any special characteristics other then being preceded by @mixin, @include or alternatively + or =.

As we all know, as of now mixin is not a reserved word, it is perfectly legal for me to create a mixin called 'mixin', although is looks pretty stupid, it happens.

Seeing something, like the following, knowing the rules that currently regulate the language, I would assume that I can replace mixin with any other 'mixin' name I create. The passing in a variable with arguments is an entirely new construct I would need to learn/understand.

+mixin $foo with ($x, $y)

The interpolation syntax is a little loud and in some cases difficult to read. But any developer looking at this can quickly deduce what is happening as String interpolation is very common. And in this case, I can see very clearly that the result of what is happening is that the string being pulled from the variable is being put in place of the mixin statement and then arguments being applied to said mixin.

+#{$foo}($x, $y)

or

@include #{$foo}($x, $y);

Coming into this conversation late is an interesting perspective as I can see all sides of the argument. The one thing that really jumps out at me is Chris' statement "We've rejected interpolation for variables. Instead, we're adding a map data structure in 3.3."

What does this look like and is there a way to marry these concepts together?

@blackfalcon

Oh BTW - having mixins inside mixins would be AWESOME!!!!

@lunelson

Yes FWIW I think straight interpolation (+#{$foo}($x, $y)) is the most logical and elegant, even if less than pretty. It is also similar to the way it is allowed in media queries.

@chriseppstein
Member

@lunelson It's not just "less pretty". It's complete jibberish and it's the kind of thing that in aggregate makes Sass unapproachable to inexperienced developers. We don't use interpolation like that for any Sass construct. In all cases, interpolation is a way for Sass to inject itself on top of existing CSS syntax.

@blackfalcon The map data structure is discussed in #642 and I don't know what you mean by "marry these concepts together". Regarding the indented syntax, I have to admit that I don't like the direct translation of the shorthand include notation for this proposal and I think we'd just require use of the full @include in the indented syntax to get dynamic evaluation of mixin names.

@lunelson

@chriseppstein I didn't mean to provoke, I was simply acknowledging that one might validly object to how it looks. However I dont' think it's jibberish either. I think devs who need this feature will find it readable.

That being said I see your point re: interpolating of CSS declarations vs interpolating of Sass directives in terms of logical consistency; but if one decides that this is not a place to use string interpolation, then consider whether variables should be allowed directly, e.g. @include $foo($x, $y); (which is just your syntax with the keywords removed): if that doesn't seem right to you, then doesn't it suggest that interpolation is sensible?

@chriseppstein
Member

I didn't mean to provoke

I didn't mean to sound so harsh.

I think devs who need this feature will find it readable.

Perhaps. But I think the devs who don't need this feature but are trying to understand the code built by the people who did, will be very confused. This is a very important use case to me.

if that doesn't seem right to you

It doesn't seem right to me. It makes it look like the value returned by $foo is some sort of callable value which it is not, it is just a string which will have special meaning to the include directive so I want it to look very much. There are other parsing based reasons to not allow that syntax as I've mentioned above, for instance the expression can and should be any SassScript expression including but not limited to function calls, strings, string concatenation, etc. Many of those use cases will look very strange with this syntax.

then doesn't it suggest that interpolation is sensible?

Nope. there's not only two options here.

I get that for the most part this is an aesthetic bikeshed and we probably won't make everyone happy with the decision. I'd like to put this conversation on hold until we have @nex3's thoughts on the matter.

@chriseppstein
Member

Another option to avoid a leading keyword: Require a with keyword following the name expression for dynamic includes even when there is no argument. E.g.:

@include <name expression> with no arguments;

or

@include <name expression> with ();
@Snugug
Contributor
Snugug commented Jul 1, 2013

Hmm, with no arguments seems a bit clunky to me

On Monday, July 1, 2013 at 11:36 AM, Chris Eppstein wrote:

Another option to avoid a leading keyword: Require a with keyword following the name expression for dynamic includes even when there is no argument. E.g.:
@include with no arguments;

or
@include with ();


Reply to this email directly or view it on GitHub (#626 (comment)).

@Snugug
Contributor
Snugug commented Jul 7, 2013

I've been thinking about it, while a bit clunky, I'm OK with it. @nex3, thoughts? This functionality is really needed for advanced Sass work.

@jslegers

Sorry for the duplicate

I'm currently working on a metaframework in Sass where I wanted to implement something like this (nevermind the syntax) :

@mixin setComponent($component, $rules : (), $components : ()) {
  @if $extendComponents == true {
    %#{$component} {
      @include rules($rules);
      @include components($components);
    }
  } @else {
    @mixin #{$component} {
      @include rules($rules);
      @include components($components);
    }
  }
}

@mixin getComponent($component) {
  @if $extendComponents == true {
    @extend %#{$component};
  } @else {
    @include #{$component};
  }
}

@include setComponent(
  component-block,
  (
    (display,block)
  )
);
@include setComponent(
  component-gutter,
  (
    (padding,10px)
  )
);
@include setComponent(
  component-gutter-block,
  (
  ),
  (
    (component-block),
    (component-gutter)
  )
);

.cell {
  @include getComponent('component-gutter-block');  
}

For this kind of thing to work, it is quite essential to be able to create both extends and mixins dynamicly.

I was glad to see that dynamic extends work as expected. This construct works perfectly :

%#{$component} {
     // do something
}

Unfortunately, I got stuck with the dynamic mixins. The following gave me an error :

@mixin #{$component} {
     // do something
}

I tried dynamic variables as a workaround, but the following didn't work for me either :

$var1 : parametername;
$$var1 : 5;
$var1 : parametername;
$#{$var1} : 5

Thusfar, the only workaround I can think of, is to do something like this :

$dynamicMixins : (
    (
        component-block,
        (
            (display,block)
        )
    ),
    (
        component-gutter,
        (
            (padding,10px)
        )
    ),
    (
        component-gutter-block,
        (
        ),
        (
            (component-block),
            (component-gutter)
        )
    )
);

@mixin getDynamicMixin ($component) {
    @include setParameters(getDynamicMixinPars($component));
}

This seems like a very inefficient and convoluted workaround, though, which means I'm probably going to skip this feature until dynamic mixins are available.

I can barely wait until they are, though. The meta-programming possibilities are endless :-)

@chriseppstein
Member

There's no harm in just defining both the placeholder and the mixin. Only
the one you use will be in the output and no indirection is necessary.

Chris

On Friday, July 19, 2013, John Slegers wrote:

Sorry for the duplicate

I'm currently working on a metaframework in Sass where I wanted to
implement something like this (nevermind the syntax) :

@mixin setComponent($component, $rules : (), $components : ()) {
@if $extendComponents == true {
%#{$component} {
@include rules($rules);
@include components($components);
}
} @else {
@mixin #{$component} {
@include rules($rules);
@include components($components);
}
}
}

@mixin getComponent($component) {
@if $extendComponents == true {
@extend %#{$component};
} @else {
@include #{$component};
}
}

@include setComponent(
component-block,
(
(display,block)
)
);
@include setComponent(
component-gutter,
(
(padding,10px)
)
);
@include setComponent(
component-gutter-block,
(
),
(
(component-block),
(component-gutter)
)
);

.cell {
@include getComponent('component-gutter-block');
}

For this kind of thing to work, it is quite essential to be able to create
both extends and mixins dynamicly.

I was glad to see that dynamic extends work as expected. This construct
works perfectly :

%#{$component} {
// do something
}

Unfortunately, I got stuck with the dynamic mixins. The following gave me
an error :

@mixin #{$component} {
// do something
}

I tried dynamic variables as a workaround, but the following didn't work
for me either :

$var1 : parametername;
$$var1 : 5;

$var1 : parametername;
$#{$var1} : 5

Thusfar, the only workaround I can think of, is to do something like this :

$dynamicMixins : (
(
component-block,
(
(display,block)
)
),
(
component-gutter,
(
(padding,10px)
)
),
(
component-gutter-block,
(
),
(
(component-block),
(component-gutter)
)
)
);

@mixin getDynamicMixin ($component) {
@include setParameters(getDynamicMixinPars($component));
}

This seems like a very inefficient and convoluted workaround, though,
which means I'm probably going to skip this feature until dynamic mixins
are available.

I can barely wait until they are, though. The meta-programming
possibilities are endless :-)


Reply to this email directly or view it on GitHubhttps://github.com/nex3/sass/issues/626#issuecomment-21236740
.

@jslegers

The problem is that I need to be able to define the name of the mixin dynamicly, like this :

@mixin setcomponent($component, $args : ()) {
    @mixin #{$component}($args) {
       // doSomething
    }
  }
}

@mixin getcomponent($component, $args : ()) {
    @include #{$component}($args);
  }
}

.tab {
  @include component('tab');
}
table {
  @include component('table');
}
td, th {
  @include component('table-cell');
}

This allows me to programmatically generate any number of mixins I want.

Hardcoding mixins the traditional way doesn't provide me the meta-programming capabilities I need.

For placeholders it works fine, but it doesn't work for mixins.

@Snugug Snugug referenced this issue in Compass/compass Jul 19, 2013
Closed

Mixin variable variable? #1343

@chriseppstein
Member

I don't understand why you need to define the mixin dynamically -- there is no such planned feature and it's more unintuitive than just defining the mixins explicitly. Here's how I'd do this in 3.3 -- the bookkeeping gets way easier with maps.

$components-defined: ();
$extend-components-default: false;
$extend-components: (table: true, table-cell: true);
@function extend-component($name) {
  @return if(map-has-key($extend-components, $name),
                 map-get($extend-components, $name),
                 $extend-components-default);
}
@mixin initialize-component-or-extend($name) {
  @if extend-component($name) {
    @if not map-get($components-defined, $name) {
      $components-defined: map-set($components-defined, $name, true);
      @at-root {
        %#{$name} {
           @content;
        }
      }
     }
     @extend %#{$name};
  }
  @else {
    @content;
  }
}
@mixin table {
  @include initialize-component-or-extend(table) {
    table-stuff: here;
  }
}
@mixin tab {
  @include initialize-component-or-extend(tab) {
    tab-stuff: here;
  }
}
@mixin table-cell {
  @include initialize-component-or-extend(table-cell) {
    table-cell-stuff: here;
  }
}
.tab {
  @include tab;
}
table {
  @include table;
}
td, th {
  @include table-cell;
}
@chriseppstein
Member

Notice how this provides a very familiar interface for the sass developer while still producing the desired output -- the extra indirection is just obfuscating the goal -- afaict.

@chriseppstein
Member

@jslegers also, I think what you're trying to do has already been done by this plugin: https://github.com/linkedin/archetype. We use it at work, but I dislike the syntax it uses, and I want to make it more in line with standard sass/css syntax.

@jslegers

Let me explain what I'm trying to achieve here.

I created Cascade Framework a while ago. The CSS code is optimised for footprint and flexibility, which makes it difficult to read. See https://github.com/jslegers/cascadeframework/blob/master/assets/css/cascade/development/build-full.css .

To optimise the maintainability, I'm building a layered metaframework around it to provide a simple yet powerful way for maintaining the framework.

Currently, the most high level syntax looks like this :

// Definitions
@include register((4px),margin);
@include register((18px 0),margin,hr);
@include register((18px ),margin-bottom);
@include register((9px ),margin-bottom);
@include register((4px ),margin-bottom);
@include register((-1px ),margin-bottom);
@include register((-1px ),margin-bottom,ie6-7);
@include register((-1px ),margin-bottom,ie6);
@include register((4px ),margin-left);
@include register((9px ),margin-left);
@include register((4px ),margin-right);
@include register((5px ),margin-right);
@include register((-1px ),margin-top);


// Selectors
button {@include button(button);}
dd {@include extend(definitionlist);}
hr {@include extend(horizontalrule);}
img {@include extend(image);}
.bottom-nav {@include extend(tab-block-navigation-bottom);}
.button {@include extend(button);}
.button-group {@include extend(buttongroup);}
.icon {@include extend(icon);}
.label {@include extend(label);}
.left-nav {@include extend(tab-block-navigation-left);}   
.mediaobject {@include extend(mediaobject);}
.top-nav {@include extend(tab-block-navigation-top);}
.right-nav {@include extend(tab-block-navigation-right);}

Output with global variable $optimiseOutput : true; :

.mediaobject .cell {
  margin: 4px;
}

.mediaobject .cell {
  margin: 4px;
}

d {
  margin: 4px;
}

img {
  margin-bottom: 4px;
}

.left-nav .tab-content, .top-nav .tab-content {
  margin-bottom: -1px;
}

.bottom-nav .tab-content {
  *margin-bottom: -1px;
}

.bottom-nav .tab-content {
  _margin-bottom: -1px;
}

.bottom-nav .tab-content {
  _margin-bottom: -1px;
}

.icon {
  margin-left: 4px;
}

dd {
  margin-left: 9px;
}

.icon, img {
  margin-right: 4px;
}

button, .button, .button-group, .label {
  margin-right: 5px;
}

Output with global variable $optimiseOutput : false; :

button, .button {
  margin-right: 5px;
}

.button-group {
  margin-right: 5px;
}

hr {
  margin: 18px 0;
}

dd {
  margin-left: 9px;
}

.icon {
  margin-left: 4px;
  margin-right: 4px;
}

img {
  margin-bottom: 4px;
  margin-right: 4px;
}

.label {
  margin-right: 5px;
}

.mediaobject .cell {
  margin: 4px;
}

.bottom-nav .tab-content {
  _margin-bottom: -1px;
}

.left-nav .tab-content {
  margin-bottom: -1px;
}

.top-nav .tab-content {
  margin-bottom: -1px;
}

.bottom-nav .tab-content {
  *margin-bottom: -1px;
}

Unfortunately, the I have some trouble implementing the "register" mixin, as it needs one of the following to function properly.

Option 1 : test whether a placeholder exists

  @function register () {
        @if exists($placeholder) == false {}
  }

Option 2 : read and write values to a global variable from within a mixin

  $register : ();

  @mixin register () {
        $register : append($register, $value)
  }
  @function register () {
        $value : index($register, $key)
  }

Option 3 : define a mixin with a variable name

   @mixin register() {
        @mixin #{$component}($args);
    }

I basicly need to be able to dynamicly write a value in the global context from within a mixing and read it from within another mixing. Maybe there's another option I'm overlooking?

It is possible to achieve the same effect in a different way, though, but it's not as powerful and requires a far more convoluted syntax :

$margin-bottom-18px : 18px;
$margin-bottom-9px : 9px;
$margin-bottom-4px : 4px;
$margin-bottom--1px : -1px;
@include generateList(margin-bottom, (
        $margin-bottom-18px,
        $margin-bottom-9px,
        $margin-bottom-4px,
        $margin-bottom--1px
));

$margin-bottom--1px : -1px;
@include generateList(margin-bottom, (
        $margin-bottom--1px
), ie6-7);

$_margin-bottom--1px : -1px;
@include generateList(margin-bottom, (
        $margin-bottom--1px
), ie6);
@chriseppstein
Member

Maybe there's another option I'm overlooking?

I think you're overlooking the option I prescribed above. Why doesn't it work for you?

@scottkellum

@jslegers This is a great discussion for the Sass mailing list

Issues on point with isolated code examples.

@chriseppstein
Member

@jslegers There's about a dozen people on this thread. I'd like to move it elsewhere. This is about dynamic includes, you're asking for dynamic mixin definitions. I'm pretty sure this isn't going to happen, but if you want to continue the conversation, let's start a new issue.

@jslegers

@ chriseppstein

I basicly need to be able to dynamicly write a value in the global context from within a mixin and read it from within another mixin to allow me to provide a simplified global register I can check for configurations.

It would allow me to use the following code for setting the default margin and padding of the dynamicly generated %horizontalruler placeholder in the global register :

@include register((18px 0),margin,horizontalruler);
@include register(0,padding,horizontalruler);
@include register(block,display,block);

Then I do this :

%horizontalruler {
    register(margin,horizontalruler);
    register(padding,horizontalruler);
}

%block {
    register(display,block);
}

div {
   @include extend(block);
}

hr {
   @include extend(horizontalruler);
}

.hr {
   @include extend(horizontalruler);
}

And I get this output :

div {
  display: block;
}

hr,.hr {
   margin : 18px,0;
}

hr,.hr {
   padding : 0;
}

I can control at 3 levels in which order my code is output :

  • The order of registering the parameters;
  • The component placeholders
  • The selectors

You can change each parameter individually or group parameters. The order of the CSS code automaticly adjusts to those changes, allowing you a chance to rapidly reorganise your code in an optimal way.

For me, this would make the maintenance and customisation of Cascade Framework a hell of a lot more efficient!

These features already work, but right now the syntax is far too convoluted.

@chriseppstein
Member

I repeat: let's start a new issue

@jslegers

@ chriseppstein

I had started issue #857 a day ago. It was user @Snugug who said it was a duplicate of this issue, refered me to here and closed my issue.

Should I reopen it?

@lunelson

So this thread has been inactive for a while. Those who were most interested are probably working with 3.3.0rc1 now to see what can be done; but as far as I can tell the original concerns haven't been completely addressed and I'm wondering where it all stands. As @chriseppstein said:

it's incongruous from a user's perspective why they can interpolate a placeholder selector for extend but not for mixin & function definitions and calls

In 3.3 we have a call() function for dynamic function calls, and maps as a workaround for variable interpolation but still no dynamic calling of mixins. So what's the state of mind then, re include $mixin with or an include($mixin, $args...) function? I would also say that IMHO the call() function suggests an obvious and simple way to handle dynamic variables, namely: return($variable-name). Such a function would handle the use-case of accessing variables by [interpolated] name (i.e. without the $ sign), just as call($function-name, $args...) does for functions. I say this because I find that maps functionality—of which I am a big fan, and which creates exciting new potentials—is overkill in many cases for simple dynamic call of a variable.

@jslegers

I fully agree with @lunelson.

I really like the PHP syntax for variable interpolation : just add another $ sign for every level of interpolation :

$name : 'Chris'; // assign value 'Chris' to variable $name
$foo: 'name'; // assign value 'name' to variable $foo
$bar : 'foo'; // assign value 'foo' to variable $bar
$foobar : $$bar ; // assign value 'name' of variable foo to variable $foobar through interpolation of variable $bar
$meta : $$$bar;  // assign value 'Chris' to variable $meta through secondary interpolation

It may not be the most readable code, but it does seriously open up SCSS's metaprogramming potential... which is very useful at a (meta)framework level.

@chriseppstein
Member

but still no dynamic calling of mixins

It just didn't make it into Sass 3.3 because we couldn't find a syntax we agreed on. This is simply a fact of how we build Sass. We don't release things we aren't happy with. This capability will be in a future release and is why we haven't closed this issue.

I really like the PHP syntax for variable interpolation

I really hate it. Because:

It may not be the most readable code

Bingo.

but it does seriously open up SCSS's metaprogramming potential...

Every reasonable use case I've seen for variable interpolation is just association of data. Maps are sufficient for this purpose. Variable interpolation is just misusing the internal variable lookup table as a map and limits lookup keys to strings, which maps do not do.

suggests an obvious and simple way to handle dynamic variables, namely: return($variable-name).

It's not just a syntax issue, I just don't see enough benefit this feature brings over maps for the increased complexity, the existence checks that we provide for variables are sufficient, IMO to cover all the use cases I've seen so far.

I find that maps functionality ... is overkill in many cases for simple dynamic call of a variable

I disagree with this. I don't find variable interpolation simple, obvious, or readable.

@jslegers

@chriseppstein :

If people prefer interpolation to maps for a specific implementation, shouldn't they be able to so choose that syntax instead of having the language creators decide it for them?

And while I get your point about variable interpolation, i don't see why you don't just allow any part of your SCSS to be interpolated with the #{$var} syntax?

Not only would you have consistent interpolation syntax and behavior across your language, but it would allow you to do stuff like this :

// Dynamicly decide whether you use extends or mixins
// ------------------------------------------------------------------
$extend : true;

@if ($extend == true) {
     $define-ruleset : '%';
     $use-ruleset : '@extend %';
} @else {
     $define-ruleset : 'mixin ';
     $use-ruleset : '@include ';
}

#{$define-ruleset}border-none {
     border : 0;
}

#{$define-ruleset}box-default {
     padding : 0;
     margin : 0;
}


// Dynamic media query creation
// ------------------------------------------------------------------
$media-options : ' only screen';
$min : 650;
$max : 1024;

@if ($min != null) {
      $media-options : $media-options + ' and (min-width: #{$min})';
 }

@if ($max != null) {
      $media-options : $media-options + ' and (max-width: #{$max})';
 }

@media#{$media-options} {
       div {
             #{$use-ruleset}border-none;
             #{$use-ruleset}box-default;
       }
}

This is the sort of flexibility that I take for granted when using PHP and that I constantly miss whenever I'm using SCSS.

@chriseppstein
Member

If people prefer interpolation to maps for a specific implementation, shouldn't they be able to so choose that syntax instead of having the language creators decide it for them?

Perhaps, If interpolation provided reasonable usefulness beyond aesthetic preference. We don't add features for aesthetic choice very often it makes the language harder to learn for little to no benefit.

And while I get your point about variable interpolation, i don't see why you don't just allow any part of your SCSS to be interpolated with the #{$var} syntax?

Interpolation is a work around for CSS syntax limitations and is an unsightly necessity. Sass Syntax is under our control, why would make it less readable on purpose?

Not only would you have consistent interpolation syntax and behavior across your language, but it would allow you to do stuff like this

We've explicitly decided to not allow interpolation to change the abstract syntax tree on purpose. you can accomplish this with a mixin that chooses to extend or include.

This is the sort of flexibility that I take for granted when using PHP and that I constantly miss whenever I'm using SCSS.

Given that we do not take any inspiration from the language design of PHP and most programmers view PHP as the quintessential example of bad language design, I'm ok with this. Please keep in mind that you can use PHP to processes your stylesheets, maybe you'd prefer that to Sass.

Sass syntax has not ever been a democratic process. We listen to arguments and decide. I'm sorry if you don't like our decisions. I think we've struck a good balance between power and ease of use so far.

I'd like this issue to remain open for tracking the dynamic mixin feature, let's keep further discussion here to that topic.

@Snugug Snugug referenced this issue Nov 13, 2013
Closed

@end #1013

@Snugug
Contributor
Snugug commented Dec 26, 2013

Having recently written my first code using the new call() functionality of 3.3, I'd like to revisit this as I truly love how dynamic it makes my libraries.

I firmly believe that mixin interpolation is a must-have feature for Sass, so we should work out defining a syntax we're al happy with. So far, the two front runners seem to be interpolation and @include mixin(). Having worked with call() and having come up with @include mixin this morning only to see that @robwierzbowski and I have already mentioned it before, I believe that's the way to go.

To go back to earlier concerns, yes it has the potential to look like a function call, but all mixins run that issue. I, however, don't see that issue. Instead, I see the semantics as read; include a mixin named X with arguments Y and Z which is pretty much identical to how an include statement reads now. It also has the advantage of sharing the syntax meaning anything you'd expect to be able to do with a mixin you can without changing any of your syntax.

Thoughts again I guess @chriseppstein and @nex3?

@jslegers

@chriseppstein :

Due to various improvements in Sass 3.3 (muchos kudos for that!), I've found a far more elegant syntax for my implementation, although I still need interpolation of mixins to get where I want.

This is basically the sort of thing I'd like to achieve...

Input:

// Polyfill for "opacity"
@mixin opacity($value) {
  opacity: $value;
  filter: alpha(opacity=$value * 100);
}

// Polyfill for "transform"
@mixin transform($value) {
  -ms-transform: $value;
  -webkit-transform: $value;
  transform: $value;
}

// This mixin will use a polyfill or custom rule definition when available
@mixin rules($styles) {
  @each $key, $value in $styles {
    if mixin-exists($key) {
      @include #{$key}($value);
    } @else {
      #{$key}: $value;
    }
  }
}

// The user can use standard CSS3 without caring about browser
// support. For browsers that don't support certain features, 
// polyfills will automatically be used when available.
// Polyfills can be modified or deleted at any time without any
// impact on user defined code.
.exampleclass {
  @include rules(
    width : 100%,
    opacity : 0.5,
    transform : rotate(7deg)
  );
}

Output:

.exampleclass {
   width : 100%;
   opacity : 0.5;
   filter: alpha(opacity=50);
   -ms-transform: rotate(7deg);
   -webkit-transform: rotate(7deg);
   transform: rotate(7deg);
}
@jslegers

@chriseppstein :

Additional to easy polyfilling, another use case would be custom CSS properties.

See this example :

Input:

// Custom "grid" property
@mixin grid-cell($values) {
  @extend %clearfix;
  $float : nth($values, 1);
  $width : nth($values, 2);
  float: $float;
  width: $width;
}

// This mixin will use a polyfill or custom rule definition when available
@mixin rules($styles) {
  @each $key, $value in $styles {
    if mixin-exists($key) {
      @include #{$key}($value);
    } @else {
      #{$key}: $value;
    }
  }
}
.exampleclass1 {
  @include rules(
    grid-cell : left 30%,
    color : #000
  );
}
.exampleclass2 {
  @include rules(
    grid-cell : right 40%,
    color : #fff
  );
}

Output:

.exampleclass1, .exampleclass2 {
  display: block;
}

.exampleclass1:before, .exampleclass1:after,
.exampleclass2:before, .exampleclass2:after {
  content: "";
  display: table;
}

.exampleclass1:after, .exampleclass2:after {
  clear: both;
}

.exampleclass1 {
  float: left;
  width: 30%;
  color : #000;
}

.exampleclass2 {
  float: right;
  width: 40%;
  color : #fff;
}
@lunelson

I still think as I did 9 months ago that @include ($foo, $x, $y) is the clearest and simplest of these options, and close to what we see with media queries, e.g. @media (min-width: 20em) FWIW, so not really weird looking. In this approach, the @include directive would either take a keyword, or not. If no keyword is given, the name of the mixin is assumed to be the first argument in the arglist. Thus the two would be equal:

$foo: bar;
@mixin bar(a,b) {...}
.test {
  @include bar(a, b);
  @include ($foo, a, b);
}
@lunelson

However, I find myself realizing again why I still prefer interpolation to any of these "call"-like variants, which is simply that $foo is not equal to @mixin bar, it is only equal to the string `'bar', and with interpolation this fact is always clearly implied. These other approaches might seem more readable/less ugly, but I think logically they are less clear than a string-interpolated statement.

@chriseppstein
Member

@lunelson Ok. I retract "I don't like that this directive now looks like a function call, where all the others look like statements." This is the best option we have.

I support adding

@include ($mixin-name, $arg-list...)

and a corresponding syntax for defining mixins:

@mixin ($mixin-name, <arglist definition>)

@nex3 what say you?

@Snugug
Contributor
Snugug commented Oct 2, 2014

Would arglist allow for named arguments? I think, especially for mixins, that they'd important

On Oct 2, 2014, at 12:35 PM, Chris Eppstein notifications@github.com wrote:

@lunelson Ok. I retract "I don't like that this directive now looks like a function call, where all the others look like statements." This is the best option we have.

I support adding

@include ($mixin-name, $arg-list...)

and a corresponding syntax for defining mixins:

@mixin ($mixin-name, )

@nex3 what say you?


Reply to this email directly or view it on GitHub.

@chriseppstein
Member

@Snugug yes, we support this now if you pass arglist as a map. You can pass positional and keyword args as @include foo($args..., $kw-arg-map...) since 3.3.

@lunelson
lunelson commented Oct 2, 2014

This will be a superpower

@Undistraction

Let me add another use-case. This is slightly convoluted and without explaining the context it might not make a lot of sense, but briefly, I want to handle keywords (like single, half, triple etc) for box-property values. These keywords are then tied into rhythm properties which are wrapped in MQs as needed.

The short of it is that I need to set box-properties through mixins rather than directly through properties.
This means I need to ape all the box properties:

@mixin margin(...){}
@mixin margin-left(...){}
@mixin margin-right(...){}
@mixin margin-bottom(...){}
@mixin margin-top(...){}

@mixin border(...){}
@mixin border-left(...){}
@mixin border-right(...){}
@mixin border-bottom(...){}
@mixin border-top(...){}

@mixin padding(...){}
@mixin padding-left(...){}
@mixin padding-right(...){}
@mixin padding-bottom(...){}
@mixin padding-top(...){}

The content of each mixin is essentially the same other than an $orientation value that is pushed into a function that contains the meat, and could easily be added in a loop. Being able to run through margin border padding and build out interpolated mixins would be really useful here and would remove a lot of duplication.

@lunelson

@chriseppstein it seems to me a use-case for dynamically creating mixins would be passing a @content block from the running mixin to the one that's being created—sort of a 'factory' mixin if you will.
However, this would require that the @content be passed on as SassScript and not be evaluated... only evaluated later, at the runtime of the new @mixin. Thoughts?

@lunelson

P.S. @content complexities related discussion #871

@nex3
Contributor
nex3 commented Oct 17, 2014

The more I think about it, the less I like adding new syntax for this. I'd rather mirror what we do with call and have a built-in mixin that dynamically invokes other mixins (include or maybe just call). The parallel with function calls is a huge benefit, as is not adding new syntax. It also provides readers of the code a very straightforward path to looking up the functionality and figuring out what's going on if they don't understand it.

I'm extremely skeptical about dynamically defining mixins. That level of dynamism can be very difficult to understand, and it will have global performance implications once we start compiling to Ruby, since it will essentially never be possible to statically determine which mixin is being included at the include site.

@Undistraction For that use case, I'd just define a mixin that takes the property name as a parameter.

@thejase
thejase commented Oct 17, 2014

If we went the call route, how does it distinguish between a function and a mixin?

@xzyfer
Contributor
xzyfer commented Oct 17, 2014

I'd rather mirror what we do with call and have a built-in mixin that dynamically invokes other mixins

Big 👍 for this. It'll also make adding this libsass trivial.

@nex3
Contributor
nex3 commented Oct 17, 2014

@thejase If it's a statement that starts with @include, it's a mixin; if it's an expression, it's a function.

@lunelson

So if I understand the syntax would be

@include call(my-mixin, $args...) {...}
@Undistraction

It would so handy to be able to do this:

// Namespace
$ns: 'example';

@function #{$ns}-some-function() {
   ...
}

@mixin #{$ns}-some-mixin {
   ...
}
@reactivestudio

And what's about with
@include call(my-mixin, $args...) {...}

When we can able to use it?

@Snugug
Contributor
Snugug commented Dec 15, 2014

@Undistraction you can currently do call($name, $args...), so dynamic function calls are possible.

@nex3 I like @include call($name, $args...) and agree that mirroring the function syntax would be good.

@Undistraction

@Snugug Calls yes, but not declarations.

@Snugug
Contributor
Snugug commented Dec 15, 2014

@Undistraction Misunderstood your comment, sorry.

@chriseppstein
Member

I don't feel strongly about dynamic mixin definitions. I understand the desire to be similar to call for dynamic functions. But I think @include mixin($mixin-name, $arglist...) reads better.

@nex3
Contributor
nex3 commented Jan 30, 2015

@chriseppstein If you strongly prefer that to @include include() or @include call() I could be persuaded.

@chriseppstein
Member

@nex3 Well my preference was for @include ($mixin-name, $arglist...) but if we have to have a special mixin name then of those three, I feel mixin is by far the most readable.

@danielpeplow

Wow, great thread.

I've stumbled upon this due to wanting to dynamically call a mixin within a loop and I hit the problem you've all faced.

@chriseppstein - 👍 for being so awesome...(schmoozing over)...how close are you guys to getting something like this in? It'd be a life saver right about now :)

@mtpultz
mtpultz commented Mar 29, 2015

+1 @chriseppstein suggestion @include ($mixin-name, $arglist...) reads better.

@gonssal
gonssal commented Apr 3, 2015

Use case for dynamic names.

Let's say I have a site with 10 sections (sec1, sec2, sec3, etc...). I create a mixin to style some elements of every section differently (different colors, backgrounds, etc...). I also create a complex background mixin for every section, named sec1-background, sec2-background, etc... Then, I want to do this:

@mixin section-content($section) {
    #content a {
        color: #{$section}-color;
    }
    #content div.background {
        @include #{$section}-background();
    }
}

Dynamic variable, function and class naming is a feature that every extensible programming language / system has.

@jslegers

Whenever I play around with SCSS, I keep stumbling on this same issue.

Current use case :

// Define placeholder
@mixin define($key, $value) {
    %#{$key} {
        @include rules($value);
    }
}

// Define hover styles
@mixin hover($key, $value) {
    &:hover {
        @include rules($value);
    }
}

// Print rule
@mixin rule($key, $value) {
    #{$key} : $value;
}

// Print rules
@mixin rules($name, $rules) {
    @each $key, $value in $rules {
        @if(str-index($key, '_') == 1) {
             // Here, I need to be able to call ANY mixin, based on the value of $key
            @include #{str-slice($key, 2)}($name, $value);
        } @else if(type-of($value) == "map") {
            @include rules($name + '-' + $key, $value);
        } @else {
            @include rule($key, $value);
        }
    }
}

// Define component
@include rules('button', (
    _define : (
        background : #fff,
        color : #111,
        padding : 0.5em 1em,
        border-width : 1px,
        _hover : (
            background : #eee,
        )
    ),
));
@gkiely
gkiely commented Aug 27, 2015

Has there been an agreement on this?
Mixin interpolation would solve most of my life problems.

@yoannisj
yoannisj commented Sep 1, 2015

I would agree with @chriseppstein: including mixins dynamic doesn't need to be similar to invoking functions dynamically.

Indeed, the @include part is already very explicit about what you are doing and what is going on – when invoking functions doesn't involve such a keyword. I do think it is more intuitive if the syntax looks similar to including mixins regularly, by passing arguments inside of parentheses.

Therefore, the @include mixin($mixin-name, $args..); seems the most readable and intuitive to me. And it actually does look similar to invoking functions dynamically, since you pass the mixin's name as first argument, just like you do with the call function.

So, +1 for @include mixin($mixin-name, $args..);. Looking forward to see this feature in Sass!

@gkiely
gkiely commented Sep 7, 2015

I would love to see this in sass,
@chriseppstein @nex3 When there's an agreement on syntax I'll gladly kick off a modest bounty to help this along.

@nex3
Contributor
nex3 commented Sep 11, 2015

I think we're both reasonably happy with @include mixin().

@Snugug
Contributor
Snugug commented Sep 11, 2015

I wish GitHub had Slack reactions.

👍

@gkiely
gkiely commented Sep 17, 2015

As promised here is a modest bounty to help things along (:
https://www.bountysource.com/issues/395319-mixin-and-function-interpolation

@gkiely
gkiely commented Oct 8, 2015

Well I've done what I can on this, there doesn't seem to be much drive on this as far as I can tell. I'll leave the bounty up for another couple months.
It's in the hands of the sass gods now.

@nex3 nex3 added Planned and removed Under Consideration labels Oct 9, 2015
@nex3
Contributor
nex3 commented Oct 9, 2015

Unfortunately, we have very limited resources. But we're always happy to help out if other people want to contribute :).

@gkiely
gkiely commented Oct 20, 2015

No worries @nex3 I appreciate yours and others work on this, and hopefully it's something that makes it into sass eventually.

@baseten
baseten commented Nov 13, 2015

+1 Would love to see this feature

@georgeevans1995

Hi, Is this feature getting added any time soon? I have recently stumbled upon some annoying circumstances I can't seem to get around due to this issue.

I have a mixin which needs to call another mixin based on its arguments

@mixin keyframes($name, $strength) {
@if $name == bounce {
@include bounce($strength);
}
@elseif $name == bounceIn {
@include bounceIn($strength);
}
@elseif $name == bounceInDown {
@include bounceInDown($strength);
}
@elseif $name == bounceInLeft {
@include bounceInLeft($strength);
}
}

what I would like to have is :
@mixin keyframes($name, $strength) {
@include #{$name}($strength);
}

I currently have a list of 70 of these if/else statements and was hoping to do it with a loop to reduce this very long if statement?

Anybody know another way of doing this or is it not possible until this issue has been resolved?

@ModulaShop

+1 - in whatever syntax

I would prefer:

@include #{$var}: 

Here's some thoughts why:

It is ugly, for sure - but People are used to it now.
Someone familiar with SASS that wants to archive to call a variable-named mixin would try this first, so it might be easiest if it just works on the first try.

I wouldn't care too much about beginners for this usecase - they most likely won't even think about using such a pattern. And if someone is more experienced later he will also expects this to work the same way all the other interpolations in the language do - even if it`s ugly...

Here`s my use-case, i try to internally map bem-style mixin/class names from a basic framework and apply them to maybe-not bem-enabled DOM-Elements or custom selectors for some reason. In the end it should be sort of a factory-mixin that allows to customize something deeper in the framework.

@mixin getRule ($bemClass, $args,$rule: null) {
  @if $rule == null {
    $rule = #{'.'}#{$bemClass};
  }

  #{$rule} {
    @include #{'base-'}#{$bemClass}($args); // <- here ... 
    @content;
  }
}

usage, for example map internal BEM-Classes to non-BEM Style DOM in markup:

@include getRule('tabbedPanel__item-active',$args,'#myPanel .active') {
    color: red;
    font-weight: bold;
}

Result:

#myPanel .active {
   /* all rules from base-tabbedPanel__item-active, with $args applied */
  /* passed definitions and additional mixins */
}

I currently only get what i wand by ommiting @include #{'base-'}#{$rule}($args); from the factory-mixin and instead adding as @include in the @content param and without $args for the mixin:

@include getRule('tabbedPanel__item-active','#myPanel .active') {
    @include: base-tabbedPanel__item-active($args); // <-- want to avoid this line ....
    color: red;
    font-weight: bold;
}

what is not really DRY :(

@chriseppstein
Member

This feature requires that we deprecate all mixins named mixin. I will add this deprecation warning to Sass 3.4.x.

@chriseppstein chriseppstein added a commit that referenced this issue Dec 11, 2015
@chriseppstein chriseppstein Add deprecation warning for #626.
This paves the way to allow us to implement a new syntax for including a
mixin where the mixin being included is specified by a script
expression.
7543c98
@nex3
Contributor
nex3 commented Dec 11, 2015

@chriseppstein If we have good namespacing in Sass 4 (which I hope to), we can define this mixin in some sort of meta namespace and avoid collisions. I'd really like to support that sort of namespacing—it's a big reason I'm okay with adding a bunch of new functions—so I think we should hold off on deprecating existing mixins.

@chriseppstein
Member

@nex3 This isn't a mixin, it's syntax that is ambiguous with the name of a mixin. If Sass was actually exposing mixins, then I would agree that is a proper solution for this case. But this is special syntax for invoking a mixin and that syntax should be global, just like the call method for invoking functions dynamically. Worse, is that we currently allow users to define a function named call which would break Sass semantics. We should deprecate both and not make things complex by allowing important builtins to be swapped around locally with different semantics.

@chriseppstein chriseppstein added a commit that referenced this issue Dec 16, 2015
@chriseppstein chriseppstein Revert "Add deprecation warning for #626."
This reverts commit 7543c98.
bce9509
@chriseppstein
Member

Reverted the deprecation warning in bce9509 so we can release a patch update.

@nex3
Contributor
nex3 commented Jan 8, 2016

I consider adding syntax to be a substantial cost, especially when the syntax is identical to something that's not syntactic (in this case, a function or mixin with a specific name). What's the benefit of making mixin() and call() special syntax as opposed to making them namespaced names?

@chriseppstein
Member

@nex3 it's not an actual mixin that is special, it's a name that is special cased to behave differently. If it were the former, I would agree with you, but it's not.

@nex3
Contributor
nex3 commented Jan 8, 2016

Special cases also come at a cost, and I don't understand what we get in exchange for that cost.

@chriseppstein
Member

users get to call a mixin by passing the name as a string.

@nex3
Contributor
nex3 commented Jan 8, 2016

But we could get that benefit without adding a special case. We could define a built-in mixin in the meta namespace that does exactly that, and then we'd get to avoid the pain of deprecation and special casing.

@jslegers
jslegers commented Jan 8, 2016

@nex3 @chriseppstein

But we could get that benefit without adding a special case. We could define a built-in mixin in the meta namespace that does exactly that, and then we'd get to avoid the pain of deprecation and special casing.

Whenever possible, it's always better to add new features in a way that doesn't break backward compatibility.

@nex3
Contributor
nex3 commented Jan 8, 2016

After talking to Chris elsewhere, we've decided to put the discussion of whether or not to make this a top-level versus a namespaced mixin on hold until the new module system is more solid.

@gkiely
gkiely commented Feb 11, 2016

Hi @nex3 @chriseppstein has there been any update on this feature?

@ArmorDarks

I really hope to see this feature too. Without it It's impossible to do some very basic things.

Functions can be called with call(), and it basically allows to do whatever you want with their names and calls. I'm quite ensured that mixins which are in it's nature very similar to functions should have call method too.

Maybe even like this

.test {
  @call(mixinName, parameters)
}

Nunjucks/Jinja, for example, using something similar for macros.

This also seems to be understandable for most people, since it's similar to JavaScript ways.

@BertmV
BertmV commented Apr 27, 2016

Add my vote to this (for what it is worth).
I am currently trying to do this:

@each $component in $components {
   .component.#{$component} {
      @include component-#{$component}_#{$theme};
   }
}

Only to be stumped by the fact it is impossible,

@fronteed
fronteed commented May 11, 2016 edited

What about an extra function for retrieving variable's value using the dynamic name?

Something like

$module: "bar"
$submodule: "baz"
.class
  property: get-color(#{$module}-#{$submobule}-foo);

=>
.class {
  property: bar-baz-foo;
}

That should be much simpler, since it changes the current runtime specs only inside get-color() function.
get-color() would look into the current selector's scope, than parent's and so on.

Variable interpolation is really needed, since sometimes number of variables is overheaded just because sass doesn't handle this.

@ArmorDarks

@fronteed For such cases we already have maps. During retrieving process from maps you can form path in whatever form you need. Please, take a look at #132 for details.

@cagintopkaya
cagintopkaya commented Dec 15, 2016 edited

this is not good for SASS , modular design or whatever we're trying to achieve.
I was expecting this feature to be exist before I started this approach because of #{} , but sadly it doesn't exist so I feel like I'm in a dead end.

screen shot 2016-12-15 at 18 46 23

@Jahnp Jahnp referenced this issue in OfficeDev/office-ui-fabric-core Dec 20, 2016
Merged

Add support for scoped versioning #877

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