question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC composable class names

See original GitHub issue

Motivation & Examples

Currently css returns a single string with all class names, so it isn’t possible to get a single class name for a specific property. For example:

const classNames = css({ color: 'blue', background: 'red' }) // e.g output: "c0 c1"
classNames.color // <<< this isn't possible

It’d be great if the result of css was like:

{
   color: 'c0',
   background: 'c1',
   toString() { return 'c0 c1' },
}

An example use case:

const styles = css({ color: 'blue', background: 'red' })
function MyComponent() {
   return (
      <div className={styles.background}>
         <ChildComponent className={styles.color} />
      </div>
   )
}

This would offer many benefits:

  • As a user, you could avoid using a selector to style children (e.g: & > div) and just pass down class names as props. In this case, a child component that is re-usable but it should be styled differently depending on where it is used. For example, a re-usable Heading component which could have different color.
  • It’d make it easier to create a Webpack plugin/loader to extract static styles #37 because. For example: Static analysis is difficult or impossible
// A.js
const ComponentA = ({ color }) => <div className={css({ color })} /> // What is color??
// B.js
const ComponentB = () => <ComponentA color="#fff" />

However, this is better and makes it possible to make static analysis of the code:

// A.js
const ComponentA = ({ color }) => <div className={color} />
// B.js
const styles = css({ color: '#fff' }) // Static analysis is possible
const ComponentB = <ComponentA color={styles.color} />

Details

stylex was shown in ReactConf by Facebook, so the original idea comes from there:

const styles = stylex.create({
   blue: { color: 'blue' },
   red: { color: 'red' },
})
function MyComponent() {
   return (
     <span className={styles('red', 'blue')}>I'm blue</span>
   )
}

Based on that:

  • css could return an object instead of serializing all classnames:
const styles = css({
   color: 'hotpink',
   backgroundColor: 'red',
   selectors: {
        '&:focus': { color: 'green' },
        '&:hover': { color: 'lime' },
   },
})
// Returned value should look like:
const styles = {
   color: 'c0',
   backgroundColor: 'c1',
   selectors: {
        '&:focus': { color: 'c2' },
        '&:hover': { color: 'c3' },
        toString() { return 'c2 c3' },
   },
   toString() { return 'c0 c1 c2 c3' },
}

This could allow to access any classname:

styles.toString() // c0 c1 c2 c3
styles.color // c0
styles.selectors.toString() // c2 c3
styles.selectors['&:focus'] // c2

Note: also add valueOf()? 🤔

  • otion could export a create method (or choose better name) that could allow to create a styles object. This could basically just be (overly simplified version):
function create(styleObj: Record<string, CSSPropsThing>) { // << Generic type is better
    const keys = Object.keys(styleObj)
    const styles = {}
    keys.forEach(k => styles[k] = css(styleObj[k])) // << Take into account nested stuff (selectors)
    styles.toString = function() { return '....' /* get all classnames somehow */ }
}

So, similarly as with css:

const styles = create({
   square: {
      color: 'hotpink',
      border: '1px solid blue',
   },
})
// Returned value should look like
const styles = {
   square: {
       color: 'c0',
       border: 'c1',
       toString() {...}
   },
   toString() { return combineAllStyles(...) } // See next 👇 
}
  • otion could export a combine method (or choose better) to combine style objects and deduplicate styles:
const style1 = css({ color: 'red', background: 'blue' }) // { color: 'c0', background: 'c1' }
const style2 = css({ color: 'blue' }) // { color: 'c2' }
const finalStyle = combineOrDedupeOrFancyName(style1, style2)
// Return should be
const finalStyle = { color: 'c2', background: 'c1' } // color is blue, background is blue

TL;DR It’s a breaking change but I believe this could allow for:

  • Easier static analysis, so a babel plugin would be easier to write (see: https://github.com/giuseppeg/style-sheet)
  • It allows for composable class names that could be passed down as props in React apps (or similarly outside of React) and can potentially allow the user to avoid writing complex selectors that style children

Summarizing how the new API could look like and it’s usage:

// A.js - unchanged (because `toString()`)
const MyComponentA = () => (
  <div className={css({ color: 'red' })} />
)
// B.js - `toString` of `create` return would combine & serialize all class names
const MyComponentB = () => (
    <div className={css.create({
      box: { border: '1px solid red' },
      other: { color: 'blue' }
   })} />
)
// C.js - composable
const MyComponentC = ({ className }) => <div className={className} />

const styles = css.create({
   box: { border: '1px solid green', background: 'grey' },
   child: { color: 'blue' },
})
const MyComponentD = ({ inheritStyles }) => (
    <div className={css.combine(styles.box, inheritStyles)}> // < Combine own styles & optionally inherit from props
        <MyComponentC className={styles.child} /> // pass down single class name
    </div>
)

const sectionStyles = css.create({
   div: { ... }
   box: { border: '2px solid lime' },
})
const MyComponentE = () => (
   <div className={sectionStyles.div}>
      <MyComponentD inheritStyles={sectionStyles.box} /> // <<< override MyComponentD `border`
   </div>
)

Let me know what do you think 😅 and if it’s within the scope of otion to support this. I can help with the implementation 😄

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:6 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
kripodcommented, Aug 2, 2020

Thank you for sharing your thoughts in detail, @etc-tiago! While object spreading is possible, someone may decide to pass stringified class names generated by otion to a component, e.g.:

type GreetingProps = {
  className: string;
}

function Greeting({ className }: ComponentProps) {
  return (
    <p
      className={css(
        { color: "red" }, // Pre-defined styles
        className // Overrides specified as a string of class names
      )}
    >
      Hello!
    </p>
  );
}

function App() {
  return <Greeting className={css({ color: "green" })} />;
}

For this case, otion should keep track of its injected rules in a class-keyed Map. When composing an object with a list of class name strings, the latter should be mapped back to objects and then deep-merged with the initial object. I think the library should be re-architectured to accommodate room for implementing this functionality.

1reaction
etc-tiagocommented, Aug 2, 2020

As much as I don’t see a use in my current cases, I believe the proposal is quite interesting. So my proposals for changes if you choose to implement them are:

Before implementation, separate otion into 3 packages:

  • otion-core - the logic behind the generation of classes
  • otion-injectors - with injectors in html
  • otion - the combination of otion-core and otion-injectors

After the separation, implement a fourth package, callend otion-compose with the proposed changes.

It could also be called @otion/core,@otion/css and @otion/combine if separation is an option.

why: I think the package size can increase considerably if everything is added in a package and otion would have two directions, one to generate classes as currently and the other to create combined classes

Tell me what you think about it.

Read more comments on GitHub >

github_iconTop Results From Across the Web

RFC 2660 - The Secure HyperText Transfer Protocol
DN-1779 Name Class The argument is an RFC-1779 encoded DN. 3.3.2. Certificate-Info In order to permit public key operations on DNs specified by ......
Read more >
rfcs/0013-composition-api.md at master · vuejs/rfcs - GitHub
The APIs proposed in this RFC provide the users with more flexibility when organizing component code. Instead of being forced to always organize...
Read more >
Update to the Language Subtag Registry - » RFC Editor
An "inverted" name is one that is altered from the usual English- language order by moving ... each of the following grandfathered tags...
Read more >
PHP RFC: Constants in Traits
Members of the same name usually have different entities for each class, but by specifying virtual at the time of inheritance, the entities...
Read more >
[RFC-427] Name the new WCS class lsst::afw::geom::SkyWcs - Jira
The new composable WCS class that replaces lsst::afw::image::Wcs and ... I chose this name to emphasize that this is a 2-dimensional celestial WCS...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found