Introduction

This is part of an ongoing series about the Rescript programming language. I’ve been taking notes as I build a toy “Recipe Book” app in this delightful language.

Rescript is everything Javascript should be and no more. It works beautifully with best practices in the React and Redux ecosystem.

This article completes the first miniseries by adding styling to the app we’ve been developing all along.

Patreon

This series takes a lot of time to write and maintain. I started it while I have some free time between jobs, but I’ll probably need some extra motivation to keep working on it once I’m busy with work again.

It’s become popular enough that I’ve already fielded a few change requests that have taken additional time. This has slowed me down on new content, but it makes the existing content a lot better.

For some context, I’ve written about 40,000 words of rescript content so far, which would fill a short book, and I’m still writing.

I’m not one for begging for money on the Internet, but if you’re interested in providing some extra motivation, I’ve set up a Patreon account. I thank you for your support.

As a bonus for patrons, articles will be published there at least two weeks before they make it to my public blog.

Table of Contents

Let’s begin

If you haven’t been following along with these articles, you can git checkout the stateful-components branch of the accompanying github repository.

Commits that roughly map to the sections of this article are made on the emotion-styling branch. You may find the diffs in the JS files interesting, as it shows what Rescript is compiling to underneath it all.

A note on design

I’m not a designer. I can convert a designer’s vision to actual product, but I can’t make a designer’s vision myself. luckily, current trends in design seem to be to just make everything flat and blocky, and even I can do that!

So for this article, I’ll just be adding a few styles to our recipes app to try it out. I’m kind of targetting a “recipe on an index card” look, but please don’t judge my results.

Dependencies

From my research so far, the standard for rescript styling appears to be the bs-css library. It provides bindings to the emotion CSS library, a popular library for both framework-agnostic and React styling.

The first step, of course, is to install the library. We haven’t done a lot of dependencies in this series, so you may not recall one of the weird things about rescript: You need to depend two dependency arrays. You need the normal javascript dependencies in package.json, and another for rescript dependencies in bs-config.json.

Let’s install the library in both places:

npm install --save bs-css-emotion

will put the library in package.json.

Next, manually add both bs-css and bs-css-emotion to the bs-dependencies in bsconfig.json.

  • First, we’ll need to install the library
    • One of the weird things about rescript is that you need to maintain two dependency arrays
    • First yarn add bs-css-emotion
      • This will update the dependencies in package.json
    • Then add bs-css and bs-css-emotion to the bs-dependencies in your bsconfig.json
    • Finally, run bsb -make-world to actually build those dependencies
  • We already did a bit of lightweight hand-coded styling for NavButton and NavBar

Finally, run bsb -make-world to actually build those dependencies

Port NavButton to emotion styles

We already did a bit of lightweight hand-coded styling for NavButton and NavBar in an earlier article. Let’s port that over to emotion components first. I’ll add a few tweaks to the style to go for that recipe card aesthetic.

First add a new module at the top of the NavBar.res file:

module Styles = {
  open CssJs

  let navButton = selected =>
    style(.[
      backgroundColor(
        if selected {
          "656565"->hex
        } else {
          "efefef"->hex
        },
      ),
      padding(1.0->ex),
      cursor(#pointer),
    ])

}

There’s a lot to grok here! We are creating a new module named Styles inside the NavBar module. Inside that scope, we open CssJs, which brings a bunch of style-related functions into the namespace. Opening the module makes the style calls easier to read than if we were putting CssJs.backgroundColor and CssJs.padding and so on everywhere. Note, however, that the open is local to the Styles module so we don’t have to worry about polluting the outer NavBar namespace.

Then we define a new function named navButton, which accepts a single boolean argument. It makes a single call to the style() function which accepts strongly typed values and returns a string representing an emotion classname. The strongly typed values are specified by other functions from the CssJs module.

Note that . at the beginning of the call to style(.), just inside the parenthesis. This tells Rescript that the call is “uncurried”. If you aren’t interested in the details, it’s basically a performance optimization. If you are, see the docs

The style() function accepts a list of css arguments, each of which is created by a function from the CssJs module. Each of those the functions are named after camelcase versions of the css prop names, much like React styles. So background-color becomes backgroundColor and border-bottom becomes borderBottom.

Even the arguments to those functions are strongly typed. backgroundColor can accept a variety of types. In this case, I wanted to use hex colour naming, so I used the hex() function. However, the pipe symbol allows me to send the string into the function, which I think makes it more readable. It tells you the important bit (the colour) before the type.

Recall that we want to have a different colour depending whether the current tab is selected. This was easy to do by leveraging the fact that the if statement is an expression. if “returns” the value from the chosen branch. This is the recommended way to do ternary-style conditionals (although the ? ternary operator does exist in Rescript for the cases where you think it’s more legible).

Similar to the color types passed into backgroundColor, the padding function accepts a variety of length types. In this case, I wanted to specify 1ex so I piped the 1.0 into the ex function. You will never accidentally typo a value of 1exx in a stylesheet ever again. I mean, if you’ve ever done that.

We can now simplify our NavButton component, as we have separated the displaying of the component from the logic of styling it:

module NavButton = {
  @react.component
  let make = (~name, ~selected, ~linkTo) => {
    <div
      className={Styles.navButton(selected === name)}
      onClick={_ => RescriptReactRouter.push(linkTo)}>
      {React.string(name)}
    </div>
  }
}

Reload your app to make sure the styles haven’t changed.

The important bit is the className= part. Styles.navButton returns the result of a style call, (It will look something like css-mcuw8t in the inspector) which is a string that can be used as a classname.

Styling the NavBar

We can add a second function to the Styles module to style the navigation bar itself:

let navBar = style(.[
  display(#flex),
  justifyContent(#center),
  borderBottom(1->px, #solid, "656565"->hex),
])

The #flex passed to display is a polymorphic variant. Unlike normal variants such as option, polymorphic variants don’t require an explicit type definition. In this case, they are used to guarantee you only provide valid display values. Other valid values include block and inlineBlock, for example.

The #center passed to justifyContent is similar, as is the #solid passed to borderBottom. The borderBottom function accepts multiple arguments passed in order. As an aside, I didn’t have to look anything up to figure out what the right order was. I just let the lightning-fast linter highlight my errors until I had it right. (I feel I haven’t gushed enough about how fast the Rescript tooling is in the last couple articles, so it’s time to pick it up again!)

Actually applying this style can be done by replacing the <div style =...> for the NavBars make function with <div className=Styles.navBar>. This may actually be a surprising syntax; unlike normal Javascript JSX, you do not need to put {} around the property value.

Global styles

We can also use emotion to define some global styles that apply to all components. Open Index.res and remove the %raw css import line. (You can also remove the css file itself).

  • Add a CssJs.global call to set some global styles:
{
  let maxwidth = 40.0
  open CssJs
  global(.
    "body",
    [
      fontFamily(#custom("Garamond")),
      maxWidth(maxwidth->#rem),
      border(1->px, #solid, lightgrey),
      margin(2.0->#rem),
    ],
  )
  global(.
    "h1, h2, h3",
    [
      color("656565"->hex),
      borderBottom(1->px, #dashed, lightgrey),
      maxWidth((maxwidth /. 2.0)->#rem),
    ],
  )
}

global is an uncurried (remember the .) function pretty much the same as style except the results are applied–you guessed it–globally. It accepts any css selector as the first argument, and the styles to be applied as the second.

The {...} surrounding the whole thing isn’t a module. It just creates a local scope so that the open doesn’t pollute the Index.res namespace with a bunch of functions.

One other interesting note from this example is the division by two. The /. operator is used for dividing floats. There is no operating overloading in Rescript, so there are two different division operators: / for integer division and /. for floats.

Sharing and merging styles

Looking at the add and view recipe pages, it’s clear that the values inside the “card” need a margin. I think the add recipe form also needs a little padding, which gives us a chance to showcase merging styles. Basically, cards used for data entry have a different style from those used for viewing.

  • Create a new CardStyles.res file:

    open CssJs
    
    let card = style(.[margin(2.0->#rem)])
    
    let formCard = merge(.[card, style(.[padding(1.0->#rem)])])
    

The main new concept here is the merge call on formCard. It “safely merges styles by name”, to quote the docs. In practice, it’s just a wrapper around emotion’s cx method.

These styles can be added to the top-level div of each of our components.

  • ViewRecipe and AllTags can have their outer <div> changed to <div className=CardStyles.card>
  • AddRecipeForm can get <div className=CardStyles.formCard>

Final tweak for AddRecipe

It doesn’t introduce any new ideas, but I wanted to improve the textfields on AddRecipeForm:

module Styles = {
 open CssJs
 let longEntry = style(.[
   width(100.0->#percent),
   maxWidth(20.0->#rem),
   borderRadius(0.5->#rem),
   padding(0.5->#rem),
 ])
}

Conclusion

I’m going to stop here as there doesn’t seem to be anything left to learn or teach about bs-css. I invite you to make additional styling changes to make the Recipe Book app look real special. Here are some suggestions:

  • The buttons on add tag and add recipe look out of place
  • The nav buttons could look a bit more tab like
  • The links to individual recipes in All Tags need a cursor: pointer style
  • Might want to play with centering text
  • Every good index card needs a drop shadow and rounded corners ;-)

At this point we’ve built a small app entirely in Rescript. There are now %raw tags anywhere. We’ve covered JSX syntax, React functional-style components, redux-style reducers, and even styling.

The app is fully and soundly type checked. I haven’t written any tests yet (hopefully in a future article), but I feel Rescript has given me a lot of confidence that it works.

I’m going to take a break from this single page app for a bit. My next article will be on using Rescript to write an express server! Later, though, I’ll port this app to be offline-enabled and eventually hook it up to the express server as a progressive web app.