Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Dynamic mixin invocation #626

Closed
6 of 7 tasks
Snugug opened this issue Jan 13, 2013 · 178 comments · Fixed by #3711
Closed
6 of 7 tasks

Dynamic mixin invocation #626

Snugug opened this issue Jan 13, 2013 · 178 comments · Fixed by #3711
Labels
enhancement New feature or request planned We would like to add this feature at some point requires deprecation Blocked on a deprecation cycle

Comments

@Snugug
Copy link

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
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
Author

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
Copy link
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
Copy link

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

@nex3
Copy link
Contributor

nex3 commented Feb 26, 2013

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

@lunelson
Copy link

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
Copy link

What about a mixin-including function? Something like:

include(mixin_name, args...);

?

@Snugug
Copy link
Author

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
Copy link

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

@joshuafcole
Copy link

@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
Copy link

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

@nex3
Copy link
Contributor

nex3 commented May 10, 2013

Low-priority.

@chriseppstein
Copy link

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
Copy link
Author

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
Copy link

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
Copy link

@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
Copy link
Author

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
Copy link

@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
Copy link
Author

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
Copy link

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
Copy link
Author

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
Copy link

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
Copy link
Author

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
Copy link

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

@K4rakara
Copy link

K4rakara commented Nov 8, 2022

I'd be happy to write up a formal proposal for the feature. I can get started on it tomorrow, unless there's something else blocking progress. For what its worth, I've made a working implementation of this in dart sass, with the API surface I described in #3439.

Also, apologies for opening a duplicate, I tried a few different search terms, and didn't come up with anything.

@nex3
Copy link
Contributor

nex3 commented Nov 9, 2022

Feel free to start working on a proposal—there are instructions in CONTRIBUTING.md.

@nex3
Copy link
Contributor

nex3 commented Jul 28, 2023

The proposal has landed! I'll give it a month for public comment and then we can start working on implementation.

@zaydek
Copy link

zaydek commented Jul 28, 2023

Wow this is really cool. Definitely solves a problem and removes the need to do some very meta stuff like meta.call.

@nex3
Copy link
Contributor

nex3 commented Sep 28, 2023

@stof The next steps here are implementing the embedded host wrapper and updating the website documentation. I can handle the latter if you do the former.

@connorskees
Copy link
Contributor

I assume you intended to tag me(?) Happy to take a look at updating the embedded host wrapper if that's the case.

(tangentially: the linked spec-test PR is sass/sass-spec#1942, but it should be sass/sass-spec#1933)

@nex3
Copy link
Contributor

nex3 commented Sep 28, 2023

Haha, yes, sorry about that!

@MichaelPote
Copy link

MichaelPote commented Jul 18, 2024

This issue has been closed and the new feature seems to have been merged in, but I cant see any official documentation or implementation details for the proposed solution, what is the status of this?

EDIT: I found out, for anyone looking to do dynamic Mixin includes, the following works:

@use "sass:meta";

$variable: "a";
@include meta.apply(meta.get-mixin("test-#{$variable}")); //Will include a mixin called "test-a".

More info here: https://sass-lang.com/documentation/modules/meta/#mixins

@nex3
Copy link
Contributor

nex3 commented Jul 18, 2024

This is also documented in https://sass-lang.com/documentation/values/mixins/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request planned We would like to add this feature at some point requires deprecation Blocked on a deprecation cycle
Projects
None yet
Development

Successfully merging a pull request may close this issue.