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

Dynamic mixin invocation #626

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

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

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jan 13, 2013

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.

Member

chriseppstein commented Jan 13, 2013

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

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Jan 13, 2013

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@robwierzbowski

robwierzbowski Jan 16, 2013

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.

Contributor

robwierzbowski commented Jan 16, 2013

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

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Feb 11, 2013

Contributor

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

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

This comment has been minimized.

Show comment
Hide comment
@nex3

nex3 Feb 23, 2013

Contributor

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

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

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Feb 23, 2013

Member

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

Member

chriseppstein commented Feb 23, 2013

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

@nex3

This comment has been minimized.

Show comment
Hide comment
@nex3

nex3 Feb 26, 2013

Contributor

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

Contributor

nex3 commented Feb 26, 2013

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

@lunelson

This comment has been minimized.

Show comment
Hide comment
@lunelson

lunelson 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 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

This comment has been minimized.

Show comment
Hide comment
@lunelson

lunelson Mar 11, 2013

What about a mixin-including function? Something like:

include(mixin_name, args...);

?

lunelson commented Mar 11, 2013

What about a mixin-including function? Something like:

include(mixin_name, args...);

?

@Snugug

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Mar 11, 2013

Contributor

@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.

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

This comment has been minimized.

Show comment
Hide comment
@lunelson

lunelson Mar 11, 2013

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

lunelson commented Mar 11, 2013

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

@joshuafcole

This comment has been minimized.

Show comment
Hide comment
@joshuafcole

joshuafcole Mar 23, 2013

@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!

joshuafcole commented Mar 23, 2013

@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

This comment has been minimized.

Show comment
Hide comment
@ghepting

ghepting Apr 22, 2013

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

ghepting commented Apr 22, 2013

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

@nex3

This comment has been minimized.

Show comment
Hide comment
@nex3

nex3 May 10, 2013

Contributor

Low-priority.

Contributor

nex3 commented May 10, 2013

Low-priority.

@chriseppstein

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 24, 2013

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?

Member

chriseppstein commented Jun 24, 2013

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

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Jun 24, 2013

Contributor

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;
}
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

This comment has been minimized.

Show comment
Hide comment
@cimmanon

cimmanon Jun 24, 2013

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.

cimmanon commented Jun 24, 2013

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

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 24, 2013

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.

Member

chriseppstein commented Jun 24, 2013

@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

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Jun 24, 2013

Contributor

@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 {}
    }
  }
}
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

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 24, 2013

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)

Member

chriseppstein commented Jun 24, 2013

@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

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Jun 24, 2013

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 24, 2013

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.

Member

chriseppstein commented Jun 24, 2013

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

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Jun 24, 2013

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 24, 2013

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.

Member

chriseppstein commented Jun 24, 2013

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

This comment has been minimized.

Show comment
Hide comment
@Snugug

Snugug Jun 24, 2013

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@chriseppstein

chriseppstein Jun 24, 2013

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.

Member

chriseppstein commented Jun 24, 2013

@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.

@cagintopkaya

This comment has been minimized.

Show comment
Hide comment
@cagintopkaya

cagintopkaya Dec 15, 2016

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

cagintopkaya commented Dec 15, 2016

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

@almeidap

This comment has been minimized.

Show comment
Hide comment
@almeidap

almeidap Feb 8, 2017

Definitely a critical feature for those who are building CSS frameworks with Sass. Would love to see #2057 reviewed and merged as soon as possible, so that we can unleash the true power of Sass ;)

almeidap commented Feb 8, 2017

Definitely a critical feature for those who are building CSS frameworks with Sass. Would love to see #2057 reviewed and merged as soon as possible, so that we can unleash the true power of Sass ;)

@Superpencil

This comment has been minimized.

Show comment
Hide comment
@Superpencil

Superpencil Aug 27, 2017

This is holding back rtl support for frameworks like bootstrap, and by extension:

  • 9 million Hebrew speakers
  • 60 million Persian speakers
  • 420 million Arab speakers

I thought I'd point this out since none of them are doing so. All half billion of them are probably on vacation.

Superpencil commented Aug 27, 2017

This is holding back rtl support for frameworks like bootstrap, and by extension:

  • 9 million Hebrew speakers
  • 60 million Persian speakers
  • 420 million Arab speakers

I thought I'd point this out since none of them are doing so. All half billion of them are probably on vacation.

@esr360

This comment has been minimized.

Show comment
Hide comment
@esr360

esr360 Sep 5, 2017

I want to echo what @ArmorDarks said:

.test {
  @call(mixinName, parameters)
}

We have this for functions using call(), and it's great, we need the same for mixins! Looking forward to this being implemented.

esr360 commented Sep 5, 2017

I want to echo what @ArmorDarks said:

.test {
  @call(mixinName, parameters)
}

We have this for functions using call(), and it's great, we need the same for mixins! Looking forward to this being implemented.

@julmot

This comment has been minimized.

Show comment
Hide comment
@julmot

julmot Oct 5, 2017

What's the status here? Is it still planned? If so, when?

julmot commented Oct 5, 2017

What's the status here? Is it still planned? If so, when?

@nex3

This comment has been minimized.

Show comment
Hide comment
@nex3

nex3 Oct 7, 2017

Contributor

#2057 partially implements this, but @chriseppstein hasn't had time to work on it recently.

Contributor

nex3 commented Oct 7, 2017

#2057 partially implements this, but @chriseppstein hasn't had time to work on it recently.

@jasongaylord

This comment has been minimized.

Show comment
Hide comment
@jasongaylord

jasongaylord Nov 14, 2017

@nex3 I think @chriseppstein's work would solve this, correct?

@mixin mixin-parent($weight) {
   $adjusted-weight: $weight - 100;

   @include mixin-child;
}

@mixin mixin-child() {
   font-weight: $adjusted-weight;
}

With the current builds, this is throwing: Undefined variable: "$adjusted-weight"

jasongaylord commented Nov 14, 2017

@nex3 I think @chriseppstein's work would solve this, correct?

@mixin mixin-parent($weight) {
   $adjusted-weight: $weight - 100;

   @include mixin-child;
}

@mixin mixin-child() {
   font-weight: $adjusted-weight;
}

With the current builds, this is throwing: Undefined variable: "$adjusted-weight"

@nex3

This comment has been minimized.

Show comment
Hide comment
@nex3

nex3 Nov 16, 2017

Contributor

@jasongaylord No, Chris's pull request doesn't affect variable scoping. In your example, unless $adjusted-weight is set at the top level, it's only going to be visible lexically within mixin-parent()—not within any mixins or functions it calls.

Contributor

nex3 commented Nov 16, 2017

@jasongaylord No, Chris's pull request doesn't affect variable scoping. In your example, unless $adjusted-weight is set at the top level, it's only going to be visible lexically within mixin-parent()—not within any mixins or functions it calls.

@jasongaylord

This comment has been minimized.

Show comment
Hide comment
@jasongaylord

jasongaylord Nov 18, 2017

@nex3 Makes sense. I was trying to leverage a few bootstrap mixins in my own mixin. Guess the way the variables are scoped it won't work. Thanks.

jasongaylord commented Nov 18, 2017

@nex3 Makes sense. I was trying to leverage a few bootstrap mixins in my own mixin. Guess the way the variables are scoped it won't work. Thanks.

@ArmorDarks

This comment has been minimized.

Show comment
Hide comment
@ArmorDarks

ArmorDarks Nov 18, 2017

@jasongaylord You can use !global flag on $adjusted-weight as an escape hatch.

ArmorDarks commented Nov 18, 2017

@jasongaylord You can use !global flag on $adjusted-weight as an escape hatch.

@lunelson

This comment has been minimized.

Show comment
Hide comment
@lunelson

lunelson Nov 18, 2017

@jasongaylord scoping variables from an outer mixin to an inner one—if that's what you're trying to do—is possible but as @ArmorDarks points out you need to use the global scope because that's the only shared reference between mixins. I implement this using the @content directive. It's a hack, although a formal implementation of this feature (passing variables to a @content rule) has also been under discussion for a while.

$current-weight: 1;

@mixin mixin-parent($weight) {
   $previous-weight: $current-weight;
   $current-weight: $weight !global;
   @content;
   $current-weight: $previous-weight !global;
}

@mixin mixin-child() {
   font-weight: $current-weight;
}

.test {
  @include mixin-parent(2) {
    @include mixin-child;
  }
}

.test2 {
  @include mixin-child;
}
.test {
  font-weight: 2;
}

.test2 {
  font-weight: 1;
}

lunelson commented Nov 18, 2017

@jasongaylord scoping variables from an outer mixin to an inner one—if that's what you're trying to do—is possible but as @ArmorDarks points out you need to use the global scope because that's the only shared reference between mixins. I implement this using the @content directive. It's a hack, although a formal implementation of this feature (passing variables to a @content rule) has also been under discussion for a while.

$current-weight: 1;

@mixin mixin-parent($weight) {
   $previous-weight: $current-weight;
   $current-weight: $weight !global;
   @content;
   $current-weight: $previous-weight !global;
}

@mixin mixin-child() {
   font-weight: $current-weight;
}

.test {
  @include mixin-parent(2) {
    @include mixin-child;
  }
}

.test2 {
  @include mixin-child;
}
.test {
  font-weight: 2;
}

.test2 {
  font-weight: 1;
}
@jasongaylord

This comment has been minimized.

Show comment
Hide comment
@jasongaylord

jasongaylord Nov 18, 2017

Thanks everyone. I'll come back to this over the next day or two to see if I can accomplish this.

jasongaylord commented Nov 18, 2017

Thanks everyone. I'll come back to this over the next day or two to see if I can accomplish this.

@ArmorDarks

This comment has been minimized.

Show comment
Hide comment
@ArmorDarks

ArmorDarks Nov 19, 2017

@jasongaylord Yeap, @lunelson described the most efficient pattern for this as of right now.

It is similar to how we have implemented ekzo-temp to share specific variables between scopes of mixins which are later used for wrapping other mixins into breakpoints and passing specific values inside of wrapped mixin. Here is example.

It also used for theming function and mixin. Here is another example.

Unfortunately, there are few downsides to it:

  1. It is not obvious. As you can see from code examples, it is hard to get what are you passing.
  2. It isn't performance efficient solution. Seems like messing with global scope impacts Sass compilation time.

But, right now it is the best we've got.

ArmorDarks commented Nov 19, 2017

@jasongaylord Yeap, @lunelson described the most efficient pattern for this as of right now.

It is similar to how we have implemented ekzo-temp to share specific variables between scopes of mixins which are later used for wrapping other mixins into breakpoints and passing specific values inside of wrapped mixin. Here is example.

It also used for theming function and mixin. Here is another example.

Unfortunately, there are few downsides to it:

  1. It is not obvious. As you can see from code examples, it is hard to get what are you passing.
  2. It isn't performance efficient solution. Seems like messing with global scope impacts Sass compilation time.

But, right now it is the best we've got.

@FistMeNaruto

This comment has been minimized.

Show comment
Hide comment
@FistMeNaruto

FistMeNaruto Dec 10, 2017

I'm not sure if this is the same problem, but I ran into similar issue trying to make breakpoint mixins.
I have a breakpoint map:

$breakpoints: {
  ...
  small: 640px,
  ...
}

And I'm trying to make mixins iterating through it:

@each $screen, $value in $breakpoints {
  @mixin #{$screen} {
    @media (min-width: $value) {
      @content;
    }
  }
}

So I would be able to write media queries just with @include small {...}, but I get invalid mixin name errors.

FistMeNaruto commented Dec 10, 2017

I'm not sure if this is the same problem, but I ran into similar issue trying to make breakpoint mixins.
I have a breakpoint map:

$breakpoints: {
  ...
  small: 640px,
  ...
}

And I'm trying to make mixins iterating through it:

@each $screen, $value in $breakpoints {
  @mixin #{$screen} {
    @media (min-width: $value) {
      @content;
    }
  }
}

So I would be able to write media queries just with @include small {...}, but I get invalid mixin name errors.

@ArmorDarks

This comment has been minimized.

Show comment
Hide comment
@ArmorDarks

ArmorDarks Dec 10, 2017

@FistMeNaruto you can't declare mixin in a loop that way, and I do not think this will be ever possible, even if the proposal of current mixin will be implemented.

Instead, think the other (and I'd say even right) way:

$breakpoints: {
  ...
  small: 640px,
  ...
}

@mixin breakpoint ($screen) {
  $value: map-get($breakpoints, $screen)
  
  @media (min-width: $value) {
    @content;
  }
}

@include breakpoint(small) {
  // your content
}

Also, see Kotsu breakpoint().

ArmorDarks commented Dec 10, 2017

@FistMeNaruto you can't declare mixin in a loop that way, and I do not think this will be ever possible, even if the proposal of current mixin will be implemented.

Instead, think the other (and I'd say even right) way:

$breakpoints: {
  ...
  small: 640px,
  ...
}

@mixin breakpoint ($screen) {
  $value: map-get($breakpoints, $screen)
  
  @media (min-width: $value) {
    @content;
  }
}

@include breakpoint(small) {
  // your content
}

Also, see Kotsu breakpoint().

@sass sass deleted a comment from Unillu Jan 23, 2018

@C-Bouthoorn

This comment has been minimized.

Show comment
Hide comment
@C-Bouthoorn

C-Bouthoorn Apr 2, 2018

I'd like to bump this.

In my case, I would like to do something like this:

@mixin severity($mixin) {
	@include #{$mixin}('debug', $debug);
	@include #{$mixin}('info', $info);
	@include #{$mixin}('success', $success);
	@include #{$mixin}('warning', $warning);
	@include #{$mixin}('error', $error);
}

@mixin severity-button($type, $color) {
	button.#{$type} {
		background-color: $color;

		&:hover {
			background-color: darken($color, 10%);
		}

		&:active {
			background-color: darken($color, 15%);
		}
	}
}
// ...


// @include severity('severity-button');
@include severity(severity-button);
// ...

but this gives expected identifier, was "#{$mixin}('debug', "

Especially when you're using many mixins which accept different severities, this saves quite a bit of copy pasting.

Compare:

@include severity-button('debug', $debug);
@include severity-button('info', $info);
@include severity-button('success', $success);
@include severity-button('warning', $warning);
@include severity-button('error', $error);

instead of

// @include severity('severity-button');
@include severity(severity-button);

For one mixin, this is okay, but when you're using quite a few of them, it starts to get annoying.

@mixin severity($mixin) {
	@include call($mixin, 'debug', $debug);
	@include call($mixin, 'info', $info);
	@include call($mixin, 'success', $success);
	@include call($mixin, 'warning', $warning);
	@include call($mixin, 'error', $error);
}

didn't seem to work either.

It would be nice if there existed some way to do this.

C-Bouthoorn commented Apr 2, 2018

I'd like to bump this.

In my case, I would like to do something like this:

@mixin severity($mixin) {
	@include #{$mixin}('debug', $debug);
	@include #{$mixin}('info', $info);
	@include #{$mixin}('success', $success);
	@include #{$mixin}('warning', $warning);
	@include #{$mixin}('error', $error);
}

@mixin severity-button($type, $color) {
	button.#{$type} {
		background-color: $color;

		&:hover {
			background-color: darken($color, 10%);
		}

		&:active {
			background-color: darken($color, 15%);
		}
	}
}
// ...


// @include severity('severity-button');
@include severity(severity-button);
// ...

but this gives expected identifier, was "#{$mixin}('debug', "

Especially when you're using many mixins which accept different severities, this saves quite a bit of copy pasting.

Compare:

@include severity-button('debug', $debug);
@include severity-button('info', $info);
@include severity-button('success', $success);
@include severity-button('warning', $warning);
@include severity-button('error', $error);

instead of

// @include severity('severity-button');
@include severity(severity-button);

For one mixin, this is okay, but when you're using quite a few of them, it starts to get annoying.

@mixin severity($mixin) {
	@include call($mixin, 'debug', $debug);
	@include call($mixin, 'info', $info);
	@include call($mixin, 'success', $success);
	@include call($mixin, 'warning', $warning);
	@include call($mixin, 'error', $error);
}

didn't seem to work either.

It would be nice if there existed some way to do this.

@ArmorDarks

This comment has been minimized.

Show comment
Hide comment
@ArmorDarks

ArmorDarks Apr 2, 2018

@C-Bouthoorn As a most straightforward another approach:

@mixin severity($mixin) {
  @if ($mixin == 'severity-button') {
    @include severity-button('debug', $debug);
    @include severity-button('info', $info);
    @include severity-button('success', $success);
    @include severity-button('warning', $warning);
    @include severity-button('error', $error);
  } @else if ($mixin == 'another-mixin') {
    @include another-mixin('debug', $debug);
    @include another-mixin('info', $info);
    @include another-mixin('success', $success);
    @include another-mixin('warning', $warning);
    @include another-mixin('error', $error);
    } @else if (...) {
      // ...
    }
}

However, that is very likely that you need to review why are you doing so. In most cases need to call mixin (function) dynamically is a sign of compromised architecture. For instance, take into account, that in your case severity mixin has undeterminated API and it is hard to tell what $mixin value could be — are there 2, 10, 300 valid values?

If you need to reuse some mixins in such way, I'd compose theme within specific standalone mixins instead of providing obscure API with one very generic mixin.

For instance:

@mixin severity() {
  @include severity-button('debug', $debug);
  @include severity-button('info', $info);
  @include severity-button('success', $success);
  @include severity-button('warning', $warning);
  @include severity-button('error', $error);
}

@mixin another() {
  @include another-mixin('debug', $debug);
  @include another-mixin('info', $info);
  @include another-mixin('success', $success);
  @include another-mixin('warning', $warning);
  @include another-mixin('error', $error);
}

@include severity();
@include another();

Finally, as a good practical rule, when something doesn't work out on that level, search for an issue on level upper. In this case, I'd revisit severity-button mixin. There is no need to keep top-selector with button.#{$type} within that mixin. It will automatically eliminate the need to use $type argument and thus the need to call mixins dynamically at all, leaving us with much simpler and more obvious code:

@mixin severity-button($color) {
  background-color: $color;

  &:hover {
    background-color: darken($color, 10%);
  }

  &:active {
    background-color: darken($color, 15%);
  }
}

button.test {
  @include severity-button(#fff);
}

And now if you still want to have same generic mixin, you can do now this:

@mixin severity() {
  button.debug { @include severity-button($debug); }
  button.info { @include severity-button($info); }
  button.success { @include severity-button($success); }
  button.warning { @include severity-button($warning); }
  button.error { @include severity-button($error); }
}

// or even shorter

@mixin severity() {
  button {
      &.debug { @include severity-button($debug); }
      &.info { @include severity-button($info); }
      &.success { @include severity-button($success); }
      &.warning { @include severity-button($warning); }
      &.error { @include severity-button($error); }
  }
}

ArmorDarks commented Apr 2, 2018

@C-Bouthoorn As a most straightforward another approach:

@mixin severity($mixin) {
  @if ($mixin == 'severity-button') {
    @include severity-button('debug', $debug);
    @include severity-button('info', $info);
    @include severity-button('success', $success);
    @include severity-button('warning', $warning);
    @include severity-button('error', $error);
  } @else if ($mixin == 'another-mixin') {
    @include another-mixin('debug', $debug);
    @include another-mixin('info', $info);
    @include another-mixin('success', $success);
    @include another-mixin('warning', $warning);
    @include another-mixin('error', $error);
    } @else if (...) {
      // ...
    }
}

However, that is very likely that you need to review why are you doing so. In most cases need to call mixin (function) dynamically is a sign of compromised architecture. For instance, take into account, that in your case severity mixin has undeterminated API and it is hard to tell what $mixin value could be — are there 2, 10, 300 valid values?

If you need to reuse some mixins in such way, I'd compose theme within specific standalone mixins instead of providing obscure API with one very generic mixin.

For instance:

@mixin severity() {
  @include severity-button('debug', $debug);
  @include severity-button('info', $info);
  @include severity-button('success', $success);
  @include severity-button('warning', $warning);
  @include severity-button('error', $error);
}

@mixin another() {
  @include another-mixin('debug', $debug);
  @include another-mixin('info', $info);
  @include another-mixin('success', $success);
  @include another-mixin('warning', $warning);
  @include another-mixin('error', $error);
}

@include severity();
@include another();

Finally, as a good practical rule, when something doesn't work out on that level, search for an issue on level upper. In this case, I'd revisit severity-button mixin. There is no need to keep top-selector with button.#{$type} within that mixin. It will automatically eliminate the need to use $type argument and thus the need to call mixins dynamically at all, leaving us with much simpler and more obvious code:

@mixin severity-button($color) {
  background-color: $color;

  &:hover {
    background-color: darken($color, 10%);
  }

  &:active {
    background-color: darken($color, 15%);
  }
}

button.test {
  @include severity-button(#fff);
}

And now if you still want to have same generic mixin, you can do now this:

@mixin severity() {
  button.debug { @include severity-button($debug); }
  button.info { @include severity-button($info); }
  button.success { @include severity-button($success); }
  button.warning { @include severity-button($warning); }
  button.error { @include severity-button($error); }
}

// or even shorter

@mixin severity() {
  button {
      &.debug { @include severity-button($debug); }
      &.info { @include severity-button($info); }
      &.success { @include severity-button($success); }
      &.warning { @include severity-button($warning); }
      &.error { @include severity-button($error); }
  }
}

@nex3 nex3 changed the title from Mixin and Function Interpolation to Dynamic mixin invocation Apr 5, 2018

@OskarLebuda

This comment has been minimized.

Show comment
Hide comment
@OskarLebuda

OskarLebuda May 29, 2018

@nex3, @chriseppstein what's the status of this issues ? Is this in progress or has been abandoned?

OskarLebuda commented May 29, 2018

@nex3, @chriseppstein what's the status of this issues ? Is this in progress or has been abandoned?

@nex3

This comment has been minimized.

Show comment
Hide comment
@nex3

nex3 May 30, 2018

Contributor

As the labels indicate, this is planned. However, I have a lot on my plate right now and this isn't the highest priority. If anyone wants to work on a patch for Dart Sass to add this behavior, that would be wonderful.

Contributor

nex3 commented May 30, 2018

As the labels indicate, this is planned. However, I have a lot on my plate right now and this isn't the highest priority. If anyone wants to work on a patch for Dart Sass to add this behavior, that would be wonderful.

nex3 added a commit to sass/language that referenced this issue Sep 24, 2018

[Module System] Make new functions return maps
Also remove module-mixins() pending sass/sass#626.

Closes #12

nex3 added a commit to sass/language that referenced this issue Sep 25, 2018

[Module System] Make new functions return maps
Also remove module-mixins() pending sass/sass#626.

Closes #12

nex3 added a commit to sass/language that referenced this issue Sep 25, 2018

[Module System] Make new functions return maps
Also remove module-mixins() pending sass/sass#626.

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