Hi π
I'm a software engineer based in Barcelona working with Reason/OCaml and exploring the world of functional programming. I also co-host emelle.tv, a talk show about these languages.
Check my site sancho.dev.
Type-safe styled components for ReScript, Melange and native with type-safe CSS
Home Page: https://styled-ppx.vercel.app
License: BSD 2-Clause "Simplified" License
Hi π
I'm a software engineer based in Barcelona working with Reason/OCaml and exploring the world of functional programming. I also co-host emelle.tv, a talk show about these languages.
Check my site sancho.dev.
Emotion supports inject global styles, we can support similar API:
[%styled.global
html, body {
margin: 0;
}
];
module Component = [%styled
(~size) => {
if (size > 0) {
"display: flex"
}
}
];
module Component = [%styled
(~size) => {
if (size > 0) {
"display: flex"
}
}
(~kind) => switch (kind) {
| Solid => "padding: 10px"
| _ => "padding: 2px 4px
}
];
[%styled ""]
[%styled]
Basic media queries are supported.
There are a few values that aren't
@media all and (max-device-width: 218px)
and (max-device-height: 281px)
and (orientation: portrait) {
color: green;
}
Also, a few other notes: You should not use Obj.magic without specifying the type on iether side. Not doing so breaks the type system.
And you should probably not be using @bs.deriving abstract. That generates a lot of crap you probably do not want, and has some limitations as well. Defining plain object types and using ordinary object creation would be a lot simpler and more flexible.
With one caveat, @bs.derving abstract is better in one case, when you want users to be able to specify field names that are invalid identifiers in Reason. But that benefit falls away when you write a PPX, becasue you're not restricted by the syntax.
Or the lexer, at least.
You stilll do. Js.Obj.assign will restrict it to a Js.t object, but nothing more than that. It still also only constrains one side of it.
Right, yeah I think that might be unavoidable, regardless of method.
Because not speficying an optional argument should omit the field entirely, not just set it to undefined
The only way I found is doing the Obj.magic ofc
Which only works because it breaks the type system.
As does using Js.Obj.assign btw.
They can be okay to use internally, but you should constrain the types on either side externally
Otherwise all bets are off.
And I don't think you can do so here, unless you require the consumer to annotate both the input and output type of the props.
If you don't it'll be completely valid to have an input props type of Js.t({. apples: int }) and an output props type of Js.t({ pears: string })
Both are objects, but otherwise they need have no realtion to each other
Using assign to make a copy is fine, but you still shouldn't need Obj.magic. assign will take any Js object. Obj.magic should be used with extreme care, it's not a band-aid to be slapped on whenever the type system complains.
And personally I don't trust code that uses Obj.magic without being extremely explicit about what it does.
Which means I can't really help with any other type-related issue that touches this code, since Obj.magic breaks all the rules.
As does Js.Obj.assign to some extent. That should be extremely explicit as well.
assign and magic don't turn the type into anything. They just ignore it and let the compiler infer whatever it wants for either side.
You can restrict that if you annotate the types though, but you need to generate the types for annotation.
let newProps = Js.Obj.assign(Js.Obj.empty(), Obj.magic(props));
I don't udnerstand the purpose of Obj.magic here. assign will accept any object, and in this case it just creates a copy anyway, which is rerpesentable in the type system
This is where you actually change it:
Js.Obj.assign(newProps, stylesObject),
So you want the return type to be annotated as the combination of the type of newProps and stylesObject.
The type system doesn't support types being combined like that, but if you have the AST for both types you could generate it syntactically.
Like, if you have
type props = {. href: string };
type stylesObject = {. color: Emotion.Color.t };
you could mash them together using ppx magic to get
type combined = {. href: string, color: Emotion.Color.t };
Note on discord:
@Alexander I'm not looking at a utility-class based styling for mainly 2 reasons: First learning curve and second because it goes against type-inferred languages mentality.
Since you are allowed to define types once and not care where those data structures go, I find myself using bs-emotion directly trying to play arround types and values too often
I'm very awrae that CSS it's too flexible, but trying to fix it with functions makes writing styles a cumbersome.
Right, but maybe for simple css definitions works ok, but for background-gradient, keyframes, or even border-radius becomes much much harder.
and I'm sure that large frontend apps solve the problem of scalabilty doing some design system/UI/some sort of API-based not a CSS approach.
Hi @davesnx. Thanks for sharing this work.
I am trying to run the demo unsuccessfully.
I have cloned the demo from this repo and followed the steps from the readme after running it out of the box did not work.
Here is the reproduction: https://github.com/idkjs/styled-ppx-master-demo
I have run yarn global add @davesnx/styled-ppx @ahrefs/bs-emotion
and added "ppx-flags": ["@davesnx/styled-ppx/styled-ppx", "@ahrefs/bs-emotion"]
to bsconfig.json
.
Upon running yarn run re:watch
I get the following compiler error which I am not able to resolve:
β styled-ppx-master-demo [master]yarn run re:watch
yarn run v1.22.0
warning package.json: No license field
$ bsb -make-world -w
bsb: no work to do.
bsb: no work to do.
bsb: no work to do.
>>>> Start compiling
[1/3] Building src/index.reast
FAILED: src/index.reast
/Users/prisc_000/Downloads/styled-ppx-master-demo/node_modules/bs-platform/lib/bsc.exe -w -30-40+6+7+27+32..39+44+45+101 -color always -bs-jsx 3 -ppx /Users/prisc_000/Downloads/styled-ppx-master-demo/node_modules/@davesnx/styled-ppx/styled-ppx -ppx /Users/prisc_000/Downloads/styled-ppx-master-demo/node_modules/@ahrefs/bs-emotion -o src/index.reast -bs-syntax-only -bs-binary-ast /Users/prisc_000/Downloads/styled-ppx-master-demo/src/index.re
sh: /Users/prisc_000/Downloads/styled-ppx-master-demo/node_modules/@ahrefs/bs-emotion: is a directory
We've found a bug for you!
/Users/prisc_000/Downloads/styled-ppx-master-demo/src/index.re
Error while running external preprocessor
Command line: /Users/prisc_000/Downloads/styled-ppx-master-demo/node_modules/@ahrefs/bs-emotion '/var/folders/dz/xzttny_14gs2lgxcvl5lm5680000gn/T/camlppx947ec0' '/var/folders/dz/xzttny_14gs2lgxcvl5lm5680000gn/T/camlppx74f477'
FAILED: subcommand failed.
>>>> Finish compiling(exit: 1)
Any guidance would be appreciated. Thank you.
This maps directly to unsafe https://github.com/ahrefs/bs-emotion/blob/master/bs-emotion/src/Emotion.re#L478
Look for people who have been asking on Discord & Twitter about styled-components/emotion bindings or any CSS in ReasonML.
Instead of snapshotting the component as now, would be great to snapshot the properties that emotion gives back.
Thanks to https://github.com/emotion-js/emotion/tree/master/packages/jest-emotion that's possible.
Once we finish I#26, we will need to add support to pass props to those components
Make all the changes to have this fs:
Try to setup "semantic-release" & "validate-commit-msg"
This is the umbrella issue of CSS property support.
This is the first chunk of properties that are half-supported and I think are a minimum to create any sort of styles.
Supporting those properties correctly we might unlock many more, like supporting opacity
you support: stroke-opacity
stop-opacity
flood-opacity
fill-opacity
.
animation
box-sizing
box-shadow
text-shadow
transform
transition
font-family
font-weight
border-top-right-radius
border-top-left-radius
border-bottom-right-radius
border-bottom-left-radius
background-position
transform-origin
flex
border
outline
grid
grid-template-area
There are some of them that might be tricky and some very straight forward.
The initial idea of theming is to allow users to specify minimum design values that are shared across all components, so can be consumed.
styled-components/emotion have solved that with ThemeProvider and theme object is there under props.theme
and all the logic of context is under the lib.
const theme = {
fg: "palevioletred",
bg: "white"
};
const Button = styled.button`
color: ${props => props.theme.fg};
border: 2px solid ${props => props.theme.bg};
`;
Note. Currently bs-emotion doesn't support that, so, this would include to create the bindings on https://github.com/ahrefs/bs-emotion as https://emotion.sh/docs/emotion-theming#themeprovider-reactcomponenttype
tailwind on the other side works only with classNames, so their solution it's superior on the sense of having more logic on the creation of those design values. You define a minimum set of spacing/textsize/colors and they generate a scalar css classNames in a super-long css file.
module.exports = {
important: true,
theme: {
fontFamily: {
display: ['Gilroy', 'sans-serif'],
body: ['Graphik', 'sans-serif'],
},
extend: {
colors: {
cyan: '#9cdbff',
},
margin: {
'96': '24rem',
'128': '32rem',
},
}
},
variants: {
opacity: ['responsive', 'hover']
}
}
I like the approach.
Check https://github.com/system-ui/theme-specification and https://github.com/kripod/glaze
color: red;
background: red;
background-color: red;
border-color: red;
border: 1px solid red;
https://github.com/ahrefs/bs-emotion/blob/master/bs-emotion/src/Emotion.rei#L308
Right now we only support components defined with "static" CSS. Where "static" means only working with strings or multi-line strings:
module ComponentMultiline = [%styled
{|
color: red;
background-color: white;
margin: auto 0 10px 1em;
border-bottom: thin dashed #eee;
border-right-color: rgb(1, 0, 1);
width: 70%;
background: url(http://example.com/test.jpg);
|}
]
Dynamic would be something like, instead of receiving a string, we would need to add a case where the ppx checks for a function and tries to create the make fn with the props that are being passed to that fn.
module StyledComponentWithProps = [%styled
(~color) => {|
color: $color;
|}
];
Would turn to be:
module StyledComponentWithProps = {
let styled = (~color) => Emotion.(css(color $color));
[@react.component]
let make = (~color, ~children?) => {
<div className=styled(~color)>
{switch (children) {
| Some(c) => c
| None => React.null
}}
</div>
}
];
As a reference, we would need to do similar logic from react's ppx in order to create the styled and make fns.
Here is the jsofocaml:
https://github.com/jchavarri/jsoo-react/blob/master/ppx/Rroo_jsoo_ppx.re#L213-L658
This is the first chunk of properties that are half-supported and I think are a minimum to create any sort of styles. Rendering those properties correctly we would unlock many more, like supporting opacity
you unlock: stroke-opacity
stop-opacity
flood-opacity
fill-opacity
.
animation
box-shadow
text-shadow
transform
transition
font-family
z-index
opacity
flex-grow
flex-shrink
font-weight
padding
margin
border-top-right-radius
border-top-left-radius
border-bottom-right-radius
border-bottom-left-radius
background-position
transform-origin
flex
border
outline
Parse nested properties and be able to create styles dependant on other's components.
module Children = [%styled "display: block;"];
module Parent = [%styled {|
$Children.styled {
color: red;
}
|}];
`$Children.styled` should be available.
(This would only work in static components, warn that styled isn't available, even the compiler will say it).
/* ... */
<Parent>
<Children />
</Parent>
As seen on the JavaScript world, there's a few extensions that highlight and autocomplete css:
https://marketplace.visualstudio.com/items?itemName=mf.vscode-styled-components
https://scotch.io/tutorials/create-your-first-visual-studio-code-extension
Functions are defined in the parser but aren't allowed in any property. Using any of those properties will break:
First I thought the syntax would work like that:
[%styled.div ""];
But there are a few limitations: Can't define dynamically the HTML tag. Add noise to the api, and It's a little bit complex to create those ppxs. Allows that It would remove the ability to styled(Component)
.[0] We've found a bug for you!
[0] /Users/davesnx/dev/try-styled-ppx/src/pages/Page_Home.re
[0]
[0] External preprocessor does not produce a valid file
[0] Command line: styled-ppx '/var/folders/jr/74_11td95k9420cc55tl9h_80000gn/T/camlppxee7eee' '/var/folders/jr/74_11td95k9420cc55tl9h_80000gn/T/camlppx1ee053'
Wanted to create some snapshot testing with Jest, but bs-jest doesn't support it. Found not the best option to do snapshot testing of the compiled code.
We will have the bridge between CSS AST to bs-emotion completed, we can port that bridge to a solution that doesn't cause run-time overhead, making styled components way more performant (theoretically, I would love to benchmark it first).
The steps are, try to understand what it means 0 runtime cost with dynamic styling https://compiledcssinjs.com/ or https://github.com/callstack/linaria and understand css variables:
creating bindings for it and create that bridge between CSS AST to the bindings.
Appears a weird error on the CSS_Parse apparently:
Parse error while reading token 'EOF'
Not entirely sure that it's the Parser itself, If you run Css_parse with an empty string works fine, maybe is the ppx detection that doesn't make it correctly.
Can't find binary on Github Actions, on local works great.
Would be amazing to keep a list of CSS properties that we can support without type-safeness with "unsafe" calls.
module Component = [%styled.div {|
& > *:not(:last-child) {
margin: 10px;
}
|}];
Since we use a custom lexer and pass down location everywhere we should be able to have a perfect error message.
Error: This expression has type int but an expression was expected of type Emotion.Css.LengthPercentageAuto.t
I have forked and run this repo. Built with esy
command and then attempted to run esy test
.
I got the following output in the terminal regarding the dune
version:
β styled-ppx [master]esy test
File "/Users/prisc_000/.opam/ocaml-base-compiler.4.08.1/lib/js_of_ocaml-compiler/dune-package", line 1, characters 11-14:
1 | (lang dune 2.2)
^^^
Error: Version 2.2 of dune is not supported.
Supported versions:
- 0.0
- 1.0 to 1.11
β styled-ppx [master]
Is this expected or did i do something wrong?
Thanks.
module App = [%styled.div (~background="#333") =>
should create makeProps { background: option(string) }
.
Support any type as API, this would allow dynamic components being typed:
[%styled.div (~space: int) => {j|
margin: $(space)px;
|j};
https://github.com/tizoc/ppx_pgsql#named-arguments-syntax
Another way of looking at it is like ppx_mysql and ppx_rapper:
https://github.com/issuu/ppx_mysql
https://github.com/issuu/ppx_mysql/blob/master/ppx/query.mll
Rethink some Layout flows, like Flex or Box in here: https://chakra-ui.com
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.