kettanaito / atomic-layout

Build declarative, responsive layouts in React using CSS Grid.
https://redd.gitbook.io/atomic-layout
MIT License
1.13k stars 33 forks source link

Plans to support emotion? #122

Closed bitttttten closed 5 years ago

bitttttten commented 5 years ago

What:

Are there any plans to support emotion?

Why:

How:

I don't know yet :)

kettanaito commented 5 years ago

Hello, @bitttttten. First of all, thank you for reaching out.

I have no experience with emotion, so it's hard to estimate how it can be integrated into Atomic layout. The benefits you described above are quite evident, with most of them gained by the end developer, rather than the library directly.

The big picture is to find a way to abstract from a styling solution in general. People have different preferences over styling, and I would like Atomic layout to respect them. Initially, the idea was to modularize CSS-in-JS solution (#57), so that a developer may plug in any styling library, describe its API, and get Atomic layout working with their favorite tool. Later, I've discovered this would bring too much discrepancies and make certain features (like IE support) impossible to implement, due to variety and variability of styling solutions' implementations.

This topic is still broadly open for discussion, and I would appreciate your thoughts especially. My vision now leans forward Atomic layout emitting CSS, which the end developer would process during the build step (i.e. through webpack loaders). Why I find this beneficial:

I'm yet unaware how to achieve that on the library's level, but I haven't got deep into this topic. Hope this drops some light on your question.

bitttttten commented 5 years ago

I guess the tricky part is that there is no standard pattern for writing CSS-in-JS.

Modularizing may lead to having to support a ton of edge cases in Atomic library from people x or y CiJ framework with it and running into issues. Edge cases that you never encountered and may struggle to maintain. So there is overhead in this too.

Emitting CSS may clash with styles in the end developer's app already. How would you ensure someone is not overriding some styles from this library? And how would you ensure that Atomic layout is not overriding styles in the end developer's app? The benefit of CiJ libraries is that this clash never occurs. On first thought, this is the smoothest approach. However you will need to ensure that the end developer loads in Atomic layout CSS first before anything else.

Am I the first person to ask for an alternative to styled-components? If so, then maybe you do not need to cross this bridge until there is a bit more support for it? Just a thought.

I didn't realise you had put so much thought into it. I will dwell on it a bit and come back.

kettanaito commented 5 years ago

Exactly! The more discussion there is, the more it becomes evident that Atomic layout should, most likely, pick a side. I hate this, as I hate forcing opinions on developers.

For now, I treat the library in an alpha phase until stable API lands in 1.0. There is no harm to use a specific styling solution to demonstrate the library's purpose, but for the major release I would like to find a meaningful way for people to use it in their apps, where different CiJ may be used.

Yes, you are the first person. Yet I won't base this on statistics, as the usage percentage of the library is quite small.

To give you a more technical insight, this is what I meant by pluggable styling solution:

import { css } from 'emotion'
import Layout from 'atomic-layout'

Layout.configure({
  produceStyles: (params) => {
    return css(params)
  }
})

The syntax is a draft.

A single thing that is in common of all CiJ is that they operate on CSS. Whether it's an Object, or a template string, the input is still CSS, just differently formatted. I see no technical difficulty to ship a "raw" CSS from Atomic layout, and let the end developer specify how certain parts of styles generation should be performed, according to their styling solution.

You are right, that this would mean that the end developer is responsible for a proper functioning of those styles generating functions. Unless Atomic layout just ships pre-defined plugins, ensuring it works with them:

import Layout from 'atomic-layout'
import { withEmotion } from 'atomic-layout/styles'

Layout.configure({
  produceStyles: withEmotion,
})

This would still make Internet Support impossible, but I think it's not going to happen anyway.

What do you think about the pluggable proposal? Do you see some pitfalls?

bitttttten commented 5 years ago

That's nice. I can see this working really well.

I think the pluggable approach is perfect, however since atomic-library is using string styles, it may not be possible to support all CiJ libraries that defer from this "convention" (if it is one :P). emotion and styled-components both accept string styles which is fine, but for example Aphrodite only accepts object styles (this is their API) and uses a completely different syntax altogether. It's kinda lucky that emotion and styled-component are the 2 most popular and both use string syntax, since it'll be easier to plug either of them in. Maybe it's only possible to support CiJ libraries that support string styles.

Another thing to note is the preprocessing. emotion and styled-components are using the same thing which is stylis (emotion uses a modified version of stylis) and styled-components just uses it normally as far as I last checked. But again Aphrodite is using something else which is inline-style-prefixer. I think this is not a big deal but an issue could arise where one libary does not correctly autoprefix something and may break a layout somewhere. However, I guess that is on the library that the user chooses to plug into atomic-layout and not on atomic-layout 🤔

kettanaito commented 5 years ago

Yes, the pre- and post-processing of the emitted CSS is the end developer's responsibility. As long as Atomic layout returns valid CSS string/object, it's up to their processing pipeline to ensure prefixing doesn't break the layout.

I don't think adopting the CSS output for different APIs is even necessary. String format is far more universal, and can be reduces to an Object, if needed. Some function may first transform a string into an Object supported by the library A, and then supply it to that library's interface.

I really like your input, and would like to thank you for this discussion! :) If you are interested, I would be grateful for your help with #57, as we have already discussed a lot related aspects. We can try to ship a pluggable CiJ API to make you and other developers enjoy emotion in conjuncture with Atomic layout. That would be incredible.

bitttttten commented 5 years ago

Okay, so I made a little MVP just for class names. Mainly to play around and also get to know the library a bit better.. nothing here is concrete! But it "works".

I think, in an ideal world it would be the components that are configurable, and not just how class names are generated. But it is a simple approach just to get an MVP.

Box.tsx:

import * as React from 'react'
import { GenericProps } from '../const/props'
import applyStyles from '../utils/styles/applyStyles'
import Layout from '../Layout'

export interface BoxProps extends GenericProps {
  [propName: string]: any
  flex?: boolean
  inline?: boolean
}

export default function Box(props: BoxProps) {
  return (
    <div
      {...props}
      className={
        [
          props.className,
          Layout.produceStyles(applyStyles(props)),
          Layout.produceStyles`
            display: ${(({ flex, inline }) => flex && (inline ? 'inline-flex' : 'flex'))(props)};
          `,
        ]
        .filter(Boolean)
        .join(' ')
      }
    />
  )
}

Since this approach is now react agnostic, we have to call these functions with props ourselves. Also I am managing combining class names myself here with filter/join, this is just a WIP.

diff --git a/src/const/defaultOptions.ts b/src/const/defaultOptions.ts
index b7a66bd..f41e65c 100644
--- a/src/const/defaultOptions.ts
+++ b/src/const/defaultOptions.ts
@@ -16,7 +16,7 @@ export type BreakpointBehavior = 'up' | 'down' | 'only'
 export interface Breakpoints {
   [breakpointName: string]: Breakpoint
 }
-export interface LayoutOptions {
+interface LayoutOptions {
   /**
    * Measurement unit that suffixes numeric prop values.
    * @default "px"
@@ -38,6 +38,17 @@ export interface LayoutOptions {
   defaultBehavior: BreakpointBehavior
 }

+interface UserOptions {
+  /**
+   * This will accept styles as a template literal and return a class name.
+   * It assumes generating StyleSheets or rendering styles to the document
+   * is handled by the function provided.
+   */
+  produceStyles?: (...args: any) => string
+}
+
+export interface Options extends LayoutOptions, UserOptions {}
+
 export interface Breakpoint {
   /* Index signature for dynamic breakpoint composition */
   [propName: string]: any
diff --git a/src/Layout.ts b/src/Layout.ts
index 0a723e1..84af381 100644
--- a/src/Layout.ts
+++ b/src/Layout.ts
@@ -1,21 +1,21 @@
 import defaultOptions, {
-  LayoutOptions,
+  Options,
   Breakpoint,
 } from './const/defaultOptions'
 import invariant from './utils/invariant'

 class Layout {
-  public options: LayoutOptions = defaultOptions
+  public options: Options = defaultOptions
   protected isConfigureCalled: boolean = false

-  constructor(options: Partial<LayoutOptions>) {
+  constructor(options: Partial<Options>) {
     return this.configure(options, false)
   }

   /**
    * Applies global layout options.
    */
-  public configure(options: Partial<LayoutOptions>, warnOnMultiple = true) {
+  public configure(options: Partial<Options>, warnOnMultiple = true) {
     if (warnOnMultiple) {
       invariant(
         !this.isConfigureCalled,
@@ -76,6 +76,14 @@ class Layout {

     return
   }
+
+  /**
+   * Returns the function from options that will eventually
+   * return a class name from styles as a template literal.
+   */
+  public get produceStyles() {
+    return this.options.produceStyles
+  }
 }

 export default new Layout(defaultOptions)

Here I just made a generic Options type, and merged Default and a new User options object into it. Also I just used any since it's an MVP 🙈 I added a getter for productStyles on Layout so it's possible to do Layout.produceStyles.

diff --git a/stories/index.js b/stories/index.js
index 451dea3..2947f1e 100644
--- a/stories/index.js
+++ b/stories/index.js
@@ -1,6 +1,13 @@
 import React from 'react'
 import { storiesOf } from '@storybook/react'
 import './styles.css'
+import Layout from '../lib'
+import { css } from 'emotion'
+
+Layout.configure({
+  produceStyles: (styles, ...rest) => css([styles], ...rest)
+})

 import Playground from './Playground'
 import Demo from './Demo'

And here is the usage. The syntax is pretty ugly since now I have to handle the output from the template literal. Plus, running code inbetween imports feels dirty :( Plus, I trigger warnOnMultiple now.

kettanaito commented 5 years ago

I'm closing this in favor of #57. Thank you for bringing this topic up.

kettanaito commented 4 years ago

Official Emotion support

Atomic Layout now distributes a dedicated package that supports Emotion, called @atomic-layout/emotion.

$ npm install @atomic-layout/emotion @emotion/core @emotion/styled