Styling with CSS
The restaurant website’s order confirmation widget is far from complete, but Madame Jellobutter insists that you make the widget less ugly before you do anything else. In previous chapters, we saw how to add styles using the style
prop, but let’s see how to do it with good old CSS.
Add the first CSS file
Add a new file src/order-confirmation/order-item.css
and give it these styles:
.item {
border-top: 1px solid lightgray;
}
.emoji {
font-size: 2em;
}
.price {
text-align: right;
}
.item {
border-top: 1px solid lightgray;
}
.emoji {
font-size: 2em;
}
.price {
text-align: right;
}
Import using mel.raw
In OCaml, there is no syntax to import from files, because all modules within a project are visible to all other modules[1]. However, we can make use of JavaScript’s import
syntax by using the mel.raw extension node, which allows us to embed raw JavaScript in our OCaml code. Add the following line to the top of Order.re
:
[%%mel.raw {|import "./order-item.css"|}];
[%%mel.raw {|import "./order-item.css"|}];
The {||}
string literal is known as a quoted string literal, and it is used to represent strings of arbitrary content without escaping[2]. They are similar to the {js||js}
string literals we first saw in the Celsius Converter chapter, with the difference that they won’t handle Unicode correctly.
Unfortunately, in the terminal where we’re running npm run serve
, we see this Vite compilation error:
6:05:39 PM [vite] Pre-transform error:
Failed to load url /_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css
(resolved id:
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css)
in
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/Order.js.
Does the file exist?
6:05:39 PM [vite] Pre-transform error:
Failed to load url /_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css
(resolved id:
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/order-item.module.css)
in
~/melange-for-react-devs/_build/default/src/styling-with-css/output/src/styling-with-css/Order.js.
Does the file exist?
Tell Dune to copy CSS files
The problem is that Vite is serving the app from the build directory at _build/default/src/order-confirmation/output/src/order-confirmation
, and the order-item.css
file isn’t in that build directory.
To solve this, we can add the runtime_deps field to our melange.emit
stanza in src/order-confirmation/dune
:
(melange.emit
(target output)
(libraries reason-react)
(preprocess
(pps melange.ppx reason-react-ppx))
(module_systems es6)
(runtime_deps order-item.css))
(melange.emit
(target output)
(libraries reason-react)
(preprocess
(pps melange.ppx reason-react-ppx))
(module_systems es6)
(runtime_deps order-item.css))
We also want to add styles for the Order
component, so add a new file src/order-confirmation/order.css
with these styles:
table.order {
border-collapse: collapse;
}
table.order td {
padding: 0.5em;
}
.total {
border-top: 1px solid gray;
font-weight: bold;
text-align: right;
}
table.order {
border-collapse: collapse;
}
table.order td {
padding: 0.5em;
}
.total {
border-top: 1px solid gray;
font-weight: bold;
text-align: right;
}
To ensure that order.css
is also copied to the build directory, we can add order.css
to the value of runtime_deps
:
(runtime_deps order-item.css order.css)
(runtime_deps order-item.css order.css)
If you have many .css
files, you can tell runtime_deps
to copy all .css
files over using Dune’s glob_files
configuration:
(runtime_deps (glob_files *.css))
(runtime_deps (glob_files *.css))
Check the Dune documentation for the different options for globs.
Add classes to JSX
Now we can add the appropriate classes to OrderItem.make
’s JSX:
module OrderItem = {
[@react.component]
let make = (~item: Item.t) =>
<tr className="item">
<td className="emoji"> {item |> Item.toEmoji |> React.string} </td>
<td className="price"> {item |> Item.toPrice |> Format.currency} </td>
</tr>;
};
module OrderItem = {
[@react.component]
let make = (~item: Item.t) =>
<tr className="item">
<td className="emoji"> {item |> Item.toEmoji |> React.string} </td>
<td className="price"> {item |> Item.toPrice |> Format.currency} </td>
</tr>;
};
As well as Order.make
’s JSX:
[@react.component]
let make = (~items: t) => {
let total =
items
|> Js.Array.reduce(~init=0., ~f=(acc, order) =>
acc +. Item.toPrice(order)
);
<table className="order">
<tbody>
{items
|> Js.Array.mapi(~f=(item, index) =>
<OrderItem key={"item-" ++ string_of_int(index)} item />
)
|> React.array}
<tr className="total">
<td> {React.string("Total")} </td>
<td> {total |> Format.currency} </td>
</tr>
</tbody>
</table>;
};
[@react.component]
let make = (~items: t) => {
let total =
items
|> Js.Array.reduce(~init=0., ~f=(acc, order) =>
acc +. Item.toPrice(order)
);
<table className="order">
<tbody>
{items
|> Js.Array.mapi(~f=(item, index) =>
<OrderItem key={"item-" ++ string_of_int(index)} item />
)
|> React.array}
<tr className="total">
<td> {React.string("Total")} </td>
<td> {total |> Format.currency} </td>
</tr>
</tbody>
</table>;
};
Finally, add a mel.raw
extension node at the top of Order.re
:
[%%mel.raw {|import "./order.css"|}];
[%%mel.raw {|import "./order.css"|}];
Problems with mel.raw
This solution works well for our current build configuration, but falls apart if we change the module_systems
field of the melange.emit
stanza from es6
to commonjs
. This results in a subtle runtime error caused by CommonJS needing require
instead of import
to import modules.
The mel.raw
extension node is unsafe, but it is still useful for prototyping. Fortunately, Melange provides a more reliable way to import frontend assets.
Import using external
At the top of Order.re
, replace our first mel.raw
extension node with an external declaration:
[%%mel.raw {|import "./order-item.css"|}];
[@mel.module "./order-item.css"] external _css: unit = "default";
[%%mel.raw {|import "./order-item.css"|}];
[@mel.module "./order-item.css"] external _css: unit = "default";
This essentially tells OCaml to assign the default export of the order-item.css
module to the variable _css
. The generated JavaScript looks like this:
import OrderItemCss from "./order-item.css";
var _css;
import OrderItemCss from "./order-item.css";
var _css;
Let’s break down the individual parts of the external
declaration:
[@mel.module "./order-item.css"] external _css: unit = "default";
[@mel.module "./order-item.css"] external _css: unit = "default";
- mel.module is an attribute that tells the
external
declaration which module to import from - The
external
keyword tells OCaml this is a declaration for a value defined outside of OCaml, i.e. it comes from JavaScript[3] _css: unit
means the object we get back from the import is named_css
and has typeunit
. We put an underscore in front of the name because we don’t intend to use this variable. Likewise, we give it a type of unit because it doesn’t have a meaningful value.- The
"default"
at the end tells OCaml to import the default export of the module.
TIP
A quick way to check what an external
declaration compiles to is to use the Melange Playground. For example, here’s a link to the external
declaration we just added.
Use CSS modules
Right now, the classes defined in the CSS files we’re importing are in the global scope. For non-trivial projects, it’s better to use CSS modules, which give us access to locally-scoped classes[4].
First, rename order-item.css
to order-item.module.css
, which turns it into a CSS module. Then change the corresponding external
declaration:
[@mel.module "./order-item.css"] external _css: unit = "default";
[@mel.module "./order-item.module.css"] external css: Js.t({..}) = "default";
[@mel.module "./order-item.css"] external _css: unit = "default";
[@mel.module "./order-item.module.css"] external css: Js.t({..}) = "default";
There are three changes of note:
- We change the payload of the
mel.module
attribute to./order-item.module.css
to reflect the new name of the file - We rename the
_css
variable tocss
, since we intend to use the variable later - We change the type of
css
fromunit
toJs.t({..})
[5]
If you look at your compiled app in the browser right now, you’ll see that this change breaks the styles, because the classes defined in order-item.module.css
can no longer be accessed by the names we originally gave them. To access the locally-scoped classes, we must refactor the OrderItem
component so that it accesses the class names through the css
variable:
module OrderItem = {
[@mel.module "./order-item.module.css"]
external css: Js.t({..}) = "default";
[@react.component]
let make = (~item: Item.t) =>
<tr className=css##item>
<td className=css##emoji> {item |> Item.toEmoji |> React.string} </td>
<td className=css##price>
{item |> Item.toPrice |> Format.currency}
</td>
</tr>;
};
module OrderItem = {
[@mel.module "./order-item.module.css"]
external css: Js.t({..}) = "default";
[@react.component]
let make = (~item: Item.t) =>
<tr className=css##item>
<td className=css##emoji> {item |> Item.toEmoji |> React.string} </td>
<td className=css##price>
{item |> Item.toPrice |> Format.currency}
</td>
</tr>;
};
Recall that ##
is the access operator for Js.t
objects, so className=css##item
is equivalent to className={css.item}
in JavaScript. Note that we also moved the external
declaration for ./order-item.module.css
inside the OrderComponent
module, since that’s the only place it’s used.
We have not seen the last of external
declarations, as they are the primary way in which OCaml interacts with code written in JavaScript. See the Melange docs for more details.
Class names must be the same
The class names you use in .css
files must be the same as the ones you use in your .re
files. Try changing css##emoji
in the OrderItem
component to css##emojis
.
What happens is that the styling for emojis silently breaks. This is the weakness of the CSS module approach, which requires that you manually keep all class names in sync. In a future chapter, we’ll introduce a type-safe approach to styling that doesn’t have this problem.
Excelsior! Madame Jellobutter likes how the order confirmation widget looks so far. But she plans to add more options for her current menu items, for example she’d like to have more than one type of sandwich. We’ll tackle that in the next chapter.
Overview
- The
mel.raw
extension node embeds raw JavaScript inside OCaml code- It isn’t type-safe and you can usually use
external
instead
- It isn’t type-safe and you can usually use
- The
runtime_deps
field ofmelange.emit
copies assets like.css
files to the build directory- The
glob_files
term can be used to copy all files of a certain type
- The
external
declarations are used to import CSS or JS files- The
mel.module
attribute is used to specify which module or file to import
- The
Exercises
1. Extension nodes like mel.raw
can also be prefixed with %
instead of %%
. What happens if you replace %%mel.raw
with %mel.raw
?
[%%mel.raw {|import "./order-item.css"|}];
[%mel.raw {|import "./order-item.css"|}];
[%%mel.raw {|import "./order-item.css"|}];
[%mel.raw {|import "./order-item.css"|}];
Solution
Changing %%mel.raw
to %mel.raw
will cause a compilation error in Vite because the generated JS code changes to
((import "./order.css"));
((import "./order.css"));
which isn’t valid JavaScript syntax. Changing it back to %%mel.raw
will produce syntactically valid JS:
import "./order.css"
;
import "./order.css"
;
The general rule is that you should use %%mel.raw
for statements, and %mel.raw
for expressions.
2. Refactor the Order
component so that it also uses an external
declaration instead of mel.raw
.
Solution
Order
component using external
declaration:
[@mel.module "./order.module.css"] external css: Js.t({..}) = "default";
[@react.component]
let make = (~items: t) => {
let total =
items
|> Js.Array.reduce(~init=0., ~f=(acc, order) =>
acc +. Item.toPrice(order)
);
<table className=css##order>
<tbody>
{items
|> Js.Array.mapi(~f=(item, index) =>
<OrderItem key={"item-" ++ string_of_int(index)} item />
)
|> React.array}
<tr className=css##total>
<td> {React.string("Total")} </td>
<td> {total |> Format.currency} </td>
</tr>
</tbody>
</table>;
};
[@mel.module "./order.module.css"] external css: Js.t({..}) = "default";
[@react.component]
let make = (~items: t) => {
let total =
items
|> Js.Array.reduce(~init=0., ~f=(acc, order) =>
acc +. Item.toPrice(order)
);
<table className=css##order>
<tbody>
{items
|> Js.Array.mapi(~f=(item, index) =>
<OrderItem key={"item-" ++ string_of_int(index)} item />
)
|> React.array}
<tr className=css##total>
<td> {React.string("Total")} </td>
<td> {total |> Format.currency} </td>
</tr>
</tbody>
</table>;
};
3. Replace your usage of mel.module
with bs.module
. What happens?
Solution
If you replace mel.module
with bs.module
, your code will fail to compile with this error message:
File "src/styling-with-css/Order.re", line 4, characters 4-13:
Error: `[@bs.*]' and non-namespaced attributes have been removed in favor of `[@mel.*]' attributes.
File "src/styling-with-css/Order.re", line 4, characters 4-13:
Error: `[@bs.*]' and non-namespaced attributes have been removed in favor of `[@mel.*]' attributes.
Basically, bs.module
was the old name for the attribute, but it has been replaced by mel.module
. This is worth mentioning because there’s still a decent amount of code out in the wild that uses bs.module
.
View source code and demo for this chapter.
Recall that in the
Index
modules you’ve written so far, you’ve never had to import any of the components you used that were defined in other files. ↩︎Quoted string literals are similar to multiline string literals in Python. Inside a quoted string literal, you don’t need to escape double quote or newline characters. ↩︎
In native OCaml,
external
refers to functions and variables that come from C. ↩︎The “local scoping” of CSS modules isn’t quite like scoping in a programming language. Instead, class names defined in a
.module.css
file are obfuscated so that only OCaml/JS modules that import them directly can use them. ↩︎Js.t({..})
is the type signature for aJs.t
object, which we first encountered in the Celsius Converter chapter. ↩︎