# User Guide

This documentation introduces how to manually install and configure Inertia for a combo project.

Before we begin, we need to choose the frontend framework to use. Here we'll use React, but the process is similar for other Inertia-compatible frameworks, like Vue or Svelte.

> Combo's project generator - [combo_new](https://github.com/combo-lab/combo_new), already includes all of this scaffolding and are the fastest way to get started with Combo and Inertia.

## Compatibility

`Inertia.js >= 2.0.0`

## Installation

### Generating a project using Vite

When using Inertia, it's best to use it in conjunction with modern assets build tools like Vite. To get started quickly, let's create a project from `vite` template provided by `combo_new`:

```
$ mix combo_new vite my_app
```

### Server-side setup

#### Installing dependencies

Add `:combo_inertia` to the list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:combo_inertia, "<requirement>"}
  ]
end
```

#### Setting up necessary modules

This package includes a few modules:

- `Combo.Inertia.Plug` - the plug for detecting Inertia requests and preparing the connection accordingly.
- `Combo.Inertia.Conn` - the `%Plug.Conn{}` helpers for rendering Inertia responses.
- `Combo.Inertia.HTML` - the HTML components and helpers for building Inertia views.

First, add `Combo.Inertia.Plug` into the browser pipeline:

```diff
  # lib/my_app/web/router.ex
  defmodule MyApp.Web.Router do
    use MyApp.Web, :router

    pipeline :browser do
      # ...
+     plug Combo.Inertia.Plug
    end
  end
```

Then:

- import `Combo.Inertia.Conn` into the controller helper.
- import `Combo.Inertia.HTML` into the html helper.

```diff
  # lib/my_app/web.ex
  defmodule MyApp.Web do
    # ...

    def controller do
      quote do
        # ...
+       import Combo.Inertia.Conn
      end
    end

    # ...

    defp html_helpers do
      quote do
        # ...
+       import Combo.Inertia.HTML
        # ...
      end
    end

    # ...
  end
```

#### Modifying the root layout

- add `data-ssr` attribute to the `<html>` tag, which is supplied to client-side code to identify the current rendering mode.
- replace the `<title>` tag with the `<.inertia_title>` component, which is used to keep the title in sync with client-side code.
- add the `<.inertia_head>` component.

```diff
  # lib/my_app/web/layouts/root.html.ceex
  <!DOCTYPE html>
- <html lang="en">
+ <html lang="en" data-ssr={@inertia_ssr}>
    <head>
      <!-- ... -->
-     <title>
-       {if title = assigns[:page_title], do: "#{title} · MyApp", else: "MyApp"}
-     </title>
+     <.inertia_title>
+       {if title = assigns[:page_title], do: "#{title} - MyApp", else: "MyApp"}
+     </.inertia_title>
+     <.inertia_head content={@inertia_head} />
-     <.vite_assets names={["src/css/app.css", "src/js/app.js"]} />
+     <.vite_react_refresh />
+     <.vite_assets names={["src/js/app.jsx"]} />
    </head>
    <!-- ... -->
```

> You may noticed that we add `<.vite_react_refresh />` component before the `<.vite_assets />` component.
> It's provided by [`combo_vite`](https://github.com/combo-lab/combo_vite) for enabling fast refresh in development, and only for React.

#### Adding configuration

If you'd like to add configuration, these configuration options are available.

```elixir
config :my_app, MyApp.Web.Endpoint,
  inertia: [
    # Configures the asset versioning strategy.
    #
    # Available values:
    #   * `:auto` - Automatically determines version in following order:
    #     1. check manifest file generated by Vite, and hash it if present
    #     2. check manifest file generated by `combo.static.digest`, and hash it if present
    #     3. Falls back to `"not-detected"` if no manifest found
    #
    #   * a string - Uses a fixed version string
    #
    #   * a {module, fun, args} tuple - Calls the specified function to generate version string
    #
    # Defaults to `:auto`
    assets_version: :auto,

    # Instruct the client side whether to encrypt the page object in the window
    # history state.
    # Defaults to `false`.
    encrypt_history: false,

    # Enable automatic conversion of prop keys from snake case to camel case.
    # Defaults to `false`.
    camelize_props: false,

    # Enable server-side rendering for page responses (requires some additional setup,
    # see instructions below).
    # Defaults to `false`.
    ssr: false,

    # Whether to raise an exception when server-side rendering fails.
    # Defaults to `true`.
    raise_on_ssr_failure: true
  ]
```

### Client-side setup

#### Configuring Vite for React

Add `@vitejs/plugin-react`:

```
$ cd assets
$ npm install -D --install-links @vitejs/plugin-react
```

Edit `assets/vite.config.js`:

```diff
  import { defineConfig } from "vite"
  import combo from "vite-plugin-combo"
+ import react from "@vitejs/plugin-react"

  export default defineConfig({
    plugins: [
      combo({
-       input: ["src/css/app.css", "src/js/app.js"],
+       input: ["src/js/app.jsx"],
        staticDir: "../priv/static",
      }),
+     react(),
    ],
  })
```

#### Installing React and Inertia adapter

```
$ cd assets
$ npm install -S --install-links @inertiajs/react react react-dom
```

#### Creating the Inertia app

Next, rename `app.js` to `app.jsx` and update it to create your Inertia app:

```javascript
// assets/src/js/app.jsx
import "vite/modulepreload-polyfill"

import "@fontsource-variable/instrument-sans"
import "../css/app.css"

import { createInertiaApp } from "@inertiajs/react"
import { createRoot } from "react-dom/client"

createInertiaApp({
  resolve: (name) => {
    const page = `./pages/${name}.jsx`
    const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
    return pages[page]
  },
  setup({ el, App, props }) {
    createRoot(el).render(<App {...props} />)
  },
})
```

The `resolve` callback tells Inertia how to load a page component. It receives a page name as string, and returns a page component module. By default we recommend eager loading your components, which will result in a single JavaScript bundle. However, if you'd like to lazy-load your components, you can modify the `resolve` callback like this:

```javascript
{
  // ...
  resolve: (name) => {
    const page = `./pages/${name}.jsx`
    const pages = import.meta.glob("./pages/**/*.jsx") // remove the {eager: true} option
    return pages[page]() // add parentheses at the end
  }
  // ...
}
```

See [the code splitting documentation of Inertia](https://inertiajs.com/code-splitting) for more information.

The `setup` callback receives everything necessary to initialize the client-side framework, including the root Inertia `App` component.

The above code assumes your pages live in the `assets/src/js/pages` directory and have a default export with page component, like this:

```javascript
// assets/js/src/pages/Home.jsx

export default Home({ msg }) {
  return <p>This is the home page. {msg}</p>
}
```

#### Setting up CSRF protection

`Combo.Inertia` sets the CSRF token to `CSRF-TOKEN` cookie, and `Combo` expects to receive the CSRF token via the `X-CSRF-TOKEN` header

But, Axios, the HTTP library that Inertia uses under the hood, uses the following CSRF related config by default:

```
axios.defaults.xsrfCookieName = "XSRF-TOKEN"
axios.defaults.xsrfHeaderName = "X-XSRF-TOKEN"
```

To make them work together, we should setup Axios:

```diff
  // assets/src/js/app.jsx
  import "vite/modulepreload-polyfill"

  import "@fontsource-variable/instrument-sans"
  import "../css/app.css"

+ import axios from "axios"
  import { createInertiaApp } from "@inertiajs/react"
  import { createRoot } from "react-dom/client"

+ axios.defaults.xsrfCookieName = "CSRF-TOKEN"
+ axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN"

  createInertiaApp({
    resolve: (name) => {
      const page = `./pages/${name}.jsx`
      const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
      return pages[page]
    },
    setup({ el, App, props }) {
      createRoot(el).render(<App {...props} />)
    },
  })
```

## Setting up SSR (optional)

Inertia comes with with server-side rendering (SSR) support.

> The steps for enabling SSR similar to other backend frameworks, but instead of running a separate Node.js server process to render HTML, `Combo.Inertia` spins up a pool of Node.js process workers to handle SSR calls and manages the state of those node processes from your Elixir process tree. This is mostly just an implementation detail that you don't need to be concerned about, but we'll highlight how our `ssr.js` script differs from the Inertia docs.

> To run Combo and a Node.js process pool with 1 process, you need at least 512MiB of memory. Otherwise, the machine may experience out-of-memory (OOM) errors or severe slowness.

### Client-side setup

#### Adding the SSR entrypoint

Create a Node.js module that exports a `render` function to perform the actual server-side rendering of pages. Let's name it `ssr.jsx`.

```javascript
// assets/src/js/ssr.jsx
import { createInertiaApp } from "@inertiajs/react"
import ReactDOMServer from "react-dom/server"

export function render(page) {
  return createInertiaApp({
    page,
    render: ReactDOMServer.renderToString,
    resolve: (name) => {
      const page = `./pages/${name}.jsx`
      const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
      return pages[page]
    },
    setup: ({ App, props }) => <App {...props} />,
  })
}
```

> This is similar to the server entry-point [documented here](https://inertiajs.com/server-side-rendering#add-server-entry-point), except we are simply exporting a function called `render`, instead of starting a Node.js server process.

#### Configuring Vite for the SSR entrypoint

Configure vite to build `assets/src/js/ssr.jsx`, and put the bundled `ssr.js` into `priv/ssr`.

```diff
  // assets/vite.config.js
  import { defineConfig } from "vite"
  import combo from "vite-plugin-combo"
  import react from "@vitejs/plugin-react"

  export default defineConfig({
    plugins: [
      combo({
        input: ["src/js/app.jsx"],
        staticDir: "../priv/static",
+       ssrInput: ["src/js/ssr.jsx"],
+       ssrOutDir: "../priv/ssr",
      }),
      react(),
    ],
  })
```

#### Modifying the CSR entrypoint

When SSR is enabled, `hydrateRoot` should be used.

```diff
  // assets/src/js/app.jsx
  import "vite/modulepreload-polyfill"

  import "@fontsource-variable/instrument-sans"
  import "../css/app.css"

  import axios from "axios";
  import { createInertiaApp } from "@inertiajs/react";
- import { createRoot } from "react-dom/client";
+ import { createRoot, hydrateRoot } from "react-dom/client";

  axios.defaults.xsrfCookieName = "CSRF-TOKEN"
  axios.defaults.xsrfHeaderName = "X-CSRF-TOKEN"

+ function ssr_mode() {
+   return document.documentElement.hasAttribute("data-ssr");
+ }

  createInertiaApp({
    resolve: (name) => {
      const page = `./pages/${name}.jsx`
      const pages = import.meta.glob("./pages/**/*.jsx", { eager: true })
      return pages[page]
    },
    setup({ el, App, props }) {
-     createRoot(el).render(<App {...props} />)
+     if (ssr_mode()) {
+       hydrateRoot(el, <App {...props} />);
+     } else {
+       createRoot(el).render(<App {...props} />)
+     }
    },
  })
```

#### Updating npm script

Update the `build` script in `package.json` to build the new `ssr.js` file.

```diff
 "scripts": {
    "dev": "vite",
-   "build": "vite build",
+   "build": "vite build && vite build --ssr",
    // ...
  },
```

Now you can build both your client-side and server-side bundles.

```
$ npm run build
```

#### Updating .gitignore

Since `priv/ssr/` is for generated file, add it to your `.gitignore` file.

```diff
  # .gitignore

+ # Ignore files that are produced for SSR by assets build tools.
+ /priv/ssr/
```

### Server-side setup

#### Setting up `Combo.Inertia.SSR`

First, add the `Combo.Inertia.SSR` module to the of supervision tree:

```elixir
# lib/my_app/web/supervisor.ex
defmodule MyApp.Web.Supervisor do
  use Supervisor

  @spec start_link(term()) :: Supervisor.on_start()
  def start_link(arg) do
    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
  end

  @impl Supervisor
  def init(_arg) do
    children =
      Enum.concat(
        inertia_children(),
        [
          MyApp.Web.Endpoint
        ]
      )

    Supervisor.init(children, strategy: :one_for_one)
  end

  defp inertia_children do
    config = Application.get_env(:my_app, MyApp.Web.Endpoint)
    ssr? = get_in(config, [:inertia, :ssr])

    if ssr? do
      path = Path.join([Application.app_dir(:my_app), "priv/ssr"])
      [{Combo.Inertia.SSR, endpoint: MyApp.Web.Endpoint, path: path}]
    else
      []
    end
  end
end
```

Then, update your config to enable SSR for production environment:

```elixir
# config/prod.exs
config :my_app, MyApp.Web.Endpoint,
  inertia: [
    ssr: true
  ]
```

## Setting up Inertia helper (optional)

Create a little helper can be used in the `resolve` function.

First, `assets/src/js/inertia-helper.js`:

```javascript
// An Inertia helper for resolving page component.
//
// # Usage
//
// Use it in the `resolve` function.
//
// ## Resolve page component
//
//     createInertiaApp({
//       // ...
//       resolve: (name) =>
//         resolvePageComponent(
//           `./pages/${name}.jsx`,
//           import.meta.glob("./pages/**/*.jsx", { eager: true }),
//         ),
//     })
//
// ## Resolve page component with a fallback name
//
//     createInertiaApp({
//       // ...
//       resolve: (name) =>
//         resolvePageComponent(
//           `./pages/${name}.jsx`,
//           import.meta.glob("./pages/**/*.jsx", { eager: true }),
//           { fallbackName: "./pages/404.jsx" },
//         ),
//     })
//
export async function resolvePageComponent(name, pages, options) {
  if (typeof pages[name] === "undefined" && options?.fallbackName) {
    name = options.fallbackName
  }

  const page = pages[name]

  if (typeof page !== "undefined") {
    // When code spiltting is enabled, page is a function.
    // Or, page is an object.
    return typeof page === "function" ? page() : page
  } else {
    throw new Error(`Page not found: ${name}`)
  }
}
```

Then, use it in your `assets/src/js/app.jsx` and `assets/src/js/ssr.jsx`.

## Rendering responses

Rendering an Inertia response looks like this:

```elixir
defmodule MyApp.Web.PageController do
  use MyApp.Web, :controller

  def index(conn, _params) do
    conn
    |> inertia_put_prop(:msg, "Hello world")
    |> inertia_render("Home")
  end
end
```

## Shared data

To share data on every request, you can use the `inertia_put_prop/3` function inside of a plug in your response pipeline. For example, suppose you have a `UserAuth` plug responsible for fetching the current user and you want to be sure all your Inertia components receive that user data. Your plug can be something like this:

```elixir
defmodule MyApp.Web.UserAuth do
  import Plug.Conn
  import Combo.Conn
  import Combo.Inertia.Conn

  def authenticate_user(conn, _opts) do
    user = get_user_from_session(conn)

    conn
    |> assign(:user, user)
    # put a serialized represention of the user to Inertia props.
    |> inertia_put_prop(:user, serialize_user(user))
  end

  # ...
end
```

Anywhere this plug is used, the serialized `user` prop will be passed to the Inertia components.

## Lazy data evaluation

- `Combo.Inertia.Conn.inertia_optional/1`
- `Combo.Inertia.Conn.inertia_always/1`

## Deferred props

- `Combo.Inertia.Conn.inertia_defer/1`
- `Combo.Inertia.Conn.inertia_defer/2`

## Merging props

- `Combo.Inertia.Conn.inertia_merge/1`
- `Combo.Inertia.Conn.inertia_deep_merge/1`

## Once props

- `Combo.Inertia.Conn.inertia_once/1`

## Scroll props

- `Combo.Inertia.Conn.inertia_scroll/1`
- `Combo.Inertia.Conn.inertia_scroll/2`

## History encryption

### Global encryption

To enable history encryption globally, use:

```elixir
config :my_app, MyApp.Web.Endpoint,
  inertia: [
    encrypt_history: true
  ]
```

### Per-request encryption

To encrypt the history of an individual request, use:

- `Combo.Inertia.Conn.inertia_encrypt_history/1`
- `Combo.Inertia.Conn.inertia_encrypt_history/2`

### Clearing history

To clear the history state, use:

- `Combo.Inertia.Conn.inertia_clear_history/1`
- `Combo.Inertia.Conn.inertia_clear_history/2`

## Distinctive features

### Camelizing props

Combo.Inertia allows to automatically convert your prop keys from snake case (conventional in Elixir) to camel case (conventional in JavaScript), like `first_name` to `firstName`.

To configure it globally:

```elixir
import Config

config :my_app, MyApp.Web.Endpoint
  inertia: [
    camelize_props: true
  ]
```

To configure it on a per-request basis.

```elixir
defmodule MyApp.Web.PageController do
  use MyApp.Web, :controller

  def index(conn, _params) do
    conn
    |> inertia_put_prop(:first_name, "Bob")
    |> inertia_camelize_props()
    |> inertia_render("Welcome")
  end
end
```

### Flash messages

`Combo.Inertia` automatically includes Combo flash data in Inertia props, under the `flash` key.

For example, given the following controller action:

```elixir
def update(conn, params) do
  case MyApp.Settings.update(params) do
    {:ok, _settings} ->
      conn
      |> put_flash(:info, "Settings updated")
      |> redirect(to: "/settings")

    {:error, changeset} ->
      conn
      |> inertia_put_errors(changeset)
      |> redirect(to: "/settings")
  end
end
```

When redirecting to the `/settings` page, the Inertia component will receive the `flash` prop:

```javascript
{
  "component": "...",
  "props": {
    "flash": {
      "info": "Settings updated"
    },
    // ...
  }
}
```

### Validations

Validation errors follow some specific conventions to make wiring up with Inertia's form helpers seamless. The `errors` prop is managed by `Combo.Inertia` and is always included in the props object for Inertia components. (When there are no errors, the `errors` prop will be an empty object).

The `inertia_put_errors` function is how you tell Inertia what errors should be represented on the front-end. By default, you can either pass an `Ecto.Changeset` struct or a bare map to it. For other error data types, you may implement the `Combo.Inertia.Errors` protocol:

```elixir
def update(conn, params) do
  case MyApp.Settings.update(params) do
    {:ok, _settings} ->
      conn
      |> put_flash(:info, "Settings updated")
      |> redirect(to: "/settings")

    {:error, changeset} ->
      conn
      |> inertia_put_errors(changeset)
      |> redirect(to: "/settings")
  end
end
```

The `inertia_put_errors` function will convert the changeset errors into a shape compatible with the client-side adapter. Since Inertia expects a flat map of key-value pairs, the error serializer will flatten nested errors down to compound keys:

```javascript
{
  "name" => "can't be blank",

  // Nested errors keys are flattened with a dot separator (`.`)
  "team.name" => "must be at least 3 characters long",

  // Nested arrays are zero-based and indexed using bracket notation (`[0]`)
  "items[1].price" => "must be greater than 0"
}
```

Errors are automatically preserved across redirects, so you can safely respond with a redirect back to page where the form lives to display form errors.

If you need to construct your own map of errors (rather than pass in a changeset), be sure it's a flat mapping of atom (or string) keys to string values like this:

```elixir
conn
|> inertia_put_errors(%{
  name: "Name can't be blank",
  password: "Password must be at least 5 characters"
})
```

## Testing

- `Combo.Inertia.Testing`

We recommend importing `Combo.Inertia.Testing` in your `ConnCase` helper:

```elixir
defmodule MyApp.Web.ConnCase do
  use ExUnit.CaseTemplate

  using do
    quote do
      import Combo.Inertia.Testing

      # ...
    end
  end
end
```

## Deployment

There's only one thing to note - make Node.js running in production mode, which is configured by setting following environment variable:

```
NODE_ENV="production"
```

Why? In short:

- To get best SSR performance. Node.js running in production mode will cache the SSR module in memory.
- To avoid memory leaks.

> Performance comparison for rendering a simple page when testing on an M1 MacBook Pro:
>
> - Node.js running in production mode - `4ms`
> - Node.js running in non-production mode - `15ms`

## More

Visit <https://inertiajs.com/>.
