emotion-js / emotion

šŸ‘©ā€šŸŽ¤ CSS-in-JS library designed for high performance style composition
https://emotion.sh/
MIT License
17.32k stars 1.1k forks source link

Questions about Emotion patterns #287

Closed denchen closed 5 years ago

denchen commented 6 years ago

I'm not sure if this is the right place to ask general questions on using Emotion, so I apologize in advance (if there's a better forum for this, please let me know). First off, I'm fairly new to React, and I'm very new to CSS-in-JS, so I'm still trying to wrap my head around the concepts. I've gone over the documentation, but I still have questions regarding usage. I have a bunch of CSS from an old Ember project that I want to port to ReactJS, so that's going to be my starting point in terms of what I want to accomplish.

1. Base classes

In my old project, I had a bunch of base classes that are meant to be used anywhere. For example:

.link, .link:active, .link.visited {
  color: blue;
  cursor: pointer;
}
.link:hover {
    color: purple;
}

I should be able to drop this into anywhere -- a <div>, a <button>, <MyFancyBox> -- and on a case by case basis (eg, I'm outputting a list with each <li> except the third one having .link). What's the paradigm in Emotion to do this?

2. Adapting based on props.

One of the examples from styled-components was that of <Button> and being able to compose <Button primary> and <Button secondary>. The way I understand it, the way you would implement this is:

const Button = styled.button`
  color: ${props => props.primary ? 'red' : props.secondary ? 'blue' : 'green';
  background: ${props => primary ? 'white' : props.secondary ? 'gray' : 'white;
`

This seems cumbersome. If both primary and secondary are modifying, say, 8-10 properties, then our CSS will be filled with${props => props.primary ? ...} over and over again. And let's say we add another prop on top of that. Let's say we have an active prop that modifies most of the same parameters, BUT only in the case of secondary and not primary. In plain CSS, this would be

.button.primary {
  /* stuff */
}
.button.secondary {
  /* stuff */
}
.button.secondary.active {
  /* stuff */
}

So in Emotion, how do I break this down into something readable and maintainable?

3. Using theming

I'm a little unclear on where you can use theming. In your docs, you have this example:

const H1 = withTheme(styled(Heading)`
  color: ${p => p.theme.purple};
`)

But can theming be used with the css` ` syntax? Why can't I do something like this?

const HBase = css`
  color: ${props => props.theme.red};
}

const H1 = withTheme(styled(Heading))`
  composes: ${HBase};
`

What I end up seeing in the Dev Console is something like this:

color: function (p) { return p.theme.purple; };

So I'm not exactly sure how to use props within a css` ` tag.


Thanks for any answers!

tkh44 commented 6 years ago

I want to answer this properly, but I'm going to be offline for the next few days. Hopefully, someone else can step in and help answer some of these questions before I get back. šŸ˜„

denchen commented 6 years ago

I kinda solved #2 above by passing in a series of objects to styled, so something like

const Button = styled.button({
  backgroundColor: 'white',
  color: 'green',
  fontSize: '16px',
  padding: '10px 20px'
}, (props) => {
  let css = {};
  if (props.primary) {
      css.backgroundColor = 'white';
      css.color = 'red'
    }
  }
  if (props.secondary) {
      css.backgroundColor = 'gray';
      css.color = 'blue';
      if (props.active) {
        css.color = 'lightblue';
      }
    }
  }
  return css;
});

I don't know if this is the ideal solution. The main downside is the use of camelCase notation instead of actual CSS notation, and the fact that it doesn't feel as clean as a pure CSS file. But it's manageable.

ChristopherBiscardi commented 6 years ago

1. Base Classes

You're looking for css.

Let's say we have the following program:

import { css, sheet } from "emotion";

const linkClass = css`
&, &:active, &.visited {
  color: blue;
  cursor: pointer;
}
&:hover {
    color: purple;
}
`;

console.log(linkClass);
console.log(sheet.sheet.cssRules);

With a .babelrc of

{
  "presets": [
    ["env", {
      "targets": {
        "node": "current"
      }
    }]
  ],
  "plugins": [
    "emotion/babel"
  ]
}

that compiles to:

// compiled.js
"use strict";

var _emotion = require("emotion");

const linkClass = /*#__PURE__*/(0, _emotion.css)([], [], function createEmotionStyledRules() {
  return [{
    "&, &:active, &.visited": {
      "color": "blue",
      "cursor": "pointer"
    },
    "&:hover": {
      "color": "purple"
    }
  }];
});

console.log(linkClass);
console.log(_emotion.sheet.sheet.cssRules);

The output of compiled.js when run is

css-10pfecp
[ { cssText: '.css-10pfecp, .css-10pfecp:active, .css-10pfecp.visited{color:blue;cursor:pointer;}' },
  { cssText: '.css-10pfecp:hover{color:purple;}' } ]

There are two things to notice here. One is that linkClass is the className string, so you can put it anywhere you can use a normal classname. The other is that the rules that get inserted into the DOM.

.css-10pfecp, .css-10pfecp:active, .css-10pfecp.visited{
  color:blue;
  cursor:pointer;
}

2. Adapting based on props.

I'll use an example that's probably a bit more complex than what you need. Assuming you have a Button which is a base level component (that is, configurable based on props) as such:

import { h } from "preact";
import styled from "preact-emotion";

export default styled.button`
  cursor: pointer;
  display: inline-block;
  min-height: 1em;
  outline: none;
  border: none;
  vertical-align: ${({ verticalAlign }) => verticalAlign};
  background: ${({ background }) => background};
  color: ${({ textColor }) => textColor};
  font-family: ${({ fontFamily }) => fontFamily};
  ...
`

You can create then the variant -> object of configurable props mapping as such:

import { h, Component } from "preact";
import Button from "@ocode/button";
import { buttons, sizes } from "@ocode/constants/lib/buttons";

export default class Buttons extends Component {
    render({ size, variant, children, ...props }) {
      const b = buttons[variant] || buttons.primary;
      const s = sizes[size] || sizes.default;
      return (
        <Button {...props} {...b} {...s}>
          {children}
        </Button>
      );
  }
);

Alternatively, a simpler approach would be to keep everything in the same file and use a props to construct composes. You can imagine that this function can contain any logic you want it to including fallbacks to defaults etc.

const primary = css`
  background-color: blue;
  font-size: 20px;
`
const secondary = css`
  background-color: red;
  font-size: 50px;
`
export default styled.button`
  composes: ${({primary}) => primary ? secondary : blue};
  border-radius: 3px;
`

3. Using theming

But can theming be used with the css syntax? Why can't I do something like this?

css returns a class name and doesn't know if it's being used inside React/Preact/Angular/raw JS/etc. For example, earlier in this post, I used it inside of some random node code.

Perhaps there's a way to treat css`` as something that returns a function and does fancy detection etc but I don't currently need or use themable utility classes so I'll let someone else speak to that. I generally use utility components which abstract the need for dealing with classes.

denchen commented 6 years ago

@ChristopherBiscardi Thanks for the detailed response.

1. Base Classes

Thanks, that was the solution I was hoping for. I tested out your example, and it worked exactly as I expected.

2. Adjust based on props

Your second example is pretty easy to understand, and i'll probably end up doing something really similar. But I do have a question about composes. I notice it gets applied first. So shouldn't what goes in composes be the base style, and what's in the main styled.button be for overriding that base class with whatever props (primary, secondary)? In your example, if there is no primary or secondary and I still want to set a background-color, where would I put it?

As for your first example, I'm a little confused what this is doing:

<Button {...props} {...b} {...s}>

Are you expecting b and s to be arrays? Objects? Classnames?

3.Theming

I ended up forgoing theming and just creating my own constants file with all the theming I needed (mainly colors) and importing that where ever I needed (eg. import {colors} from 'colors';). It seemed much cleaner and easier that way.

ChristopherBiscardi commented 6 years ago

2. Adjust based on props

Ah yeah, so this line in that example:

import { buttons, sizes } from "@ocode/constants/lib/buttons";

imports objects shaped like this:

import { lighten, darken } from "polished";
import colors from "./colors";

export const buttons = {
  primary: {
    color: "white",
    backgroundColor: colors.brand,
    borderColor: colors.brand,
    disabledColor: lighten(0.2, colors.brand),
    hoverBackgroundColor: darken(0.2, colors.brand),
    hoverBackgroundBordercolor: darken(0.2, colors.brand)
  },
  digital: {
    borderColor: colors.digitalBlue,
    backgroundColor: colors.digitalBlue,
    color: "#fff",
    disabledColor: lighten(0.2, colors.digitalBlue),
    hoverBackgroundColor: darken(0.2, colors.digitalBlue),
    hoverBackgroundBordercolor: darken(0.2, colors.digitalBlue)
  },
...
};

export const sizes = {
  xsmall: {
    paddingX: ".5rem",
    paddingY: ".2rem",
    fontSize: ".75rem",
    lineHeight: "1.5"
  },
...
};

Where the pattern is basically:

const thing = {
  variant: cssObj
}

so when you spread {...b} you're spreading a cssObj, which is just the configurable props from the base Button that are relevant for primary or large or whatever.

So shouldn't what goes in composes be the base style...

I avoid this kind of inheritance patterning generally but you could do it that way.

In your example, if there is no primary or secondary and I still want to set a background-color, where would I put it?

If there's no primary or secondary I usually default to primary for example. You could also just leave composes blank for those values and expect that they'll be passed in piecemeal to each prop. You could even put logic there to prevent them from being set if there is a primary, etc.

It's up to what you want to accomplish really, there are a few options.

3.Theming

I ended up forgoing theming and just creating my own constants file with all the theming I needed (mainly colors) and importing that where ever I needed (eg. import {colors} from 'colors';). It seemed much cleaner and easier that way.

Yeah, that's a valid option if you'll only have a single theme and the values are static, etc. I'm doing something similar but using styled-theming, etc to set the JSON in the context at the root of my app so I can swap themes for different users, etc.

denchen commented 6 years ago

@ChristopherBiscardi Thank you. I think I have a much better understanding of how everything comes together now.

I do have one last question: how do you handle a component whose styling may depend on the CSS of a parent DIV or component? For example, I have a <Button>, but if it's with a <ButtonGroup>, such as:

<ButtonGroup>
  <Button>A</Button>
  <Button>B</Button>
</ButtonGroup>

there needs to be some adjustment to the <Button> CSS (for example, padding). Would the pattern be to add the styling of <Button> within <ButtonGroup>, such as:

const ButtonGroup = styled.div`
  /* button group styling */
  & button {
    /* button styling */
  }
`

Or is there another way to do this?

ChristopherBiscardi commented 6 years ago

This gets more into React/Preact/etc code than it has to do with emotion. The concept is that you want your button to be able to handle applying its own styles based on props for any context you intend to render it in. In the following example, I use the btnGrouper function to apply different paddings based on a prop that is passed in from ButtonGroup. Since ButtonGroup knows the relevant information about its children, we can apply the required props to <Button> in render. (please excuse the naming, etc I rushed-wrote this example on the emotion site šŸ˜… )

function btnGrouper({ firstInGroup, lastInGroup }) {
  if (firstInGroup) {
    return "0 30px";
  }
  if (lastInGroup) {
    return "30px 0";
  }
  return "30px";
}
const Button = styled("button")`padding: ${btnGrouper};`;

class ButtonGroup extends React.Component {
  render() {
    const count = React.Children.count(this.props.children);
    return (
      <span>
        {React.Children.map(this.props.children, (child, i) => {
          return React.cloneElement(child, {
            firstInGroup: i === 0,
            lastInGroup: i === count - 1
          });
        })}
      </span>
    );
  }
}

render(
  <ButtonGroup>
    <Button>A</Button>
    <Button>B</Button>
    <Button>c</Button>
  </ButtonGroup>,
  mountNode
);

And here's a screenshot of the result:

screen shot 2017-08-30 at 9 30 40 am

If you do it this way, the Button owns it's own styles rather than making ButtonGroup override Button styles. This makes it easier to maintain and modify because you're locating all Button related styles in Button and ButtonGroup is only responsible for grouping logic.

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Thank you for your contributions.