solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.
https://solidjs.com
MIT License
32.09k stars 913 forks source link

css-in-js options? #102

Closed praneybehl closed 4 years ago

praneybehl commented 4 years ago

This is a great library and builds on the shortcomings of some big libraries out there. Absolutely prefer DOM library over virtual DOM implementation. Thanks for this great effort. 💯

I have a couple of questions:

Thanks

ryansolid commented 4 years ago

Sure it's document.createElement.

To be fair there is no exact equivalent. Solid doesn't use HyperScript so there was no need to write a generic createElement method. In fact, it's more efficient to clone a whole template of nodes and then walk the tree to apply the dynamic expressions. So to start we are dealing with fairly different things. For that reason, while I've used CSS Modules etc I doubt you will find an existing Styled CSS library that will just work. I know people have used emotion but not the styled part.

For the purposes here though maybe this would be sufficient:

import { spread } from "solid-js/dom";

function createElement(tag, props) {
  const el = document.createElement(tag);
  spread(el, () => props);
  return el;
}

However, the difference in update cycle may not let this be a direct port. I don't see anything from the syntax to suggest that this couldn't be done.

ryansolid commented 4 years ago

Actually it was really easy to do a basic form with emotion. Look at this: https://codesandbox.io/s/solid-emotion-slz4d

import { css } from "emotion";
import { createEffect } from "solid-js";
import { spread } from "solid-js/dom";

export default function styled(tag) {
  return (template, ...args) => {
    return props => {
      const el = document.createElement(tag);
      createEffect(() => {
        const c = css(
          template,
          ...args.map(i => (typeof i === "function" ? i(props) : i))
        );
        el.className = c;
      });
      spread(el, props);
      return el;
    };
  };
}

Thanks for encouraging me to look into this. It's pretty cool.

praneybehl commented 4 years ago

Brilliant! This will get me cracking, will post progress. Thanks

atfzl commented 4 years ago

@praneybehl you can directly use emotion like here: https://github.com/atfzl/solid-starter/blob/1df0a709bba140f1e2ee4d6ca6f5d4c81f437ac6/%23/components/task/task.component.tsx#L16

ryansolid commented 4 years ago

Now thinking about CSS in JS. I don't want to pick sides but none of the Styled Component libraries will work with Solid out of the box since it's not HyperScript. Stuff like theming as well will have to be Solid specific. Styled Components can work as I've demonstrated but actually are inefficient for Solid as they break apart templates. So I've been looking at the css prop implementations. Also completely incompatible. I debated building into the compiler but it requires Solid specific knowledge to handle context.

The preferred form with Solid would be in binding expressions rather than wrapping with styled components. If you wanted to lift dynamic ones out you'd need to wrap in a function:

const MyComp = props => {
  const divCSS = () => css`color: ${props.color}`;

  return <div>
    <div class={divCSS()} />
  </div>;
}

Still could support styled components and they are fine for wrapping existing Components.

Have any of you seen: https://github.com/cristianbote/goober I haven't used other libraries to see if full-featured enough but seems promising and small.

I wasn't sure if I wanted to get in here, but I could build a Solid compatible lib on top that would be fast and small (sub 1kb). Would that be interesting?

r0skar commented 4 years ago

I´d very much like to have a "native" option for Solid.

I have tried various Css-in-JS libs with Solid (based on your Codepen Demo), but I ran into a few issues (i.e. https://github.com/ryansolid/solid/issues/115). I did not try goober yet, but have been using typestyle and emotion. I'd suggest using objects for styles instead of template strings. Having used both methods, I had a much better experience with objects (even more so when using TS).

ryansolid commented 4 years ago

Yeah I've been trying to brainstorm what the best form would be. As I said styled is actually not great for Solid mostly that it splits templates apart into Components unnecessarily which is an overhead on performance and spreading on dom children is inefficient as it can't be statically analysed. Which is sort of why we've seen bugs in that area recently. I never use spread on native DOM elements if I can avoid it as it basically requires importing the hyperscript version of Solid in so that it can at runtime figure out how to handle all the attributes. So I haven't been as vigilant testing there. Sure it's not that bad as all VDOM libraries take those hits but if its avoidable.

Styled Components are also awkward from a Component wrapping standpoint because merging combining them HOC style requires cloning the props since we still need to access the parent which may or may not be wrapped in deferred getter. So unless we knew the dynamic props (which in theory could be passed into Component functions I suppose) we don't know which to rewrap and which to access directly. Unfortunately a naive cloning approach would access the values prematurely not allowing them to be tracked by downstream computations.

The natural thing I think would be to handle it as a css prop, but I don't know how generalizable that is. SSR, and Web Components come to mind as they need different style targets then the head. Most libraries handle this but an automated compiled system makes it awkward. And we don't get much benefit out of the compiler beyond having a well defined css prop. I suppose a simple babel plugin could do a transformation. But JSX types kind of drive me crazy. I'm sort of worried about creating a sort of hidden system where by opting in you need to import other libraries etc.

Finally it looks like everyone has a preference on their library. I wonder if it would be better to do something like one time:

import { configure } from "solid-css";
import { css } from "the-library-of-your-choice";

configure({ css }) // use this as the basis wrapped by Solid specific approaches

Elsewhere grab the methods configured globally:

import { css, styled, ThemeProvider } from "solid-css";

It is just if I don't do styled is there much advantage to this? Does a solution have to offer styled just for convenience even if it is less than optimal? Anyone can create a ThemeContext and css doesn't need it. I don't know. Does any of this make sense? The more time I think about it, the less it seems beneficial. Mostly in that maybe the patterns that make the most sense with Solid are the ones that are already easy to do. Like import { css } from "emotion" or import { css } from "goober". Done.

What would any of you like to see in a solution? Am I missing something?

ryansolid commented 4 years ago

I think that a Babel plugin that acts as a JSX preprocessor is probably the best choice. I could make it an option for Solid's compiler but I am thinking it's more general purpose than that. Basically it compiles the css prop to css function and adds the import. I figured this already existed but all could find is one that converts to styled Components or non universal approaches. What is cool about this approach is it should work with any library and support whatever syntax the library provides and work against any JSX framework assuming className is the prop used (although that could be configurable). Youd just use it like:

<div css={`background-color: ${props.color}`} />

<MyComp css={`background-color: ${props.color}`} />

// or if lib accepts objects
<div css={{backgroundColor: props.color}}/>

I honestly thought something like this already existed but I haven't found it so if I still cant find it maybe such a plugin will have value. I will eval during the process if there is any benefit to making it specific to Solid but I don't see it right now.

ryansolid commented 4 years ago

That approach promotes awkward css patterns though. Mostly that it promotes inlining which can get ugly when there is too much. And if there is too little per block you might be underutilizing CSS. I have found in React Native you end up producing about 30-40% more style LOCs with a non-cascading approach. Still Styled Components aren't a good fit. I think my current favourite approach is basically fix CSS Modules by allowing JS to be passed in:

// Comp.js
import useStyles from "./Comp.styles"

export default function Comp(props) {
  const styles = useStyles(props);
  return <div class={styles.background} /> 
}

// Comp.styles.js
import { useContext } from "solid-js";
import ThemeContext from "../context/ThemeContext"
import { css } from "emotion"

export default (options) => {
  const theme = useContext(ThemeContext);
  return {
    get header() { return css`background-color: ${theme.primaryColor}` }
    get background() { return css`background-color: ${options.color}` }
  }
}

It's clean in that the input types could be easily set, still can support different libraries, and it keeps the templates pure (doesn't break them apart or cloud them with too many inline styles). The problem is who wants to write that Comp.styles.js file.

I could just leave it like that. Now I don't think it's my place to introduce a new paradigm here but what if you could take a normal css stylesheet with interpolation like ${ } and generated Comp.styles.js via a compiler. Sounds convoluted a bit but essentially you write normal CSS (plus templating) that generates class identifier JSON that interpolation feeds in a CSS in JS library to be dynamic. It feels like CSS modules but it's dynamic. Like pretend instead you have a Comp.cssx file that looks like:

.header {
  background-color: ${theme.primaryColor}
}
.background {
  background-color: ${color}
}

And that generates it. Might be hard to type I suppose, but there is just something that the current JS in CSS solutions don't do for me and actually aren't beneficial to how Solid works.

ryansolid commented 4 years ago

I improved the Styled Emotion example(https://codesandbox.io/s/solid-emotion-slz4d) in the code sandbox considerably for Components. Now it properly concats nested classNames when you style the same element multiple times. It is also a simpler implementation. And I figured out a way to clone reactive props without triggering them. So all net positive.

However, it does reveal another issue. Specificity. These libraries add classes as they generate them which means they typically are in the wrong order in the stylesheet for specificity. The wrapper doesn't overried the base. It made me wonder why they don't just reverse them by default to start, But it means there needs to be more to it than this.

Emotion React implementation doesn't resolve classes until it applies them on the native element and instead uses babel to compile the styles down to JSON (AST-like) objects that can be merged until final serialization. I am not sure how other solutions handle this.

ryansolid commented 4 years ago

Emotion actually has a cx helper that does the trick for specificity as it merges classes. Obviously emotion specific but it means I think the emotion example is pretty much fully functional now. cloneProps will be in the next version of Solid.

ryansolid commented 4 years ago

I have a working version with specificity with Goober now too: https://codesandbox.io/s/solid-goober-n7dgb.

The takeaway so far is that it seems while css in generalizable into Styled Components each library has their own approach to specificity. So my idea of a universal wrapper and bring your own css seems unlikely. It will probably take specific wrappers for each library, which essentially boils down to picking a reasonable one as a base if styled is a consideration. I still feel like there should be other options more geared to Solid. A multi css that could export a function so it can be used like a "hook". The challenge, of course, is to make sure that the returned class properties wrap the reactive context properly, which is what made me lean towards compilation but maybe there are other options.

ryansolid commented 4 years ago

I'm thinking so far the best pattern for Solid I've found so far styled-jsx. It is the library used by Next.js but the approach is congruent with how Solid works. It is compiler-based and doesn't mess with the DOM structure. It has runtime but not significant so it would be easy to incorporate. Of course, it has no TypeScript capability since well all classes are just strings, there is nothing to type. Personally, I really like the approach as it keeps the JSX clean and reminds me of Web Components.

It has the upside of CSS modules in that you can use preprocessors and write straight CSS. You can declare global styles similar way. When the blocks are static Solid's compiler will be smart enough to not even wrap it and there will be no overhead after the initial render. It has the upside of a library like Emotion as you can write as many blocks as makes sense and can dynamically update styles with JavaScript bindings. No bundler needed/bundler agnostic as it uses babel.

Obviously I can't use styled-jsx as it was written for React but the babel part could almost be adopted as is. It feels like a first-class type of solution. And nothing stops using any other CSS in JS approaches.

praneybehl commented 4 years ago

Sorry for being late to the party but wow there is so much brain storming here. I personally like the styled components approach although I totally agree with your take on performance in favour of styled-system.

ryansolid commented 4 years ago

I mean using Styled Components especially to style existing Components is perfectly viable. Both the examples, using emotion or goober are perfectly functional and would do the job just fine. I imagine most css in js libraries that implement a css function would be similar.

And by no means using this approach is that bad for performance. Most UI libraries make DOM Elements one at a time, including React, Vue, Svelte, Inferno,etc.. Solid like lit-html optimizes multi node cloning. This has a noticeable advantage on initial creation of elements but has no impact on update or teardown. So I mean no one is complaining about Inferno's performance. It's ok. If you have a library of your choice(except I guess Styled Components itself which is React specific) you want to see ported into styled and are having issues let me know and I should be able to help.

I have been looking into styled JSX and it has more differences than I first noticed. Mostly around Reacts constant rerender. The positive is for dynamic cases I think I can optimize an approach like this further. But I need to basically write it from the ground up. Which is probably for the better but just takes some doing.

praneybehl commented 4 years ago

Thanks Ryan, I have a new project starting and I am planning to give Solid a spin. Also interested in contributing where I can.

matthewharwood commented 4 years ago

@ryansolid

... I need to basically write it from the ground up. Which is probably for the better but just takes some doing.

Here's me cheering you on!
Also, If you're looking for any inspiration, I really like Styletron's React API: useStyletron().

high1 commented 4 years ago

@ryansolid Thanks for goober, didn't know about it. Looks awesome. I was messing around with running solid on snowpack, without bundling, and one minimal solution that worked was simplestyle. It works, but I'd like to know your thoughts - does it fit with solid?

ryansolid commented 4 years ago

@high1 it does fairly well. Biggest thing it is doesn't break up the view template the way styled components does. The only thing is for dynamic code you'd need to wrap in a function or a createMemo since I imagine the hash changes. So it's really just a granularity issue. Should it wrap and re-evaluate the shole style tag or just a class at a time. Of course, you can probably run createStyles as many times as you want breaking things down as they make sense.

high1 commented 4 years ago

Thanks. I took a look at goober, and while I like it, I had various typing issues. Typings for styled function

styled<T = {}>(tag: string | StyledVNode<any>): Tagged<T & { children: any }>

require children - I think children should be optional, `cause there could be none. Also tried to type the styled function from your goober sample, but failed to make it work with

font-weight: ${(props: { [key: string]: string }) => (props.bold ? 'bold;: 100)}

typescript just doesn't expect the function here, and my hunch is that the definitions are coming from the goober side...

ryansolid commented 4 years ago

Oh I suspect I know what it is. I'm using the css function which doesn't support functions (as it wouldn't make sense). However as the basis of the the library to support styled it technically does. The library injects behavior by binding css. I think we'd need to override the Types for styled.

high1 commented 4 years ago

I finally came up with something like this:

export default function styled<T extends Record<string, unknown>>(
  tag: string | StyledVNode<unknown>
) {
  return (
    inner: CSSAttribute | TemplateStringsArray | string,
    ...rest: Array<string | number | ((props: Partial<Record<string, unknown>>) => string | number)>
  ) => {
    return (props: T & { className?: string }): string => {
      const newProps = cloneProps(props);
      Object.defineProperty(newProps, 'className', {
        get() {
          const pClassName = props.className,
            append = 'className' in props && /^go[0-9]+/.test(pClassName as string);
          // Call `css` with the append flag and pass the props
          const className = css.apply({ o: append, p: props }, [
            inner,
            ...rest.map(prop => (typeof prop === 'function' ? prop(props) : prop)),
          ]);

          return [pClassName, className].filter(Boolean).join(' ');
        },
        configurable: true,
        enumerable: true,
      });

      let el;
      if (typeof tag === 'function') {
        el = tag(newProps);
      } else {
        el = document.createElement(tag);
        spread(el, newProps);
      }

      return el;
    };
  };
}
const Div = styled<Partial<{ bold: boolean; border: number; color: string }>>('div')`
  color: steelblue;
  font-size: 32px;
  padding: 5px;
  border: ${({ border = 1, color = 'gainsboro' }): string => `${border}px solid ${color}`};
  background-color: linen;
  font-weight: ${({ bold = false }): string | number => (bold ? 'bold' : 100)};
`;

and this

<Div className="test" bold={true} border={1} color="whitesmoke">
  Testera
</Div>

becomes valid then.

ryansolid commented 4 years ago

So apparently I can use styled-jsx more or less as is if I change the imported component. This is pretty cool. Check it out: https://codesandbox.io/s/solid-styled-jsx-example-xgx6b

ryansolid commented 4 years ago

@high1 your code snippets were very helpful. I think it's finally safe to close this issue. I have now publish 2 CSS in JS libraries for Solid. A Styled Component one based on goober solid-styled-components and a wrapper for Zeit's styled-jsx solid-styled-jsx. So regardless of what approach you like, I think we have you covered.

zhaoyao91 commented 3 years ago

is there any css solution suitable for solid-element with shadow dom? that is, the generated css will go to the style tag hosted in the nearest shadow root automatically.

nmrshll commented 3 years ago

Just linking to the two libs you created for anyone reading down to here: https://github.com/ryansolid/solid/tree/master/packages/solid-styled-components https://github.com/ryansolid/solid/tree/master/packages/solid-styled-jsx

holbrookdev commented 2 years ago

New links: https://github.com/solidjs/solid-styled-components https://github.com/solidjs/solid-styled-jsx

dhanielsales commented 1 year ago

Other option for CSS-in-JS with zero runtime approach is https://macaron.js.org/ :)

ndrean commented 1 year ago

One CSS-in-JS implementation trial which does not use Goober: https://github.com/ndrean/solid-css

lominming commented 5 months ago

is there any css solution suitable for solid-element with shadow dom? that is, the generated css will go to the style tag hosted in the nearest shadow root automatically.

I know this is a super old thread. I am looking for exactly what you described. Any luck finding a solution?

sekoyo commented 2 months ago

Other option for CSS-in-JS with zero runtime approach is https://macaron.js.org/ :)

That's cool that it has official Solid and Solid Start support. zero runtime ftw 👌