Using local Sass variables in mixin - sass

I have a bunch of variables that are defined for use in a light/dark theme (two separate files):
$primary-1: red;
$primary-2: green;
and I don't want to declare them all twice when consuming them. I wrote a mixin that does the assignments for me:
#mixin assign-vars {
--primary-1: #{$primary-1};
--primary-2: #{$primary-2};
}
and I would like to use it like this:
#import 'assign-vars';
:root,
:root[data-theme='light'] {
#import 'light-theme-variables';
#include assign-vars;
}
:root[data-theme='dark'] {
#import 'dark-theme-variables';
#include assign-vars;
}
but this does not work, as I get an error saying that $primary-1 is an undefined variable. How can I accomplish this without having to do all of the declarations twice?

I ended up moving the variables into a map:
$light-vars: (
$font-name: 'Some Font',
$primary-1: red
);
$dark-vars: (
$font-name: 'Some Other Font',
$primary-1: green
);
and created a helper mixin to assign them to CSS vars of the same name:
#mixin assign-map-properties($map) {
#each $key, $value in $map {
#if (type-of($value) == 'string') {
--#{$key}: '#{$value}';
}
#else {
--#{$key}: #{$value};
}
}
}
which allowed me to accomplish what I need:
#import 'assign-map-properties';
:root,
:root[data-theme='light'] {
#import 'light-vars';
#include assign-map-properties($light-vars);
}
:root[data-theme='dark'] {
#import 'dark-vars';
#include assign-map-properties($dark-vars);
}
Generated CSS:
:root,
:root[data-theme='light'] {
--font-name: 'Some Font';
--primary-1: red;
}
:root[data-theme='dark'] {
--font-name: 'Some Other Font';
--primary-1: green;
}

Related

Variable values based on class

Why can't I change a scss variable based on a class? I want the variable to be green when in class dark-mode.
Css:
$test: red;
#mixin darkTheme {
$test: green;
}
.theme-dark {
#include darkTheme();
.test {
background-color: $test;
}
}
Html:
<body class="dark-mode">
<div class="test">test</div>
</body>
How do I accomplish this? What I don't want is 2 variables.
This is because of Variable Scoping.
In Sass, all variables declared outside of a mixin or function will have a global scope and can be referenced in any Sass selector that uses the variable. (source)
This means that any variable value set inside of a mixin or function is only available within that mixin or function, even if the variable was previously set globally.
Switching between different sets of Sass variables
Partials
You could have a partial file for each theme, and import those under each theme's parent class.
_theme-dark.scss
$background-color: #000;
$text-color: #fff;
_theme-light.scss
$background-color: #fff;
$text-color: #000;
_themed-page.scss
body {
background: $background-color;
color: $text-color;
}
theme-styles.scss
.theme-dark {
#import "theme-dark";
#import "themed-page";
}
.theme-light {
#import "theme-light";
#import "themed-page";
}
Maps
Another option is to store the theme values in a map and have a utility function to retrieve the desired value. (source)
_theme-variables.scss
$theme: 'default';
$theme-values: (
'background-color': (
'default': #eee,
'light': #fff,
'dark': #000
),
'text-color': (
'default': #333,
'light': #000,
'dark': #fff
)
);
#function theme-value($key) {
$map: map-get($theme-values, $key);
#return map-get($map, $theme);
}
_themed-page.scss
body {
background: theme-value('background-color');
color: theme-value('text-color');
}
theme-styles.scss
.theme-dark {
$theme: 'dark';
#import "themed-page";
}
.theme-light {
$theme: 'light';
#import "themed-page";
}

SASS Mixin Rewrite & (ampersand)

I'm trying to write a mixin that will modify the parent selector on output. The idea is that in cases where a mixin is called, the parent selector will need to have a string replacement done on it. I have most of this working, but I can't figure out how to swallow the &.
.test {
#include alt_parent() {
content: 'test';
}
}
The mixin is something like this:
#mixin alt_parent() {
#{str-replace(unquote("#{selector_append(&)}"), "s", "x")} {
#content;
}
}
I have the string replacement working, so that isn't the problem. What I get is this (and I understand why):
.test .text {
content: 'test';
}
What I want is this:
.text {
content: 'test';
}
You have to use the #at-root directive to defeat the automatic inclusion of the selectors represented by &.
http://alwaystwisted.com/articles/2014-03-08-using-sass-33s-at-root-for-piece-of-mind
#mixin alt_parent($parent) {
#at-root {
#{str-replace(unquote("#{selector_append(&)}"), "s", "x")} {
#content;
}
}
}

Sass - How to Import a Message into Directive

I use #warn a lot, but I'd like to import pre-written messages into my warnings that are stored for example in another function or file.
Is this possible?
I know the following won't work, but so you get the idea...
#if $a == 'red' {
color: red;
} #else if $a == 'blue' {
color: blue;
} #else {
#warn '#include error-msg_no-color-detected';
}
This can be done using variables...
$msg: 'Something went wrong!';
#if ... {
...
} #else {
#warn: '#{$msg}';
}
This allows me to create a _debug.scss file containing lots of messages.
#import 'config';
#import 'debug';
#import 'dark';
#import 'application';
_debug.scss:
#msg-1: 'Oops!';
#msg-2: 'No color defined. Check the config file.';

Creating a sass map value from within a mixin - saved globally [duplicate]

This question already has answers here:
How to assign to a global variable in Sass?
(2 answers)
Closed 6 years ago.
Is it possible to update a value in a sass map from within a mixin so that the change is saved globally?
Eg
$obj: (
init: false
)
#mixin set($map) {
#if map-get($obj, init) != true {
// mixin hasn't been called before
$map: map-set($map, init, true);
}
#else {
// mixin has been called before
}
}
.test {
#include set($obj);
// sets the init value to true
}
.test-2 {
#include set($obj);
// init value has already been set to true
}
I'm not sure if I understood what you are trying to do, but your code seems to be fine (haven't tested it though), excepting that there is no map-set function, but you can create one or just use map-merge (check here: http://oddbird.net/2013/10/19/map-merge/). I hope that helps.
#update 1: I think I got your question now, you want to pass the reference through the mixin, so if you have multiple maps, you can send the one you want to update to the mixin, I don't think this is possible though, because no reference is kept, if you need to update the variable you have to link directly to it, for exemple, this works (tested):
$obj: (
init: false
);
#mixin set($map) {
#if map_get($map, init) != true {
$obj: map-set($map, init, true) !global;
body {
background-color: #000;
}
} #else {
body {
background-color: #ff0000;
}
}
}
#include set($obj);
#include set($obj);
But if you reference to $map instead of $obj (in this line $obj: map-set($map, init, true) !global;), then a new global map (called $map), will be created. And every time you call the mixin again, it will be replaced by the map you sent as a parameter.
#update 2: I found a way to do it, but you have to keep a global 'map of maps', and every time you update this guy, you send the name of the map you want to update as parameter, so I came up with the following code, it's tested and working fine :)
#function map-set($map, $key, $value) {
$new: ($key: $value);
#return map-merge($map, $new);
}
$maps: (
obj1: (
init: false
),
obj2: (
init: false
),
);
#mixin set($prop) {
#if map_get(map_get($maps, $prop), init) != true {
$obj: map-set(map_get($maps, $prop), init, true);
$maps: map-set($maps, $prop, $obj) !global;
body {
background-color: #000;
}
} #else {
body {
background-color: #ff0000;
}
}
}
#include set(obj1); //black
#include set(obj2); //black
#include set(obj1); //red
#include set(obj2); //red
source: myself
Following on from #Paulo Munoz
Here is the solution
#function map-set($map, $key, $value) {
$new: ($key: $value);
#return map-merge($map, $new);
}
$extend : ();
$obj : (
margin: 0,
padding: 10
);
#mixin set($map, $name) {
#if map-has-key($extend, $name) {
map: has-key;
// call placeholder class
} #else {
$extend: map-set($extend, $name, true) !global;
map: does-not-have-key;
// create placeholder class
// call placeholder class
}
}
.test {
#include set($obj, test);
}
.test-2 {
#include set($obj, test);
}
which generates
.test {
map: does-not-have-key;
}
.test-2 {
map: has-key;
}

What is the SASS equivalent of additive mixin definitions in LESS?

Is there a way to have SASS emulate the way LESS concatenates repeated mixin definitions (redefining a mixin in LESS doesn't overwrite the original).
For instance, would it be possible to push a block of CSS rules into a buffer, and then flush them all at once?
Example:
With LESS I'd do this:
// _menu.less
#_base () { .menu{ /*styles*/ } }
#_mobile () { .menu{ /*styles*/ } }
#_desktop () { .menu{ /*styles*/ } }
...
// _widget.less
#_base () { .widget{ /*styles*/ } }
#_mobile () { .widget{ /*styles*/ } }
#_desktop () { .widget{ /*styles*/ } }
...
and then:
// styles.less
#import "_menu.less";
#import "_widget.less";
#media screen { #_base(); }
#media screen and (max-width:700px) { #_mobile(); }
#media screen and (min-width:701px) { #_desktop(); }
...
// styles-oldie.less
#import "_menu.less";
#import "_widget.less";
#media screen {
#_base();
#_desktop();
}
To the best of my knowledge, there is no way to replicate what you want to achieve by building on to existing mixins. If you define a mixin two times, the second will overwrite the first. See example
AFAIK the common practice in Sass is to use media query mixins inside the selector to keep the code clean and readable. Breakpoint is a popular library that adds a lot of nice functionality for doing this.
An example of the code would be.
#import "breakpoint";
$medium: 600px;
$large: 1000px;
$breakpoint-no-queries: false; // Set to true to ignore media query output
$breakpoint-no-query-fallbacks: true; // Set to true to output no-query fallbacks
$breakpoint-to-ems: true; // Change px to ems in media-queries
.menu {
content: "base";
// Mobile styles
#include breakpoint(min-width $medium - 1) {
content: "mobile";
}
// Tablet styles
#include breakpoint($medium $large) {
content: "tablet";
}
// Desktop styles with no-query fallback
#include breakpoint($large, $no-query: true) {
content: "large";
}
}
This could output (depending on your settings)
.menu {
content: "base";
content: "large";
}
#media (min-width: 37.4375em) {
.menu {
content: "mobile";
}
}
#media (min-width: 37.5em) and (max-width: 62.5em) {
.menu {
content: "tablet";
}
}
#media (min-width: 62.5em) {
.menu {
content: "large";
}
}
You can play around with the settings here
I often have a stylesheet for modern browsers that support media queries set up like this:
// main.scss
$breakpoint-no-queries: false;
$breakpoint-no-query-fallbacks: false;
#import "imports";
And another stylesheet for older browsers that don't support media queries
// no-mq.scss
$breakpoint-no-queries: true;
$breakpoint-no-query-fallbacks: true;
#import "imports";

Resources