@nasi/ppx-react-native v0.2.1
ppx-react-native
PPX rewriters for ReScript React Native
Usage
Recommended to use with ppx-install. Add to package.json:
{
"ppx": ["@nasi/ppx-react-native"]
}This package also requires @nasi/react-native as a dependency.
If compiling with Melange, update in bs-config.json:
{
"ppx-flags": ["@nasi/ppx-react-native"]
}also update in esy.json:
{
"dependencies": {
"@nasi/ppx-react-native": "*"
}
}Stylesheet
This package adds the %%stylesheet extension, which removes the need for Js.t types for stylesheet calls. It allows the use of:
%%stylesheet(let style = {
main: Style.create(~flex=1., ()),
text: Style.create(~fontSize=14., ()),
})This will allow you to use style.main and style.text further down in the code (or in another module) if required.
Interaction with %style
This package also allows for %style extension to be used within a %%stylesheet extension as such:
%%stylesheet(let style = {
main: { flex: 1. },
text: { fontSize: 14. },
})Here, you'll get style.main with type Style.typed_t<[> #flex ]>> and style.text with type Style.typed_t<[> #fontSize ]>.
This translates to:
let style = ReactNative.StyleSheet.create({
main: { flex: 1 },
text: { fontSize: 14 },
});Translation
Consider the following:
%%stylesheet(let style = {
style1: Style.create(~flex=1., ()),
style2: Style.create(~fontSize=14., ()),
})This roughly translates to:
type stylesheet = {
style1: Style.t,
style2: Style.t,
}
let style: stylesheet = StyleSheet.unsafeCreate({
style1: Style.create(~flex=1., ()),
style2: Style.create(~fontSize=14., ()),
})In case a %style extension is present, then the tags are also added to the type definition. Consider the following:
%%stylesheet(let style = {
style1: Style.create(~flex=1., ()),
style2: Style.create(~fontSize=14., ()),
style3: %style({ flex: 1. }),
style4: %style({ fontSize: 14. }),
})This would translate to:
type stylesheet<+'a, +'b> = {
style1: Style.t,
style2: Style.t,
style3: Style.typed_t<'a>,
style4: Style.typed_t<'b>,
}
let style: stylesheet<[> #flex ], [> #fontSize ]> = StyleSheet.unsafeCreate({
style1: Style.create(~flex=1., ()),
style2: Style.create(~fontSize=14., ()),
style3: Obj.magic(Style.create(~flex=1, ())),
style4: Obj.magic(Style.create(~fontSize=14., ())),
})In both these cases, StyleSheet.unsafeCreate has the following definition:
@module("react-native") @scope("StyleSheet")
external unsafeCreate: 'a => 'a = "create"Style Typing
Similar to TyXML and bs-css, we can use tagged types and variance to include/exclude specific css elements. I've defined
// Style.resi
type typed_t<+'a>Which allows labels, such as #flex, #alignContent etc. to be attached. So, a JavaScript style
const style = {
flex: 1,
marginTop: 20,
justifyContent: "center",
}would be typed as:
let style: Style.typed_t<[> #flex | #marginTop | #justifyContent ]>At consumption, we can write:
module Element = {
@react.component
let make: (~style:Style.typed_t<[< #flex ]>=?) => React.element
}where we put all the supported style attributes in the list. In the example above, this element only accept the flex attribute, so if we were to write:
<Element style /> // Compile errorThis would raise a compile error, since style contains at least #flex, #marginTop and #justifyContent, and Element wants at most #flex.
However, if we had a style:
let style1: Style.typed_t<[> #flex ]>Then
<Element style=style1 />would be allowed.
The %style extension
Normally, it would be pretty difficult to achieve this kind of covariant tagging, since the only thing that would make sense for it would be lists, and you'll end up with lots of code that breaks the zero-cost principle. So, an extension is introduced to specifically perform this translation. It also has the benefit of making the code look closer to how it's written in JavaScript. In the example above, we fine the JavaScript as
const style = {
flex: 1,
marginTop: 20,
justifyContent: "center",
}Currently, we would have to write:
let style = Style.style(~flex=1., ~marginTop=dp(20.), ~justifyContent=#center, ())With the %style extension, we'll write:
let style = %style({
flex: 1.,
marginTop: dp(20.),
justifyContent: #center,
})Translation
The extension roughly translates the above call to:
let style: Style.typed_t<[> #flex | #marginTop | #justifyContent ]> = Obj.magic(
Style.style(
~flex=1.,
~marginTop=dp(20.),
~justifyContent=#center,
()
)
)The tags are read from record fields themselves, and the field/value pair gets translated into a labelled argument to the function style. To give a very arbitrary example:
let arbitrary = %style({
field1: value1,
field2: value2,
field3: value3,
})translates to
let arbitrary: Style.typed_t<[> #field1 | #field2 | #field3 ]> = Obj.magic(
Style.style(
~field1=value1,
~field2=value2,
~field3=value3,
()
)
)Practical Usage (tags)
I've included TypedStyle.res with definitions scraped from @types/react-native definitions, and are clustered similarly.
They include:
1. flexStyle
2. transformsStyle
3. shadowStyleIOS
4. viewStyle
5. textStyleIOS
6. textStyleAndroid
7. textStyle
8. imageStyle
I've also included utility functions to convert subsets of the above typed styles to Style.t. To give an example, we have:
let flexStyle: Style.typed_t<[< flexStyle ]> => Style.tWe can begin to migrate component definitions to utilise the tagged styles and use these functions for conversion in the meantime.
Composition
I've also included 2 ways of composing typed styles. These are:
- The
composefunction defined inTypedStyle.res - Overloading the
++operator defined in moduleInfix.
Both methods are defined using arrays, that is:
open TypedStyle.Infix // shadows the ++ operator (the ^ operator in OCaml)
let composed = style1 ++ style2 ++ style3would produce something like:
const composed = $caret(style1, $caret(style2, style3))which is equivalent to
const composed = [style1, [style2, style3]]Implementation
The entire project is built on esy and dune, and exists in the ppx directory. In order to allow development work on Windows, OCaml 4.11.x was used with ocaml-migrate-parsetree to allow the code to understand 4.06.1 parsetree generated by bs-platform.
A postinstall script is created to copy the ppx.exe to the top-level of the package.
Limitations
The %%stylesheet extension only works on "structure items", so:
%%stylesheet(let style1 = { ... }) // works
module A = {
%%stylesheet(let style2 = { ... }) // should work, not tested
}
let make = () => {
%%stylesheet(let style3 = { ... }) // not supported
}Upcoming changes
I'm looking to include the following changes in the PPX rewriter:
- Allow punning in the
%styleextension, allowing:
@react.component
let make = (~fontSize) => {
<Text style={%style({ fontSize })} />
}