Creating a SASS function to fallback to a value - sass

I would like to create a function that gets 2 variables as input and, if the first one exists, returns that variable and if not, it returns the fallback value.
I was trying this
#function component-token($token-name, $fallback) {
#if variable-exists($token-name) {
#return $token-name;
}
#return $fallback;
}
And I wanted to use it as
.my-class {
color: component-token($component-button-primary-color, $color-primary-base);
}
However, this poses two problems:
variable-exists expects a string to be passed in.
If $component-button-primary-color does not exist, compilation fails.
I tried calling the function by passing in a string, as such
.my-class {
color: component-token(component-button-primary-color, $color-primary-base);
}
but this left me with color being the string component-button-primary-color, which is of course not what I want.
To give a little bit of context, we're preparing a project for multibranding, in which we want to have our CSS have a base set of values, but every value should be overwriteable by a brand.
In the example above, we can assume that a brand will always have $color-primary-base. But a brand can also define the $component-button-primary-color variable, which should then overwrite the value.
Our first approach was going with !default as can be seen here. But this brings a lot of boilerplate, will require a lot of context switching because you can't find the needed information in the one line.
Any idea?

You can use optional parameters to get the required result.
Required parameters must precede optional parameters.
#function button-color($color-primary-base, $color-primary-button: null) {
#if $color-primary-button != null {
#return $color-primary-button;
}
#return $color-primary-base;
}
I changed the code / names to match your use case : changing the color of a button.
The caller:
$customer-color-primary-base : red;
$customer-color-primary-button: green;
button {
color: button-color($customer-color-primary-base, $customer-color-primary-button);
}
As you can see, it does not require a string as parameter.
You can experiment with keeping the parameters empty or not providing the optional parameter at all:
$customer-color-primary-button: null;
or
color: button-color($customer-color-primary-base);
it does allow you to change the variable later on (dynamic props are not possible, but it is possible to decalare it with a null value at first):
$customer-color-primary-base : red;
$customer-color-primary-button: null;
$customer-color-primary-button: green;
.button {
color: button-color($customer-color-primary-base, $customer-color-primary-button);
}

You can never call the mixin with an undefined variable. What you can do is make sure it exist just before calling the mixin and give a default value of 'false', for example.
EDIT: Another choice is to use a map or list to store the colors, instead of using regular variables, like so:
$color-primary-base: red;
$colors: (
component-button-primary-color: #007dc6
);
#function component-token($token-name, $fallback) {
#if (map-has-key($colors, $token-name)) {
#return map-get($colors, $token-name);
}
#return $fallback;
}
.my-class {
color: component-token(component-button-primary-color, $color-primary-base);
}
.my-class-2 {
color: component-token(unexistent-key, $color-primary-base);
}

Related

Getting module from string parameter

I'm trying to get a module name from a string. Here is an example of what I imagine it would look like:
#use 'dark'; /* _dark.scss, with a $color variable */
#use 'light'; /* _light.scss, with a $color variable */
#mixin theme($name) {
body {
color: #{$name}.$color;
}
}
If I passed in "dark" as an argument, I would want this result:
...
color: dark.$color;
...
If I passed in "light" as an argument, I would want this result:
...
color: light.$color;
...

Sass change the value of variable based on body class?

I am trying to create a sass mixin which allows to redefine the value of a variable according to a class added on . I am stuck at this stage (which does not work) and I wonder if it is finally possible to do so:
$color-1: #9E619B;
$color-2: #BB496A;
$themecolor: red !default;
$color-themes: (
   theme1-pages:
       (
           themecolor: $color-1
       )
   theme2-pages:
       (
           themecolor: $color-2
       )
);
#each $name, $theme in $color-themes {
   body.#{$name} {
       $themecolor: map-get ($name, themecolor);
   }
}
I think you've got a syntax error (extra space after map-get) and a mistake in the arguments for map-get(): the arguments should be $theme and themecolor respectively, not $name and themecolor:
#each $name, $theme in $color-themes {
body.#{$name} {
$themecolor: map-get($theme, themecolor);
}
}
That is because $name is simply the key, while $theme is the reference to the value. If you paste the fixed version of your code into SassMeister:
$color-1: #9E619B;
$color-2: #BB496A;
$themecolor: red !default;
$color-themes: (
theme1-pages:
(
themecolor: $color-1
),
theme2-pages:
(
themecolor: $color-2
)
);
#each $name, $theme in $color-themes {
body.#{$name} {
$themecolor: map-get($theme, themecolor);
color: $themecolor;
}
}
You should expect to get this back without any error:
body.theme1-pages {
color: #9E619B;
}
body.theme2-pages {
color: #BB496A;
}

Simple if inside an each scss

I've just started my scss experience and I'm trying to figure out why this kind of code it's not respond like I want.
#each $key,
$par in (a:1, b:0, c:1) {
#if #{$par}==1 {
.#{$key} {
color:red
}
}
}
I'm trying to create a class only if the $par element of the map is equal to 1. I'm preety sure that I'm stucking in some logical easy step, but searching around have found nothing about this simple example.
my_codepen
Lose the interpolation in the if-statement and you're good.
#each $key, $par in (a:1, b:0, c:1) {
#if ($par == 1) {
.#{$key} {
color:red
}
}
}

Passing parameters to i18n model within XML view

How can we pass parameters to the i18n model from within a XML view?
Without parameters
<Label text="{i18n>myKey}"/>
works but how can we pass a parameter in that expression?
The only piece of information I've found so far is http://scn.sap.com/thread/3586754. I really hope that this is not the proper way to do it since this looks more like a (ugly) hack to me.
The trick is to use the formatter jQuery.sap.formatMessage like this
<Label text="{parts:['i18n>myKey', 'someModel>/someProperty'],
formatter: 'jQuery.sap.formatMessage'}"/>
This will take the value /someProperty in the model someModel and just stick it in myKey of your i18n resource bundle.
Edit 2020-05-19:
jQuery.sap.formatMessage is deprecated as of UI5 version 1.58. Please use sap/base/strings/formatMessage. See this answer on usage instructions.
At the moment this is not possible. But you can use this simple workaround, that works for me.
Preparations
First of all we create a general i18n handler in our Component.js. We also create a JSONModel with a simple modification, so that immediatly the requested path is returned.
sap.ui.define([
"sap/ui/core/UIComponent",
"sap/ui/model/json/JSONModel"
], function(UIComponent, JSONModel) {
"use strict";
return UIComponent.extend("your namespace", {
/**
* Add a simple "StringReturnModel" to the components' models
*/
init: function() {
// [...] your other code in the init method
// String Return Model
var stringModel = new JSONModel({});
stringModel.getProperty = function(sPath) {
return sPath;
};
this.setModel(stringModel, "string");
},
/**
* Reads out a string from our text domain.
* The model i18n is defined in your manifest.json
*
* #param param text parameter
* #param arr array for parameters
* #return string
*/
i18n: function(param, arr) {
var oBundle = this.getModel("i18n").getResourceBundle();
return oBundle.getText(param, arr);
},
});
});
Now, a model with the context {string>} exists. To use the i18n function in the XML view, we create a formatter function. This function parses the parts of the binding and returns the localized string.
sap.ui.define([
], function() {
"use strict";
var formatter = {
/**
* First argument must be the property key. The other
* one are the parameters. If there are no parameters, the
* function returns an empty string.
*
* #return string The localized text
*/
i18n: function() {
var args = [].slice.call(arguments);
if (args.length > 1) {
var key = args.shift();
// Get the component and execute the i18n function
return this.getOwnerComponent().i18n(key, args);
}
return "";
}
};
return formatter;
});
How To Use:
Together with the string-model you can use the formatter to pass paramaters to your i18n:
<Text text="{ parts: ['string>yourProperty', 'string/yourFirstParamter', 'anotherModel/yourSecondParamter'], formatter: '.model.formatter.i18n' }" />
You can pass how many paramaters as you want, but be sure that the first "part" is the property key.
What is written at the link is correct for complex formatting case.
But if you want to combine two strings you can just write
<Label text="{i18n>myKey} Whatever"/>
or
<Label text="{i18n>myKey1} {i18n>myKey2}"/>
create file formatter.js
sap.ui.define([
"sap/base/strings/formatMessage"
], function (formatMessage) {
"use strict";
return {
formatMessage: formatMessage
};
});
View
<headerContent>
<m:MessageStrip
text="{
parts: [
'i18n>systemSettingsLastLoginTitle',
'view>/currentUser',
'view>/lastLogin'
],
formatter: '.formatter.formatMessage'
}"
type="Information"
showIcon="true">
</m:MessageStrip>
</headerContent>
Controller
var oBundle = this.getModel("i18n").getResourceBundle();
MessageToast.show(this.formatter.formatMessage(oBundle.getText("systemSettingsLastLoginTitle"), "sInfo1", "sInfo2"));
i18n
systemSettingsLastLoginTitle=You are logged in as: {0}\nLast Login: {1}
As ugly as it may seem, the answer given in the link that you mentioned is the way to go. However it may seem complicated(read ugly), so let's break it down..
Hence, you can use the following for passing a single parameter,
<Label text="{path: 'someParameter', formatter: '.myOwnFormatter'}"/>
Here, the someParameter is a binding of a OData model attribute that has been bound to the whole page/control, as it is obvious that you wouldn't bind a "hardcoded" value in a productive scenario. However it does end with this, as you see there isn't a place for your i18n text. This is taken care in the controller.js
In your controller, add a controller method with the same formatter name,
myOwnFormatter : function(someParameter)
{
/* the 'someParameter' will be received in this function */
var i18n = this.i18nModel; /* However you can access the i18n model here*/
var sCompleteText = someParameter + " " + i18n.getText("myKey")
/* Concatenate the way you need */
}
For passing multiple parameters,
Use,
<Label text="{parts:[{path : 'parameter1'}, {path :'parameter2'}], formatter : '.myOwnFormatter'}" />
And in your controller, receive these parameters,
myOwnFormatter : function(parameter1, parameter2) { } /* and so on.. */
When all this is done, the label's text would be displayed with the parameter and your i18n text.
In principle it is exactly as described in the above mentioned SCN-Link. You need a binding to the key of the resource bundle, and additional bindings to the values which should go into the parameters of the corresponding text. Finally all values found by these bindings must be somehow combined, for which you need to specify a formatter.
It can be a bit shortened, by omitting the path-prefix inside the array of bindings. Using the example from SCN, it also works as follows:
<Text text="{parts: ['i18n>PEC_to',
'promoprocsteps>RetailPromotionSalesFromDate_E',
'promoprocsteps>RetailPromotionSalesToDate_E'}],
formatter: 'retail.promn.promotioncockpit.utils.Formatter.formatDatesString'}"/>
Under the assumption, that you are using {0},{1} etc. as placeholders, a formatting function could look like the following (without any error handling and without special handling of Dates, as may be necessary in the SCN example):
formatTextWithParams : function(textWithPlaceholders, any_placeholders /*Just as marker*/) {
var finalText = textWithPlaceholders;
for (var i = 1; i < arguments.length; i++) {
var argument = arguments[i];
var placeholder = '{' + (i - 1) + '}';
finalText = finalText.replace(placeholder, arguments[i]);
}
return finalText;
},

SASS Index in nested list

I'm trying to get the index of an item in a nested SASS list — by the first property. But the only way i can get a result is to include both properties in the item.
Is is doable with native SASS, or would it require a mixin/function? And any input to how i would do that?
The code i got:
$icons : (
'arrow--down--full' '\e806', /* '' */
'cog' '\e805', /* '' */
'info' '\e807', /* '' */
'arrow--down' '\e800', /* '' */
'arrow--left' '\e801', /* '' */
'arrow--right' '\e802', /* '' */
'arrow--up' '\e803', /* '' */
'close' '\e804', /* '' */
'search' '\e804', /* '' */
'spin' '\e809' /* '' */
);
And my lookup
//Working
index($icons, 'search' '\e804');
//Not working, but what i want to achieve
index($icons, 'search');
It sounds like what you're talking about is a hash or lookup table, which Sass does not currently have. However, you can easily work around that in a variety of ways. Here are some examples.
You could structure your list a little differently:
$icons : (
'arrow--down--full', '\e806', /* '' */
'cog', '\e805', /* '' */
'info', '\e807', /* '' */
...
);
I've added a comma after each item. Now to look it up you'd write a function like
#function lookup($list, $key) {
#return nth( $list, ( ( index($list, $key) ) + 1) );
}
And call it like so
lookup($icons, 'cog'); // => '\e805'
You could push this a little further by making 2 different lists and then associating them via a similar function:
$icon-keys: ('arrow--down--full', 'cog', 'info' ... );
$icon-values: ('\e806', '\e805', '\e807' ... );
I've lined up the values with whitespace only to make them more legible to me so that they appear a bit like an actual table, but there are tons of ways of writing Sass lists, and you may prefer another. Then the function that associates them:
#function lookup($lookup-key, $all-keys, $all-values) {
#return nth($all-values, index($all-keys, $lookup-key));
}
And using it:
lookup('cog', $icon-keys, $icon-values); // => '\e805'
For my tastes, these are both a bit clunky so I'd make a shortcut function to make it a bit more legible:
for the first variation:
#function icons($lookup-key) {
#return lookup($icons, $lookup-key);
}
for the second:
#function icons($lookup-key, $types: $icon-keys, $values: $icon-values) {
#return lookup($lookup-key, $types, $values);
}
so you could just in either case call
icons('cog');
You'd probably want to put a little more logic in your lookup functions to error catch, and you could also expand it to both accept and return a list rather than a single value, but this is just a basic example.
#cimmanon answered this: https://stackoverflow.com/a/17004655/786123
#function find-value($list, $key) {
#each $item in $list {
#if ($key == nth($item, 1)) {
#return nth($items, 2);
}
}
#return false;
}

Resources