Styled Components Possible?

reasonreact

#1

We use styled components at my work. The reason we like it above other solutions (at least speaking for myself) is:

You don’t need to provide class names, it just generates a component

  const Button = styled.a`
      width: 11rem;
      background: transparent;
      color: white;
  `
 // later

 const myComponent = () => (<div><Button /></div>);

Also, you can make it dynamic very easily

  const Button = styled.a`
      width: 11rem;
      background: ${{color} => color ? color : 'transperent' };
      color: white;
  `

This desugars to:

  const Button = styled.a([
       "width: 11rem; background:",
      "; color: white;"
    ], 
    ({color}) => color ? color : 'transperent' 
 )

Also, you can reference other components like so:

  const Link = styled.a`
    background: papayawhip;
    color: palevioletred;
  `;

  const Icon = styled.svg`
    ${Link}:hover & {
      fill: rebeccapurple;
    }
  `;

This seems like it’ll be really hard to type, I’m not sure if I can use Functors because components are usually modules in ReasonReact. I’m just wondering if anyone has any other ideas on how to make this kind of API in a sane way in Reasons’ type system.

Also, I’m assuming there’s not really tagged template literals in Reason as well…


#2

bss-css might interest you.
It is binding for glamor (not styled-component, but looking like)


#3

This is pretty darn cool. I think I might be able to convince people to use it, however when you introduce a DSL (instead of using CSS pretty directly as StyledComponents does) it can make it harder for people to adopt. The other thing I dislike is that it doesn’t create the component for you. There’s a lot less visual noise when you can do <Button> rather than <div className={Styles.button} />.

I guess to have more directed question, Is there way to generate ReasonReact components using a function call?


#4

The benefit of DSL is that statically typed.

Yes it possible to generate a ReasonReact components using bs-css
But I don’t recommend it, because reason, there is no property spread, and it might be verbose or not safe.

Example:

module CreateComponent = (Config: {
  let tag: string;
  let style: Css.t;
}) => {
  let component = ReasonReact.statelessComponent("createcomponent");
  let make = (~onClick, ~className="", children) => {
    ...component,
    render: (_) =>
      ReasonReact.createDomElement(
        Config.tag,
        ~props={"className": Css.style(Config.style) ++ " " ++ className, "onClick": onClick},
        children,
      ),
  };
};

module MyButton = CreateComponent({
  	let tag = "button";
	let style = Css.([margin(px(10))]);
});

<MyButton className="extra" />

You should list all available html properties in props of CreateComponent


#5

I’d personally stray away from injecting vanilla CSS in there, since you lose the ability to use the language features and safety to build up your styles.

What you’d want instead is a nicely typed DSL that interprets down to the objects React would use to compile to CSS.

Then CSS becomes an implementation detail and migrating your app to becomes switching that interpreter.


#6

@maarekj Wow that code example is pretty darn awesome. I think there’s a way that we could basically steal the type signature of a div and use that in that functor. That’s sweet.

@ostera: I actually would love to (long term) make it so that I can make a PPX transformation like the graphql one that would basically transform CSS into Reason objects at compile time then (fingers crossed) have statically typed CSS, while typing real css. However even @maarekj’s solution is pretty darn good.


#7

The solution that I proposed is incomplete and presents some cons.
I will detail it tomorrow


#8

Yeah I would be interested in that. I’ve often wondered if making Comonents at the module level is a good idea :thinking:. Maybe the problem is not related to that though.


#9

Here is a better snippet:

module CreateComponent =
       (
         Config: {
           let tag: string;
           type params;
           let defaultParams: params;
           let style: params => list(Css.rule);
         },
       ) => {
  external propsToObj : ReactDOMRe.reactDOMProps => Js.t({..}) = "%identity";
  let mergeProps = (a: ReactDOMRe.reactDOMProps, b: ReactDOMRe.reactDOMProps) =>
    ReactDOMRe.objToDOMProps(Js.Obj.assign(propsToObj(a), propsToObj(b)));
  let component = ReasonReact.statelessComponent("createcomponent");
  let make =
      (
        ~tag=Config.tag,
        ~params=Config.defaultParams,
        ~className="",
        ~props: ReactDOMRe.reactDOMProps=ReactDOMRe.props(),
        children,
      ) => {
    ...component,
    render: (_) =>
      ReasonReact.createDomElement(
        tag,
        ~props=
          propsToObj(
            mergeProps(
              props,
              ReactDOMRe.props(
                ~className=Css.style(Config.style(params)) ++ className,
                (),
              ),
            ),
          ),
        children,
      ),
  };
};

type params = {
  variant: [ | `primary | `secondary],
  size: [ | `sm | `md | `lg],
};

module Button =
  CreateComponent(
    {
      let tag = "button";
      type nonrec params = params;
      let defaultParams = {variant: `primary, size: `md};
      let variantRules =
        fun
        | `primary => [Css.(background(hex("00FF00")))]
        | `secondary => [Css.(background(hex("0000FF")))];
      let sizeRules =
        fun
        | `sm => Css.[margin(px(10))]
        | `md => Css.[margin(px(12))]
        | `lg => Css.[margin(px(15)), padding(px(20))];
      let style = params =>
        Css.merge([sizeRules(params.size), variantRules(params.variant)]);
    },
  );

let primarySm =
  <Button
    params={variant: `primary, size: `sm}
    props=(ReactDOMRe.props(~onClick=event => Js.log(event), ()))
  />;

let secondaryLarge =
  <Button
    params={variant: `secondary, size: `lg}
    props=(ReactDOMRe.props(~onClick=event => Js.log(event), ()))
  />;

let secondaryLargeInput =
  <Button
    tag="input"
    params={variant: `secondary, size: `lg}
    props=(ReactDOMRe.props(~_type="button", ~onClick=event => Js.log(event), ()))
  />;