Something I’ve not seen much and wanted to share, is how I’ve implemented layout properties in our product with bs-css.
This work was inspired by a react-europe conference that clearly explains the differences that should be made between the two categories of css properties. You can find it on youtube (https://www.youtube.com/watch?v=FNdByml3zr4).
The problem appears when you use css-in-js and create components with well defined APIs and only want to expose a very limited set of properties.
I’ll use a very basic and naive Alert component for example. This is how I use bs-css to define the design of the component:
module Styles = {
open Css;
let info = style([backgroundColor(blue)]);
let alert = style([backgroundColor(red)]);
};
[@react.component]
let make = (~type_, ~text) =>
<div className={type_ == "alert" ? Styles.alert : Styles.info}>
text->React.string
</div>;
When you want maximum composability, it’s better if the component doesn’t define margins, widths, flex properties, etc. It’s especially bad if you definer margins on your root jsx tag, defining spacing is the responsability of the container.
You then have two choices.
You can expose every layout property in your component API:
[@react.component]
let make = (~type_, ~text, ~marginTop, ~marginBottom, ~minWidth, ~maxWidth) => {
let style = ReactDOMRe.Style.make(~marginTop, ~marginBottom, ~minWidth, ~maxWidth, ());
<div style className={type_ == "alert" ? Styles.alert : Styles.info}>
text->React.string
</div>;
};
but it is very tedious. You can’t do that for every component and you can’t guess which property will be needed by the users of your component.
The other choice is to expose an additional className that will be merged with the internal one:
let make = (~type_, ~text, ~className) => {
let finalClassName = Css.merge([className, type_ == "alert" ? Styles.alert : Styles.info]);
<div className=finalClassName>
text->React.string
</div>;
};
But this time you can’t control what is published, and anything can be changed by user.
To solve that problem, what we did is to create a new module that is a subset of Css and only contains layout rules. This module is abstracted to create an incompatible type with the one from Css.
CssLayout interface:
// Subset that only contains Css properties related to layout organisation
type t;
// In the same spirit, a layoutRule type is a subset of the Css.rule type for layout only
type layoutRule;
// Create a layout style, use it like Css.style
let style: list(layoutRule) => t;
// Helper function that join a layout style with a Css style.
let join: (option(t), string) => string;
// Css layout properties, see Css.rei file
let visibility: [< Css.Types.Visibility.t | Css.Types.Cascading.t] => layoutRule;
let position: [< Css.Types.Position.t | Css.Types.Var.t | Css.Types.Cascading.t] => layoutRule;
let top: [< Css.Types.Length.t | Css.Types.Var.t | Css.Types.Cascading.t] => layoutRule;
let left: [< Css.Types.Length.t | Css.Types.Var.t | Css.Types.Cascading.t] => layoutRule;
let bottom: [< Css.Types.Length.t | Css.Types.Var.t | Css.Types.Cascading.t] => layoutRule;
let right: [< Css.Types.Length.t | Css.Types.Var.t | Css.Types.Cascading.t] => layoutRule;
let width: [ Css.Types.Width.t | Css.Types.Percentage.t | Css.Types.Length.t | Css.Types.Cascading.t] => layoutRule;
let minWidth: [ Css.Types.Width.t | Css.Types.Percentage.t | Css.Types.Length.t | Css.Types.Cascading.t] => layoutRule;
let marginTop: [ Css.Types.Length.t | Css.Types.Margin.t] => layoutRule;
let marginBottom: [ Css.Types.Length.t | Css.Types.Margin.t] => layoutRule;
// ...
CssLayout implementation:
type t = string;
type layoutRule = Css.rule;
let style = Css.style;
let join = (layout, className) =>
switch (layout) {
| None => className
| Some(layout') => className ++ " " ++ layout'
};
let important = Css.important;
let visibility = Css.visibility;
let position = Css.position;
let top = Css.top;
let left = Css.left;
let bottom = Css.bottom;
let right = Css.right;
let width = Css.width;
let minWidth = Css.minWidth;
let marginTop = Css.marginTop;
let marginBottom = Css.marginBottom;
// ...
As you can see, this is just aliases to the Css functions, but put in a module with an abstract type to make them incompatible with the Css.rule type.
With that new module in your hands, you can safely expose a layout poperty in every component you have:
[@react.component]
let make = (~type_, ~text, ~layout=?) => {
<div className={CssLayout.join(layout, {type_ == "alert" ? Styles.alert : Styles.info})}>
text->React.string
</div>;
};
And this is how you would use it:
module Styles = {
open Css;
let container = style([display(flexBox)]);
};
module Layouts = {
open CssLayout;
let alert = style([minWidth(200->Css.px), marginTop(10->Css.px)]);
};
[@react.component]
let make = () => {
<div className=Styles.container>
<Alert layout=Layouts.alert type_="info" text="info" />
// ...
</div>;
};
It works really well for us.
I’m interested to hear if you have other solutions.