README.md

# Inertia Wisp

[![Package Version](https://img.shields.io/hexpm/v/inertia_wisp)](https://hex.pm/packages/inertia_wisp)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/inertia_wisp/)

An [Inertia.js](https://inertiajs.com/) adapter for Gleam and the [Wisp](https://github.com/gleam-wisp/wisp) web framework.

## Installation

```sh
gleam add inertia_wisp
```

## Quick Start

### 1. Define Your HTML Layout

First, create a layout function that generates your HTML document structure. You can use Lustre's HTML builder or the built-in default layout:

```gleam
// src/layout.gleam
import gleam/json
import lustre/attribute
import lustre/element
import lustre/element/html

/// Custom HTML layout built with Lustre
pub fn html_layout(component_name: String, app_data: json.Json) -> String {
  html.html([attribute.attribute("lang", "en")], [
    html.head([], [
      html.meta([attribute.attribute("charset", "utf-8")]),
      html.meta([
        attribute.name("viewport"),
        attribute.attribute("content", "width=device-width, initial-scale=1"),
      ]),
      html.title([], component_name),
      html.link([
        attribute.rel("stylesheet"),
        attribute.href("/static/css/styles.css"),
      ]),
    ]),
    html.body([], [
      html.div(
        [
          attribute.id("app"),
          attribute.attribute("data-page", json.to_string(app_data)),
        ],
        [],
      ),
      html.script(
        [attribute.type_("module"), attribute.src("/static/js/main.js")],
        "",
      ),
    ]),
  ])
  |> element.to_document_string()
}
```

Alternatively, use the built-in default layout:

```gleam
import inertia_wisp/html

// Use html.default_layout for a simple starting point
```

### 2. Basic Server Setup

```gleam
import gleam/erlang/process
import mist
import wisp
import wisp/wisp_mist
import inertia_wisp/inertia
import layout  // Your custom layout module

pub fn main() {
  wisp.configure_logger()

  let assert Ok(_) =
    fn(req) { handle_request(req) }
    |> wisp_mist.handler("your_secret_key")
    |> mist.new
    |> mist.port(8000)
    |> mist.start_http

  process.sleep_forever()
}

fn handle_request(req: wisp.Request) -> wisp.Response {
  use <- wisp.serve_static(req, from: "./static", under: "/static")

  case wisp.path_segments(req) {
    [] -> home_page(req)
    ["about"] -> about_page(req)
    _ -> wisp.not_found()
  }
}
```

### 3. Create Your Pages

Define your page props type and create pages:

```gleam
import gleam/dict
import gleam/json
import inertia_wisp/inertia

// Define your props type
pub type HomePageProps {
  HomePageProps(
    message: String,
    user: String,
    count: Int,
  )
}

// Create encoder for your props
fn encode_home_page_props(props: HomePageProps) -> dict.Dict(String, json.Json) {
  dict.from_list([
    #("message", json.string(props.message)),
    #("user", json.string(props.user)),
    #("count", json.int(props.count)),
  ])
}

fn home_page(req: wisp.Request) -> wisp.Response {
  let props = HomePageProps(
    message: "Hello from Gleam!",
    user: "Alice",
    count: 42,
  )

  req
  |> inertia.response_builder("Home")
  |> inertia.props(props, encode_home_page_props)
  |> inertia.response(200, layout.html_layout)
}

fn about_page(req: wisp.Request) -> wisp.Response {
  let props = AboutPageProps(title: "About Us")

  req
  |> inertia.response_builder("About")
  |> inertia.props(props, encode_about_page_props)
  |> inertia.response(200, layout.html_layout)
}
```

### 4. Frontend Setup

#### Install Dependencies

Create a `frontend/` directory and add a `package.json`. You can use your preferred JavaScript build tools; this example uses ESBuild with code splitting enabled:

```json
{
  "name": "my-app-frontend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "esbuild src/main.tsx --bundle --outdir=../priv/static/js --format=esm --splitting --jsx=automatic --minify",
    "watch": "esbuild src/main.tsx --bundle --outdir=../priv/static/js --format=esm --splitting --jsx=automatic --watch"
  },
  "dependencies": {
    "@inertiajs/react": "^2.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "esbuild": "^0.19.0",
    "typescript": "^5.3.0"
  }
}
```

Install dependencies:

```bash
npm install
```

#### Create Entry Point

Create `src/main.tsx`:

```typescript
import React from "react";
import { createRoot } from "react-dom/client";
import { createInertiaApp } from "@inertiajs/react";

createInertiaApp({
  title: (title) => `${title} - My App`,
  resolve: async (name) => {
    const module = await import(`./Pages/${name}.tsx`);
    return module.default;
  },
  setup({ el, App, props }) {
    const root = createRoot(el);
    root.render(<App {...props} />);
  },
  progress: {
    color: "#9333ea",
  },
});
```

#### Create React Components

Create your React components in `src/Pages/` that correspond to your Gleam page components:

```tsx
// src/Pages/Home.tsx
import { Link } from '@inertiajs/react'

export default function Home({ message, user, count }: {
  message: string;
  user: string;
  count: number;
}) {
  return (
    <div>
      <h1>Welcome {user}!</h1>
      <p>{message}</p>
      <p>Count: {count}</p>
      <Link href="/about">Go to About</Link>
    </div>
  );
}

// src/Pages/About.tsx
import { Link } from '@inertiajs/react'

export default function About({ title }: { title: string }) {
  return (
    <div>
      <h1>{title}</h1>
      <Link href="/">Back to Home</Link>
    </div>
  );
}
```

#### Build Your Frontend

From the `frontend/` directory, build for production:

```bash
npm run build
```

Or watch for changes during development:

```bash
npm run watch
```

This will output your bundled JavaScript to `../priv/static/js/`, which your Gleam server will serve.

## Advanced Usage

### Lazy, Optional, and Always Props

```gleam
import gleam/option.{type Option}

pub type DashboardProps {
  DashboardProps(
    auth: User,              // Always included
    stats: Stats,            // Default prop
    notifications: Option(List(Notification)),  // Lazy-loaded
  )
}

fn dashboard_page(req: wisp.Request) -> wisp.Response {
  let props = DashboardProps(
    auth: get_current_user(req),
    stats: get_user_stats(),
    notifications: option.None,  // Not loaded initially
  )

  req
  |> inertia.response_builder("Dashboard")
  |> inertia.props(props, encode_dashboard_props)
  |> inertia.always("auth")  // Include in all requests, even partial reloads
  |> inertia.lazy("notifications", fn(props) {
      // Lazy evaluation - only computed when needed
      Ok(DashboardProps(
        ..props,
        notifications: option.Some(get_notifications()),
      ))
    })
  |> inertia.response(200, layout.html_layout)
}
```

### Deferred Props

Deferred props are loaded after the initial page render, perfect for heavy background operations:

```gleam
import gleam/option.{type Option, Some, None}
import gleam/erlang/process

pub type AnalyticsPageProps {
  AnalyticsPageProps(
    basic_stats: Stats,
    heavy_report: Option(Report),
  )
}

fn analytics_page(req: wisp.Request) -> wisp.Response {
  let props = AnalyticsPageProps(
    basic_stats: get_basic_stats(),
    heavy_report: None,
  )

  req
  |> inertia.response_builder("Analytics")
  |> inertia.props(props, encode_analytics_props)
  |> inertia.defer("heavy_report", fn(props) {
      // This runs in a separate request after page loads
      process.sleep(2000)  // Simulate expensive operation
      Ok(AnalyticsPageProps(
        ..props,
        heavy_report: Some(generate_heavy_report()),
      ))
    })
  |> inertia.response(200, layout.html_layout)
}
```

### Form Handling and Validation

```gleam
import gleam/dict

pub type ContactFormProps {
  ContactFormProps(
    name: String,
    email: String,
    message: String,
  )
}

pub fn show_contact_form(req: wisp.Request) -> wisp.Response {
  let props = ContactFormProps(name: "", email: "", message: "")

  req
  |> inertia.response_builder("ContactForm")
  |> inertia.props(props, encode_contact_form_props)
  |> inertia.response(200, layout.html_layout)
}

pub fn submit_contact_form(req: wisp.Request) -> wisp.Response {
  use json_data <- wisp.require_json(req)

  let form_data = decode_contact_form(json_data)

  case validate_contact_form(form_data) {
    Ok(_) -> {
      // Success - redirect
      wisp.redirect("/thank-you")
    }
    Error(validation_errors) -> {
      // Re-render with errors
      req
      |> inertia.response_builder("ContactForm")
      |> inertia.errors(validation_errors)
      |> inertia.redirect("/contact")
    }
  }
}

fn validate_contact_form(form) -> Result(_, dict.Dict(String, String)) {
  let errors = dict.new()

  // Add validation logic
  let errors = case form.email {
    "" -> dict.insert(errors, "email", "Email is required")
    _ -> errors
  }

  case dict.is_empty(errors) {
    True -> Ok(form)
    False -> Error(errors)
  }
}
```

### Merge Props

Merge props allow efficient client-side merging of data, useful for infinite scroll or pagination:

```gleam
import gleam/option

pub type UsersListProps {
  UsersListProps(
    users: List(User),
    page: Int,
  )
}

fn users_list(req: wisp.Request, page: Int) -> wisp.Response {
  let props = UsersListProps(
    users: get_users(page),
    page: page,
  )

  req
  |> inertia.response_builder("UsersList")
  |> inertia.props(props, encode_users_list_props)
  |> inertia.merge("users", match_on: option.Some(["id"]), deep: False)
  |> inertia.response(200, layout.html_layout)
}
```

## Documentation

- **[API Documentation](https://hexdocs.pm/inertia_wisp)** - Complete API reference
- **[Inertia.js Documentation](https://inertiajs.com/)** - Official Inertia.js docs

## Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

## License

MIT