diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js
index 0737a463..f7ff9861 100644
--- a/docs/.vitepress/config.js
+++ b/docs/.vitepress/config.js
@@ -34,6 +34,7 @@ export default defineConfig({
{ text: 'Celsius Converter using Option', link: '/celsius-converter-option/' },
{ text: 'Introduction to Dune', link: '/intro-to-dune/' },
{ text: 'Order Confirmation', link: '/order-confirmation/' },
+ { text: 'Styling with CSS', link: '/styling-with-css/' },
]
}
],
diff --git a/docs/counter/index.md b/docs/counter/index.md
index e14d8d56..558cb237 100644
--- a/docs/counter/index.md
+++ b/docs/counter/index.md
@@ -8,7 +8,7 @@ We're going build the classic frontend starter app, the counter, using
[melange-for-react-devs-template](https://github.com/melange-re/melange-for-react-devs-template)
project
1. Run `make watch` to start the Melange compiler in watch mode.
-1. In another terminal window, start the webpack dev server by running `make
+1. In another terminal window, start the Webpack dev server by running `make
serve`. As a side effect, it will open a browser tab pointed to
http://localhost:8080/.
diff --git a/docs/intro-to-dune/index.md b/docs/intro-to-dune/index.md
index 315e4f4a..bfaf07c3 100644
--- a/docs/intro-to-dune/index.md
+++ b/docs/intro-to-dune/index.md
@@ -138,7 +138,7 @@ contains:
- `Index.re` to render the app to the DOM
<<< @/../src/counter/Index.re
-- `Makefile` to serve the app using webpack dev server
+- `Makefile` to serve the app using Webpack dev server
<<< @/../src/counter/Makefile{make}
@@ -202,7 +202,7 @@ use
./_build/default/src/$(app)/output/src/$(app)/Index.js
```
-This means that the entry script served by webpack dev server depends on the
+This means that the entry script served by Webpack dev server depends on the
`app` environment variable, which is provided by `src/counter/Makefile`.
You're now ready to run the new Counter app you created! Go into the
diff --git a/docs/intro/index.md b/docs/intro/index.md
index b856790c..039cae56 100644
--- a/docs/intro/index.md
+++ b/docs/intro/index.md
@@ -32,6 +32,7 @@ teaches the language from the ground up and goes much deeper into its features.
| Celsius Converter using Option | The same component from the last chapter but replacing exception handling with Option | Option, `Option.map`, `when` guard |
| Introduction to Dune | A introduction to the Dune build system | `dune-project` file, `dune` file, `melange.emit` stanza, `Makefile`, monorepo structure |
| Order Confirmation | An order confirmation for a restaurant website | variant type, primary type of module (`t`), wildcard (`_`) in switch, `fun` syntax, `Js.Array` functions, `React.array`, type transformation functions |
+| Styling with CSS | Styling the order confirmation using CSS | `mel.raw` extension node, `runtime_deps` field, `glob_files` term, `external`, `mel.module` attribute |
...and much more to come!
diff --git a/docs/styling-with-css/Order.re b/docs/styling-with-css/Order.re
new file mode 100644
index 00000000..5faa96b4
--- /dev/null
+++ b/docs/styling-with-css/Order.re
@@ -0,0 +1,102 @@
+module Item = {
+ type t =
+ | Sandwich
+ | Burger
+ | Hotdog;
+
+ let toPrice =
+ fun
+ | Sandwich => 10.
+ | Burger => 15.
+ | Hotdog => 5.;
+
+ let toEmoji =
+ fun
+ | Sandwich => {js|🥪|js}
+ | Burger => {js|🍔|js}
+ | Hotdog => {js|🌭|js};
+};
+
+module Format = {
+ let currency = _value => React.null;
+};
+
+// #region order-item
+module OrderItem = {
+ [@react.component]
+ let make = (~item: Item.t) =>
+
+ {item |> Item.toEmoji |> React.string} |
+ {item |> Item.toPrice |> Format.currency} |
+
;
+};
+// #endregion order-item
+
+type t = array(Item.t);
+
+// #region order-make
+[@react.component]
+let make = (~items: t) => {
+ let total =
+ items |> Js.Array.reduce((acc, order) => acc +. Item.toPrice(order), 0.);
+
+
+
+ {items
+ |> Js.Array.mapi((item, index) =>
+
+ )
+ |> React.array}
+
+ {React.string("Total")} |
+ {total |> Format.currency} |
+
+
+
;
+};
+// #endregion order-make
+
+module OrderItemV2 = {
+ // #region order-item-css
+ module OrderItem = {
+ [@mel.module "./order-item.module.css"]
+ external css: Js.t({..}) = "default";
+
+ [@react.component]
+ let make = (~item: Item.t) =>
+
+ {item |> Item.toEmoji |> React.string} |
+
+ {item |> Item.toPrice |> Format.currency}
+ |
+
;
+ };
+ // #endregion order-item-css
+};
+
+module OrderV2 = {
+ // #region order-external
+ [@mel.module "./order.module.css"] external css: Js.t({..}) = "default";
+
+ [@react.component]
+ let make = (~items: t) => {
+ let total =
+ items
+ |> Js.Array.reduce((acc, order) => acc +. Item.toPrice(order), 0.);
+
+
+
+ {items
+ |> Js.Array.mapi((item, index) =>
+
+ )
+ |> React.array}
+
+ {React.string("Total")} |
+ {total |> Format.currency} |
+
+
+
;
+ };
+ // #endregion order-external
+};
diff --git a/docs/styling-with-css/dune b/docs/styling-with-css/dune
new file mode 100644
index 00000000..2ff19d7b
--- /dev/null
+++ b/docs/styling-with-css/dune
@@ -0,0 +1,8 @@
+(melange.emit
+ (target output)
+ (libraries reason-react)
+ (preprocess
+ (pps melange.ppx reason-react-ppx))
+ (module_systems es6)
+ (runtime_deps
+ (glob_files *.css)))
diff --git a/docs/styling-with-css/index.md b/docs/styling-with-css/index.md
new file mode 100644
index 00000000..dd8ecaf9
--- /dev/null
+++ b/docs/styling-with-css/index.md
@@ -0,0 +1,337 @@
+# 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.
+
+## Update Webpack config
+
+In order to use CSS from our ReasonReact components, we'll install the
+`style-loader` and `css-loader` Webpack plugins[^1]:
+
+```
+npm install --save-dev css-loader style-loader
+```
+
+Add a new rule to `webpack.config.js` so that these plugins will be applied to
+your `.css` files:
+
+```javascript{5-12}
+module.exports = {
+ devServer: {
+ historyApiFallback: true,
+ },
+ module: {
+ rules: [
+ {
+ test: /\.css$/i,
+ use: ["style-loader", "css-loader"],
+ },
+ ],
+ },
+};
+```
+
+## Add the first CSS file
+
+Add a new file
+`src/order-confirmation/order-item.css` and give it these styles:
+
+
+<<< @/../src/styling-with-css/order-item.module.css
+
+## 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[^2]. However, we can make use of
+JavaScript's `import` syntax by using the [mel.raw extension
+node](https://melange.re/v2.1.0/communicate-with-javascript/#generate-raw-javascript),
+which allows us to embed raw JavaScript in our OCaml code. Add the following
+line to the top of `Order.re`:
+
+```reason
+[%%mel.raw {|import "./order-item.css"|}];
+```
+
+The `{||}` string literal is known as a [quoted
+string literal](https://v2.ocaml.org/manual/lex.html#sss:stringliterals), and it
+is used to represent strings of arbitrary content without escaping[^3]. They are
+similar to the `{js||js}` string literals we first saw in the [Celsius
+Converter](/celsius-converter-exception/#solutions) chapter, with the difference
+that they won't handle Unicode correctly.
+
+Unfortunately, in the terminal where we're running `make serve`, we see this
+Webpack compilation error:
+
+```
+ERROR in ./_build/default/src/order-confirmation/output/src/order-confirmation/Order.js 7:0-8:1
+Module not found: Error: Can't resolve './order-item.css' in '~/melange-for-react-devs/_build/default/src/order-confirmation/output/src/order-confirmation'
+```
+
+## Tell Dune to copy CSS files
+
+The problem is that Webpack 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](https://melange.re/v2.1.0/build-system/#handling-assets) to our
+`melange.emit` stanza in `src/order-confirmation/dune`:
+
+```clj{7}
+(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:
+
+
+<<< @/../src/styling-with-css/order.module.css
+
+To ensure that `order.css` is also copied to the build directory, we can add
+`order.css` to the value of `runtime_deps`:
+
+```clj
+(runtime_deps 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:
+
+```clj
+(runtime_deps (glob_files *.css))
+```
+
+Check the Dune documentation for the [different options for
+globs](https://dune.readthedocs.io/en/stable/concepts/dependency-spec.html#glob).
+
+## Add classes to JSX
+
+Now we can add the appropriate classes to `OrderItem.make`'s JSX:
+
+<<< Order.re#order-item{4-6}
+
+As well as `Order.make`'s JSX:
+
+<<< Order.re#order-make{6,13}
+
+Finally, add a `mel.raw` extension node at the top of `Order.re`:
+
+```reason
+[%%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](https://melange.re/v2.2.0/communicate-with-javascript/#external-functions)
+declaration:
+
+```reason
+[%%mel.raw {|import "./order-item.css"|}]; // [!code --]
+[@mel.module "./order-item.css"] external _css: unit = "default"; // [!code ++]
+```
+
+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:
+
+```javascript
+import OrderItemCss from "./order-item.css";
+var _css;
+```
+
+Let's break down the individual parts of the `external` declaration:
+
+```reason
+[@mel.module "./order-item.css"] external _css: unit = "default";
+```
+
+- [mel.module](https://melange.re/v2.2.0/communicate-with-javascript/#using-functions-from-other-javascript-modules)
+ is an
+ [attribute](https://melange.re/v2.2.0/communicate-with-javascript/#attributes)
+ 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[^4]
+- `_css: unit` means the object we get back from the import is named `_css` and
+ has type `unit`. 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](https://reasonml.github.io/docs/en/overview#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](https://melange.re/v2.2.0/playground/). For example,
+here's a
+[link](https://melange.re/v2.2.0/playground/?language=Reason&code=W0BtZWwubW9kdWxlICIuL29yZGVyLWl0ZW0uY3NzIl0gZXh0ZXJuYWwgX2NzczogdW5pdCA9ICJkZWZhdWx0Ijs%3D&live=off)
+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](https://css-tricks.com/css-modules-part-1-need/), which give us access
+to locally-scoped classes[^5].
+
+First, rename `order-item.css` to `order-item.module.css`, which turns it into a
+CSS module. Then change the corresponding `external` declaration:
+
+```reason
+[@mel.module "./order-item.css"] external _css: unit = "default"; // [!code --]
+[@mel.module "./order-item.module.css"] external css: Js.t({..}) = "default"; // [!code ++]
+```
+
+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 to `css`, since we intend to use the variable
+ later
+- We change the type of `css` from `unit` to `Js.t({..})`[^6]
+
+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:
+
+<<< Order.re#order-item-css{7-9}
+
+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](https://melange.re/v2.2.0/communicate-with-javascript/#external-functions)
+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](/todo), 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.
+
+## Exercises
+
+
+1. Extension nodes like `mel.raw` can also be prefixed with `%` instead
+of `%%`. What happens if you replace `%%mel.raw` with `%mel.raw`?
+
+2. Refactor the `Order` component so that it also uses an `external`
+declaration instead of `mel.raw`.
+
+3. Replace your usage of `mel.module` with `bs.module`. What happens?
+
+## Overview
+
+- The `mel.raw` extension node embeds raw JavaScript inside OCaml code
+ - It isn't type-safe and you can usually use `external` instead
+- The `runtime_deps` field of `melange.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
+- `external` declarations are used to import CSS or JS files
+ - The `mel.module` attribute is used to specify which module or file to import
+
+## Solutions
+
+1. Changing `%%mel.raw` to `%mel.raw` will cause a compile error in
+Webpack because the generated JS code changes to
+
+```
+((import "./order.css"));
+```
+
+which isn't valid JavaScript syntax. Changing it back to `%%mel.raw` will
+produce syntactically valid JS:
+
+```javascript
+import "./order.css"
+;
+```
+
+The general rule is that you should use `%%mel.raw` for statements, and
+`%mel.raw` for expressions.
+
+2. After you refactor the `Order` component to use an `external`
+declaration, it should look something like this:
+
+<<< Order.re#order-external
+
+3. If you replace `mel.module` with `bs.module`, your code will continue
+to compile but you get a warning from Melange:
+
+```
+File "src/styling-with-css/Order.re", line 4, characters 4-13:
+Alert deprecated: The `[@bs.*]' attributes are deprecated and will be removed in the
+next release.
+Use `[@mel.*]' instead.
+```
+
+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`.
+
+-----
+
+[Source code for this
+chapter](https://github.com/melange-re/melange-for-react-devs/blob/main/src/styling-with-css/)
+can be found in the [Melange for React Developers
+repo](https://github.com/melange-re/melange-for-react-devs).
+
+[^1]: The `css-loader` plugin allows you to import CSS files and the
+ `style-loader` plugin injects imported CSS into the DOM.
+
+[^2]: 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.
+
+[^3]: Quoted string literals are similar to [multiline string literals in
+ Python](https://ioflood.com/blog/python-multiline-string/). Inside a quoted
+ string literal, you don't need to escape double quote or newline characters.
+
+[^4]: In native OCaml, `external` refers to functions and variables that come
+ from C.
+
+[^5]: 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.
+
+[^6]: ` Js.t({..})` is the type signature for a `Js.t` object, which we [first
+ encountered](/celsius-converter-exception/#js-t-object) in the Celsius
+ Converter chapter.
diff --git a/src/styling-with-css/Format.re b/src/styling-with-css/Format.re
new file mode 100644
index 00000000..1536313f
--- /dev/null
+++ b/src/styling-with-css/Format.re
@@ -0,0 +1,2 @@
+let currency = value =>
+ value |> Js.Float.toFixedWithPrecision(~digits=2) |> React.string;
diff --git a/src/styling-with-css/Index.re b/src/styling-with-css/Index.re
new file mode 100644
index 00000000..35fc8fe8
--- /dev/null
+++ b/src/styling-with-css/Index.re
@@ -0,0 +1,17 @@
+module App = {
+ let items: Order.t = [|Sandwich, Burger, Sandwich, Hotdog|];
+
+ [@react.component]
+ let make = () =>
+
+
{React.string("Order confirmation")}
+
+ ;
+};
+
+let node = ReactDOM.querySelector("#root");
+switch (node) {
+| None =>
+ Js.Console.error("Failed to start React: couldn't find the #root element")
+| Some(root) => ReactDOM.render(, root)
+};
diff --git a/src/styling-with-css/Item.re b/src/styling-with-css/Item.re
new file mode 100644
index 00000000..15af8c87
--- /dev/null
+++ b/src/styling-with-css/Item.re
@@ -0,0 +1,16 @@
+type t =
+ | Sandwich
+ | Burger
+ | Hotdog;
+
+let toPrice =
+ fun
+ | Sandwich => 10.
+ | Burger => 15.
+ | Hotdog => 5.;
+
+let toEmoji =
+ fun
+ | Sandwich => {js|🥪|js}
+ | Burger => {js|🍔|js}
+ | Hotdog => {js|🌭|js};
diff --git a/src/styling-with-css/Makefile b/src/styling-with-css/Makefile
new file mode 100644
index 00000000..9f647544
--- /dev/null
+++ b/src/styling-with-css/Makefile
@@ -0,0 +1,2 @@
+serve:
+ app=styling-with-css make -C ../.. serve
diff --git a/src/styling-with-css/Order.re b/src/styling-with-css/Order.re
new file mode 100644
index 00000000..bf5b4855
--- /dev/null
+++ b/src/styling-with-css/Order.re
@@ -0,0 +1,35 @@
+type t = array(Item.t);
+
+module OrderItem = {
+ [@mel.module "./order-item.module.css"]
+ external css: Js.t({..}) = "default";
+
+ [@react.component]
+ let make = (~item: Item.t) =>
+
+ {item |> Item.toEmoji |> React.string} |
+ {item |> Item.toPrice |> Format.currency} |
+
;
+};
+
+[@mel.module "./order.module.css"] external css: Js.t({..}) = "default";
+
+[@react.component]
+let make = (~items: t) => {
+ let total =
+ items |> Js.Array.reduce((acc, order) => acc +. Item.toPrice(order), 0.);
+
+
+
+ {items
+ |> Js.Array.mapi((item, index) =>
+
+ )
+ |> React.array}
+
+ {React.string("Total")} |
+ {total |> Format.currency} |
+
+
+
;
+};
diff --git a/src/styling-with-css/dune b/src/styling-with-css/dune
new file mode 100644
index 00000000..2ff19d7b
--- /dev/null
+++ b/src/styling-with-css/dune
@@ -0,0 +1,8 @@
+(melange.emit
+ (target output)
+ (libraries reason-react)
+ (preprocess
+ (pps melange.ppx reason-react-ppx))
+ (module_systems es6)
+ (runtime_deps
+ (glob_files *.css)))
diff --git a/src/styling-with-css/order-item.module.css b/src/styling-with-css/order-item.module.css
new file mode 100644
index 00000000..c90296f2
--- /dev/null
+++ b/src/styling-with-css/order-item.module.css
@@ -0,0 +1,11 @@
+.item {
+ border-top: 1px solid lightgray;
+}
+
+.emoji {
+ font-size: 2em;
+}
+
+.price {
+ text-align: right;
+}
diff --git a/src/styling-with-css/order.module.css b/src/styling-with-css/order.module.css
new file mode 100644
index 00000000..0d6b4d9c
--- /dev/null
+++ b/src/styling-with-css/order.module.css
@@ -0,0 +1,13 @@
+table.order {
+ border-collapse: collapse;
+}
+
+table.order td {
+ padding: 0.5em;
+}
+
+.total {
+ border-top: 1px solid gray;
+ font-weight: bold;
+ text-align: right;
+}