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

Content functions. #1582

Open
chriseppstein opened this Issue Jan 5, 2015 · 36 comments

Comments

Projects
None yet
@chriseppstein
Member

chriseppstein commented Jan 5, 2015

I'd like to introduce a function content() that returns a Sass data structure that represents the css specified as mixin content that is normally placed by the @content directive. The exact form of this data is something that would need to be well specified, I will work on that if people are in agreement that this is a good idea.

I also think Sass should provide a mixin that effectively translates this data back into CSS. E.g. @include expand-content(content()); would be identical to @content; when used inside a mixin. Sass doesn't currently expose any mixins automatically so we probably just want to make a special import location so that Sass mixins can be manually imported if they are needed. @import 'stdlib' or something?

Here's the rationale. I see a lot of users clamoring to use SassScript to manipulate their CSS. To do this, they end up defining their CSS as maps instead of as CSS. I'd like things that are stylesheets to be specified in CSS syntax, not in some it's a map and it's almost css syntax just so that it can be manipulated.

Another use case is that, https://github.com/oddbird/true needs a way to verify that the output of a mixin is correct without relying on string-based output comparison tests. By providing this capability, the css output of a mixin can be tested using SassScript.

In the past, I've proposed special directives to accomplish this, but I don't think that's necessary.

@chriseppstein chriseppstein added this to the 4.0 milestone Jan 5, 2015

@StefanoRausch

This comment has been minimized.

StefanoRausch commented Jan 6, 2015

Indeed that would be great to have!

Regarding the testing of Sass / mixins output, there is currently a viable way via SassUnit. It works really well.

@jakob-e

This comment has been minimized.

jakob-e commented Jan 8, 2015

👍 +1

@jakob-e

This comment has been minimized.

jakob-e commented Jan 8, 2015

This may be a bit off topic but when talking content manipulation – how about using @content as a kind of getter-setter (if possible working like "map-merge" to reduce CSS output). I know this example has a lot of unresolved – like nested selector output, shorthands and fallbacks (font:1em serif; font-size:1rem; font-size: 16px;). It was not my intention to spam :-)

@mixin a(){
  left: 0;
  right: 100px;
}

@mixin b(){
  left: 100px;
  top: 100px;
}

@mixin c(){
  // Passed @content will be present before any modifications 

  // Set @content (merge)
  // Note! 
  // ; is a separator 
  // will require content to be "@at-root" (not inside selectors)
  @content( 
    @include a();
    @include b();
  );

  // Looking up property
  @if content(top) {
    // Merge properties 
    @content( bottom: 100px; ); 
    // Remove properties
    @content( width: null; height: null; ); 
  }

  // Print content 
  @content;
}


.class {
  @include c(){
    position: absolute;
    width: 100%;
    height: 100%;
  };
}


// CSS
.class {
  position: absolute;  // from .class
  // width : deleted;
  // height: deleted;
  left: 100px;   // from b (order from a)
  right: 100px;  // from a
  top: 100px;    // from b
  bottom: 100px; // from c
}
@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Jan 8, 2015

In general, I like the idea of being able to do:

@mixin some-mixin() {
  $some-content: content();
  @content $some-content;
}

I think this could be optimized better than using a pure mixin and it's clear that it's based on existing patterns. As long as the variable passed to @content is properly structured, it could work with pure data that is assembled by hand -- it wouldn't have to come from the content() function.

Regarding the other suggestions here, I think you've missed just how complex the data that is passed as mixin content can be. It can contain selectors, media queries (and other css at-rules), etc. It needn't just be properties. In either respect, I think the fact that the content is returned as a SassScript data structure means that we can use the existing map, string, and list manipulation functions to operate on it.

@lolmaus

This comment has been minimized.

lolmaus commented Jan 8, 2015

Does this suggestion also open a possibility of emitting arbitrary content via @content without prior capturing the content via content()?

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Jan 8, 2015

@lolmaus yes. I don't see why it shouldn't.

@jakob-e

This comment has been minimized.

jakob-e commented Jan 8, 2015

@chriseppstein I know – it was just a bit of crazy thinking :)

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Jan 8, 2015

Crazy thinking is how you get good ideas. Also bad ones. The skill is in how to discern which is which.

@nex3 nex3 removed this from the 4.0 milestone Jan 9, 2015

@nex3

This comment has been minimized.

Contributor

nex3 commented Jan 9, 2015

I'm removing Milestone 4.0 because I don't want to block 4.0 on any major features other than the import overhaul.

This is a piece of functionality that would certainly solve a lot of use cases, but it risks introducing a colossal amount of complexity in the process. I'm on board with the general idea, but I want to be damn sure we have a nice, clean, comprehensible, and minimally-complex data format before we add it to the language.

Here's one example of a tricky principle we'll need to figure out: what state of resolution is the captured data structure in? In particular, how much nesting does it preserve? What happens when the user's code includes &? What about a deeply-nested &? What about @at-root? What about @if?

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Jan 9, 2015

@nex3 I agree with your comments about the data format and look forward to discussing what we think that should look like. I have a few ideas that I can propose as a starting point for discussion but nothing I am wedded to.

Regarding the state of the captured data, the only thing that makes sense to me is that it is fully resolved CSS from the lexical scope of the include. At the point that this content is placed into the stylesheet again, they can use Sass constructs like & and @at-root to manipulate the new context of the content. Reproducing Sass code like you mention is exactly what mixins are good for and if we ever needed such a construct it would probably be some sort of lambda equivalent for mixins.

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Jan 30, 2015

I recently sat down with @eoneill to review this design and make sure that it would support a better, more author friendly way of expressing a CSS component system like https://github.com/linkedin/archetype.

We identified a few more aspects of this feature that are needed to properly round out this concept and completely avoid the "data structures as alternative to CSS" issue.

The main issue with the proposal here is that it makes late-binding of some kinds of values more complicated. To fix this, my idea is that there should be a function that invokes a mixin in a given selector scope (defaulting to &) and returns the resulting CSS as a data structure like content() would. For this, I think a function like content-from-mixin($mixin-name, $selector-context: &, $arglist...) would suffice. This would allow static CSS and mixins to both work together to produce components. We should also make sure to implement #626 for dynamic mixin definitions and includes.

We also talked about how the css data would be structured.

CSS Fragment:

.foo, .bar {
  color: red;
  color: rgba(255, 0, 0, 0.5);
}
/* This is a comment about the media block that follows. */
@media (max-device-width: 400px) {
  /* A comment inside the media block. */
  aside#sidebar { width: 100%; }
}

As data:

$css-fragment: (
(ruleset: (selector: ((".foo",), (".bar",)),
           properties: (
             (color: red),
             (color: rgba(255, 0, 0, 0.5)),
           )),
(comment: " This is a comment about the media block that follows. "),
(at-rule: (name: "media",
           value: (max-device-width: 400px)
           contents: (
                       (comment: " A comment inside the media block. "),
                       (ruleset: (selector: (("aside#sidebar",),),
                                  properties: ((width: 100%),)))
                     )
          )))
);

For constructing this data structure, convenience functions can be provided:

$css-fragment: (
 ruleset(".foo, .bar", ((color: red), (color: rgba(255, 0, 0, 0.5)))),
 comment(" This is a comment about the media block that follows. "),
 at-rule("media", (max-device-width: 400px),
         comment(" A comment inside the media block. "),
         ruleset("aside#sidebar", (width: 100%)))
);

Manipulating this structure in SassScript is tedious at best due to the immutability of maps. I'm not sure what to do about that. Maybe provide some way of walking the "AST" by passing a function that can perform specific mutations leaving all the side-effect management to the caller?

@nex3

This comment has been minimized.

Contributor

nex3 commented Jan 30, 2015

Can you go into more detail about the late-binding issue and how content-from-mixin() fixes it?

Also, how are you thinking at-rule values will be parsed? I'd expect them all to be plain strings, but you seem to have the media query represented as a map there.

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Jan 30, 2015

So imagine you have a function current-locale() that let's you vary behavior based on what locale your css is targeting at that point in the stylesheet. If you check this function when a CSS fragment is captured (at definition time) then the value of current-locale() won't be correct. Mixins handle this problem very nicely, but the issue is that you then need to get the output of the mixin as data so that it can be merged with other more static structures. With dynamic mixin invocation, one could, with the help of some global functions, get access to the mixin's output as data, but this would be much more clunky than a function that just does it.

@mixin capture-mixin-output() {
  $last-captured-output: content() !global;
}
@mixin capture-mixin($mixin-name, $arglist...) {
  @include capture-mixin-output() { @include call($mixin-name, $arglist...); }
}

// ...

#some-context {
  @include capture-mixin(foo);
}
$foo-content: $last-captured-output;

And this approach would make it hard to access this mixin content from within pure functions since @include is disallowed with @function but since this operation is side-effect free (except for maybe affecting some global variables) there's no reason to make it so hard to use from within a function.

@nex3

This comment has been minimized.

Contributor

nex3 commented Feb 7, 2015

Can you give me an example of the use of content-from-mixin() with your current-locale() example?

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Feb 17, 2015

@nex3

$locale: en_US;
@function current-locale() {
  @return $locale;
}

@mixin with-locale($temp-locale) {
  $original-locale: $locale;
  $locale: $temp-locale !global;
  @content;
  $locale: $original-locale !global;
}

@mixin font-family() {
  @if current-locale() == "zh_CN" {
    font-family: "Microsoft JhengHei", sans-serif;
  }
  @else if current-locale() == "ar_AE" {
    // ...
  }
  @else {
    font-family: Helvetica, sans-serif;
  }
}

@mixin main-typography {
  p { @include font-family; }
}

@include register-component(main-typography);

// ...

@include component(main-typography);

[lang^="zh"] {
  @include with-locale(zh_CN) {
    @include component(main-typography);
  }
}

Obviously, in this simplified example there's no need for the component indirection, but you can imagine that the component system allows precision tweaks to registered components in different contexts or themes that makes it necessary to manipulate the evaluation time result of the mixin as data before outputting it.

@nex3

This comment has been minimized.

Contributor

nex3 commented Feb 20, 2015

I'm sorry, I'm still puzzled :p. Your example makes sense, but I don't see why content-from-mixin() is needed; if main-typography() is dynamically invoked when component() is called, won't it automatically see the correct dynamically-scoped locale without any extra help?

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Mar 3, 2015

@nex3, yes, but we need to intercept the results of running that mixin to merge it with other information in our theming/component system. We need the results of the mixin as data.

@eoneill

This comment has been minimized.

eoneill commented Mar 4, 2015

Here's a slightly more complex example that might demonstrate the idea @chriseppstein and I have tossed around...

// allows us to keep a store of component definitions
$component-store: () !default;
@mixin register-component($id, $def) {
  $component-store: map-merge($component-store, (
    #{$id}: $def
  )) !global;
}

// allows us to output the styles associated with a component definition (given a set of modifiers)
@mixin component($id, $modifiers: null) {
  $cmpt: map-get($component-store, $id);
  // IMPORTANT: we need to compute the definition styles given all the modifiers passed in
  // this will require us to manipulate the data before we output it
  $styles: get-component-styles($cmpt, $modifiers);
  @content $styles;
}

// given the component definition and the modifiers, merge the rules together (or some other significantly complex operation)
// note that this naturally feels like it'd be a function, and not a mixin
@function get-component-styles($cmpt, $modifiers) {
  $styles: content-from-mixin(map-get($cmpt, default));
  @each $modifier in $modifiers {
    // assume `merge-styles` does some complex operation
    $styles: merge-styles($styles, content-from-mixin(map-get($cmpt, $modifier)));
  }
  @return $styles;
}

// IMPORTANT: because we have dynamic values within this mixin, we have to invoke it each time, and can't compute the styles up-front
$button-color: black !default;
@mixin button {
  background: $button-color;
  color: invert($button-color);
  font-size: 100%;
  margin: 2px 5px;
}

@mixin button-small {
  font-size: 80%;
  margin: 1px 3px;
}

@include register-component(button, (
  default:  button,
  small:    button-small,
  // etc...
));

.btn {
  @include component(button);
  &.btn--small {
    @include component(button, small);
  }
  &.btn--large {
    @include component(button, large);
  }
  // etc
}

To be fair, the above example can be achieved purely using the capture-mixin-output mixin mentioned above, but it means we're now limited to only using the result within a mixin context. That is, we couldn't use get-component-styles as a function to do other data manipulation in another function call.

@nex3

This comment has been minimized.

Contributor

nex3 commented Mar 6, 2015

Ooooookay, I get it now. Yes, I agree that would be a useful function to have.

@helarqjsc

This comment has been minimized.

helarqjsc commented Nov 27, 2015

Are there any plans on implementing it or it was abandoned?
I've been trying to implement a mixin that will allow you to use media queries in any place and have them combined in the end of the resulting .CSS file. It works, but the syntax is not exactly pretty: https://github.com/helarqjsc/SASS-mixin-joined-media-queries/blob/master/example.scss
Being able to put @content into a variable would really help me here.

@chriseppstein

This comment has been minimized.

Member

chriseppstein commented Dec 3, 2015

@helarqjsc Things move slowly here in Sasslandia. We close issues when we decide not to do them. Right now, what we need is a design for how to represent the css abstract syntax tree as Sass values. That design needs to be very flexible, and also very user friendly.

Then what we need is an API that is efficient at mutating that AST -- all of the current Sass APIs really are quite bad at mutating deeply nested data structures. This API might be AST specific or it might just be optimizations for existing data types. It's not clear what the best way to handle this is. I can imagine a few different APIs and approaches to capturing the AST.

Then we can start writing code :) This is probably too big of a feature to make it into Sass 4.0 (a new module system is our top "big feature" priority right now) so it's hard to imagine this landing in the next 6 months.

@ArmorDarks

This comment has been minimized.

ArmorDarks commented Feb 9, 2017

To be honest, CSS fragments from #1582 (comment) blowed my mind. In a bad way. () instead of {} and [] for maps and arrays notation didn't help it too...

Those days there are quite a lot of popular JS libraries (mostly around React), which trying to bring CSS to the object world, so maybe it would be good idea to check them and take some ideas from them.

Constructions like (ruleset: (selector: ((".foo",), (".bar",)), as very exessive and hardly friendly for endusers. Logical way to solve it would be to go Radium way and input selectors and derictives as you would normaly do in CSS or Jquery, like ".foo.bar" or "@media (...)". I understand that parsing those strings willl have performance impact, but can't help thinking that this seems to be the only legit way from consumers point of view.

After all, those objects-like data of Radium and other similar libs feels so natural, because they are very close to original CSS "objects". Well, in fact they are indeed objects, but with different syntax...

@ArmorDarks

This comment has been minimized.

ArmorDarks commented Mar 2, 2017

Just for the information, I posted some related ideas in #2252

@jstoller

This comment has been minimized.

jstoller commented Jul 19, 2017

For what it's worth, I'd be happy to get @content as a string, as written, no compilation necessary. I'd like to do something like this:

@mixin ext() {
  $id: generate-placeholder-id(content());
  @include dynamic-extend($id) {
    @content;
  }
}

Where generate-placeholder-id(content()) generates a unique string based on the contents of @content. So, something like this:

.box1 {
  @include ext() {
    float: left;
    clear: left;
  }
}

.box2 {
  @include ext() {
    float: right;
    clear: right;
  }
}

.box3 {
  @include ext() {
    float: left;
    clear: left;
  }
}

is converted into this:

%ubh8jnee {
  float: left;
  clear: left;
}

.box1 {
  @extend ubh8jnee;
}

%abk4rtdt {
  float: right;
  clear: right;
}

.box2 {
  @extend abk4rtdt;
}

.box3 {
  @extend ubh8jnee;
}

To do this today I need to define my CSS as maps instead of as CSS, as @chriseppstein pointed out in the original issue, which is annoying to manage.

I have no idea what the implications of this would be, but my needs would be met by allowing #{@content} to be used as a function argument, like so...

@mixin ext() {
  $id: generate-placeholder-id(#{@content});
  @include dynamic-extend($id) {
    @content;
  }
}
@DonGissel

This comment has been minimized.

DonGissel commented Aug 2, 2017

I would very much like to be able to extract the mixin-content as a string of sorts, and then insert it at a later time for "final parsing". My current use case is wanting to group my media queries together – I'm using a mixin for inserting media queries, like @include mq(sm) { color: red; } which works just fine, but if I do that a million times in my code, I'll end up with a million media queries. If my mq() mixin could store the content temporarily in a map indexed by the chosen breakpoint, append to that map whenever a similar media query is run, and then flush the whole thing at the very end of my "main" file, I would only have a handful of media queries, which potentially can save me quite a bit of bytes.

I know there are various plugins in NPM to achieve this particular scenario, but seen from my ignorant why-doesn't-SASS-contain-this-feature-standpoint, I see no reason why SASS should not support something like this. I do know that actually implementing it would be another thing entirely, but hey – a man can dream. ;-)

@ArmorDarks

This comment has been minimized.

ArmorDarks commented Aug 4, 2017

@DonGissel good point, I was thinking about exactly same thing some time ago.

Btw, it needed not only to save few bytes. According to latests Google Lighthouse requirements, it is recommended to use media query or <link rel='stylesheet> to ensure, that certain breakpoints loaded only for certain browsers. In some cases it will drastically reduce size of stylesheets for specific devices.

As of right now, there is no way of doing that except using some PostCSS plugin to extract all media queries later into standalone files. From other side, may be it is intended way of doing things...

@cyraid

This comment was marked as spam.

cyraid commented Oct 3, 2018

Any update on this?

@cyraid

This comment has been minimized.

cyraid commented Oct 13, 2018

Why was my comment marked as spam? I was only asking for an update. It's been 3 (almost 4) years since the original issue was created. There are people eagerly awaiting a response, or any update if we can be looking forward to this or if it's going to be rejected.

What are the blockers on this? Can we spark up the debate again instead of shoving it under the rug or is this just not that big of a priority? What's the plan?

@nex3

This comment has been minimized.

Contributor

nex3 commented Oct 15, 2018

@cyraid The appropriate way to indicate interest in an issue is to 👍 it. The appropriate way to hear about updates as they come is to subscribe. Posting comments like "+1" or "Any update?" just spams project maintainers and other users who are subscribed to the issue. The Sass team has limited resources, and not every issue is going to be addressed as quickly as you'd like.

@cyraid

This comment has been minimized.

cyraid commented Oct 15, 2018

How hard is it to say "still working on it guys" or "we haven't started on this because other issues are more important".. then maybe a voting system could be used to see what is more important.

Besides, it's been 4 years since the issue was created, and a year since any communication on the topic. It looks like abandonment to some, so 'bumping' it after that long for a status update I didn't think would be so terrible. Would someone really reply after a year after seeing a thumbs up? Don't think so.

@nex3

This comment has been minimized.

Contributor

nex3 commented Oct 15, 2018

How hard is it to say "still working on it guys" or "we haven't started on this because other issues are more important".. then maybe a voting system could be used to see what is more important.

Sass language design isn't a democracy. We do consider user demand (based on number of 👍s) when prioritizing issues, but we also consider many other factors. Again, we have very limited resources, and responding individually to every "Any update?" comment takes up those resources.

If someone is particularly interested in making a feature happen, they're encouraged to spend their own resources on helping to make that happen. But you don't get to insist that we allocate our resources on the features you want, or on responding to your comments in the way you want.

Besides, it's been 4 years since the issue was created, and a year since any communication on the topic. It looks like abandonment to some, so 'bumping' it after that long for a status update I didn't think would be so terrible. Would someone really reply after a year after seeing a thumbs up? Don't think so.

That's how most projects work. We use the issue tracker to track features we'd like to get to someday, which means that they can stay open with relatively little change for a long time.

@cyraid

This comment has been minimized.

cyraid commented Oct 15, 2018

Yes but, say I want to add the addition myself, and it does not conform to the team's design specifications, and I just wasted a ton of time because everyone would have wanted to discuss how it works first (usually how it goes in most open source projects). But if there are no resources to talk about it, then how am I to know what to implement?

@nex3

This comment has been minimized.

Contributor

nex3 commented Oct 16, 2018

If you'd put forth a proposal for discussion, I wouldn't have marked your comment as spam. But asking for an update without providing any new suggestion or volunteering to help contributes nothing to the discussion.

Arguing about this here also contributes nothing to the discussion. If you want to address this further, feel free to do it privately over email.

@cyraid

This comment has been minimized.

cyraid commented Oct 16, 2018

For sure, bringing forth a proposal I shall :) .. This entire issue almost entirely seems to be summed up with the ability to interact from within a mixin (as far as I'm aware). So taking the bits of the conversation (I hope I have got the most important bits), perhaps something like the below? Trading off usability complexity and implementation complexity, would satisfy most?

@mixin some-mixin() {

  // Yes this could be provided as a parameter, but other nested mixins may benefit from the content //
  @if (content-has(color) ) {
    color : content-get(color);
  } // IF //

  // Here, the test for the block content, and using it with no second parameter, no preceding block means actually including it //
  @if (content-has(some-block-content) ) {
    @content(some-block-content);
  } // IF //

} // Mixin //

.some-class {
  @include some-mixin() {

    // Having a second parameter here means setting a value //
    @content(color, black);

    // No second parameter, but having a block after means setting it as a block //
    @content(some-block-content) {
      color : red;
      background-color : black;
    } // Content //

  } // Include //
} // Style //

Thank you for taking the time to respond.

@nex3

This comment has been minimized.

Contributor

nex3 commented Oct 17, 2018

I don't want to tie the behavior too closely to @content in particular. If we're going to add all the complexity of being able to pull data out of a chunk of generated CSS, I want that CSS to be first-class: you should be able to assign it to a variable and pass it around like any other value.

I'm also not clear on what your proposal is doing. Look at the various proposals in the language repo for an idea of the level of detail we're looking for.

@cyraid

This comment has been minimized.

cyraid commented Oct 18, 2018

I see.. I had no idea there was a proposals area. This would have changed my mindset entirely and I would not have wanted an update here, seeing as most of the updates are happening there.. Question though, why didn't you just reference that to begin with? haha there's even a proposal there that is pretty much the same thing.

Edit: Regardless though, I'll see if I can think of anything to add to the content using proposal that will satisfy what would be very useful, and tend to the power users as well. I do see value in the content functions.

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