7

Tagged Template Literals - The Magic Behind Styled Components

 2 years ago
source link: https://dev.to/dekel/tagged-template-literals-the-magic-behind-styled-components-2f2c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Intro & Background

If you have some experience with React, you probably came across styled-components. In the last few years, the concept of css-in-js became more popular, and there are multiple libraries that are available for us to use. styled-components is one of them, but you can also find Emotion, Radium, JSS, and more. In this post I'm not going to cover the pros and cons of traditional stylesheet files vs. styled-components, and instead - I'm going to focus on tagged template literals - the "magic" that let us use the styled-components syntax.

styled-component basics - a quick reminder

Let's take the following simple syntax for example:

const MyComponent = () => { return <StyledDiv>Welcome to my website</StyledDiv>; };

const StyledDiv = styled.div` color: red; font-weight: bold; `;

The StyledDiv in the example above is actually a React component that returns a div block with the css of color: red; font-weight: bold;.
Well... kind of. Actually - it's a bit more complicated than that. The output of the above is a div with specific css class-names having the above css definitions inside:

<style> .gVKleL { color:red; font-weight:bold; } </style> <div class="sc-bdnylx gVKleL">Welcome to my website</div>

Some of you are probably using this without giving it too much of a thought. If we take a closer look we can see the usage of the backtick (`) right after the styled.div.
This syntax in Javascript is called Tagged Template Literals.

Template Literals

Let's start with Template Literals, and then move on to the more interesting concept.
In javascript - template literals are strings that can contain expressions within them:

const a = 1; const b = 2; const s = `${a} + ${b} = ${a + b}`; console.log(s); // 1 + 2 = 3

As you can see from the example above - expressions can be variables, but are not limited to them:

const sum = (a, b) => a + b; const a = 1; const b = 2; const s = `${a} + ${b} = ${sum(a, b)}`; console.log(s); // 1 + 2 = 3

We can use either variable or function inside a template literal, and the value that will be used is basically the string representation of the expression:

class MyClass { toString() { return 'This is the string of MyClass'; } } const c = new MyClass(); console.log(`${c}`); // This is the string of MyClass

Now that we understand the power of template literals - it's time to move on to tagged template literals - or just tagged templates.

Tagged Templates - Whats the fuzz?

With tagged templates, we have the power to parse the template literal ourselves using our own "home-made" function.

const funcA = () => "String A"; const strB = funcA`String B`; console.log(strB); // String A

Note that in the example above - the variable strB contains the string String A (and not String B as you might expect).

Let's break it down

  1. The function funcA returns the string String A.
  2. By using the function funcA as a tagged template - we completely ignored the string that was sent, and we just return something else.
  3. We can use it with an empty string, and the output will be the same.

Check it out:

const funcA = () => "String A"; const strB = funcA``; console.log(strB); // String A

Advanced features

The function we use in a tagged template can return everything that we want - we are not limited to only return strings:

const strAsArray = (strings) => [strings[0]]; const arr = strAsArray`Some string`; // arr == ["Some string"] console.log(arr);

Building tagged templates have an option to also accept variables that can be used:

const sumStr = (strings, num1, num2, sum) => { return num1 + strings[1] + num2 + strings[2] + sum }; sumStr`${1} + ${2} = ${1 + 2}`; // 1 + 2 = 3

The first argument is a special object, which behave as an array and provides access to all the "native strings" in the original string that was passed to the tag-template function (strings[0], strings[1], strings[2]), alongside a raw property, which allows you to access the original raw strings.

The rest of the arguments are the expressions that we used inside the template literal.

Time to build - let's start with something basic

Now that we know a bit more about template literals it's time to move on to some more complex examples.
We already know that template literals don't have to return strings, so how about creating a simple example of a tagged template that returns a react component with the relevant style?
We will start by creating a simple div example to just wrap our text with some styling on it:

const MyComponent = () => { return <Div>Welcome to my website</Div>; };

const Div = divTag` color: red; font-weight: bold; `;

The tagged template is the following code:

const divTag = (cssDefs) => { // This is the tagged template const cmp = ({ children }) => { const styleObj = cssObjFromStr(cssDefs[0]); return <div style={styleObj}>{children}</div>; };

// We need to return a function (react component), not JSX object. return cmp; };

The full example is available here:

Check out the helper functions (the cssObjFromStr). we are going to focus on it in the next example.

Using props in the component

The basic example gave us a nice intro, but what about the component's props? We use them all the time in React, and losing them is not an option. Moving to the next example, we will add the option to also use props as part of our component that we would like to style:

const divTag = (cssDefs) => { const cmp = ({ children, ...rest }) => { const styleObj = cssObjFromStr(cssDefs[0]); return ( <div style={styleObj} {...rest}> {children} </div> ); };

return cmp; };

We will use the onClick prop on the div element.
The full example is here:

Using props in the template

Props are not only relevant to the components, but also to the template itself. We want to use the props of the component inside the template itself - colors, elements behavior, and more.
To do this we will need to pass the props from the <Div...> to the cssObjFromStr method:

const cmp = ({ children, ...rest }) => { const styleObj = prepareCssString(template, expressions, rest); ... return ( <div style={styleObj} {...rest}>

But this is not enough.
Let's assume that we use the <Div> element with the property textColor="blue":

<Div textColor="blue" onClick={() => { console.log("click"); }} >

The issue we face here is that the <div> component (inside the cmp) will get the textColor property, which is not a valid property of a div element.
A specific solution can be to extract the textColor from the props, and pass the rest of the properties down to the <div> element:

const { textColor, ...nonStyledProps } = rest;

return ( <div style={styleObj} {...nonStyledProps}> {children} </div> );

Working example can be found here:

The styled-components solution is a bit more elegant (and much more generic) - all props that start with $ are considered "private props" and will not pass down to the actual jsx component.
We will use the same concept, but in our example, we will use the _ (underscore) to create private props.

const nonStyledProps = Object.keys(rest).reduce((accu, item) => { if (item[0] === "_") { accu[item] = rest[item]; } return accu; }, {});

return ( <div style={styleObj} {...nonStyledProps}> {children} </div> );

Full working example can be found here:

Summary

The styled-component library contains much more than that, with the entire built-in HTML tags, wrapped-components, classes and inheritance (instead of inline-style), global themes and more, however styled-components is just an example of how to use the tagged template literals, which is eventually "just" a native javascript feature as of ECMAScript 2015 (also known as ES6).


Cover photo by Gerd Altmann @ pixabay

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK