PAD + islands

Typed props

The island is TypeScript, and the type of its props is written by pad types from the page's own data: the order the PHP makes, captured as the page's sample. A field the PHP does not have fails the check, not the visitor.

pad types_samples/$padExpose .tsxtsc --noEmit
Live

Order 1042 for Ada Fernández

How it is built

PAD, on the server the islands, built by Vite

<div data-island="OrderCard" data-props="{^order}">
  <p>Order {$order.number} for {$order.customer.name}</p>
</div>
<?php

  // An order as a page's PHP makes it - from a database, here an array. The island gets it as
  // its props, and its component, in TypeScript, knows its shape from the types pad types
  // wrote: the sample of this page, captured from this very data.

  $order = [ 'number'   => 1042,
             'status'   => 'shipped',
             'paid'     => TRUE,
             'note'     => NULL,
             'customer' => [ 'name' => 'Ada Fernández', 'city' => 'Amsterdam' ],
             'lines'    => [ [ 'product' => 'Aurora Headphones', 'quantity' => 1, 'price' => 189.0 ],
                             [ 'product' => 'Tactile Keyboard',  'quantity' => 2, 'price' => 119.0, 'gift' => TRUE ] ] ];

  $padExpose = [ 'order' ];

?>
// The order card, in TypeScript: its props are the order the page's PHP made, and their type
// is the one pad types wrote from the page's own data - src/types/pad.d.ts. A field the PHP
// does not have, or one used as the wrong type, fails npm run check before it fails a visitor.

import { useState } from 'react';
import type { ExamplesTypedVars } from '../types/pad';

type Order = ExamplesTypedVars['order'];
type Line = Order['lines'][number];

const money = (amount: number) => '€ ' + amount.toFixed(2);

export default function OrderCard(order: Order) {
  const [open, setOpen] = useState<Line | null>(null);
  const total = order.lines.reduce((sum, line) => sum + line.quantity * line.price, 0);

  return (
    <div className="card order">
      <div className="framework-head">
        <strong>Order #{order.number}</strong>
        <span className={'badge' + (order.paid ? ' is-good' : '')}>{order.status}{order.paid ? ' · paid' : ''}</span>
      </div>
      <p className="muted small">{order.customer.name}, {order.customer.city}</p>
      <ul className="order-lines">
        {order.lines.map((line) => (
          <li key={line.product} onClick={() => setOpen(open === line ? null : line)}>
            <span>{line.quantity} × {line.product}{line.gift ? ' 🎁' : ''}</span>
            <span>{money(line.quantity * line.price)}</span>
          </li>
        ))}
      </ul>
      <div className="order-total"><span>Total</span><strong>{money(total)}</strong></div>
      {order.note === null && <p className="muted small">No note: the order had none, so its type is null - capture an order with one, and it becomes string.</p>}
      <p className="framework-tag">React · TypeScript</p>
    </div>
  );
}
// The types of what the pages of islands hold - written by pad types islands examples/typed.
// Made from the data itself: run the command again when a page's data changes.

// examples/typed - the variables of its PHP
export interface ExamplesTypedVars {
  title: string;
  navSection: string;
  islandsCss: number;
  example: {
    page: string;
    title: string;
    icon: string;
    lede: string;
    server: unknown[];
    client: unknown[];
    files: unknown[];
    groupTitle: string;
    prev: {
      page: string;
      title: string;
    };
    next: {
      page: string;
      title: string;
    };
  };
  serverChips: unknown[];
  clientChips: unknown[];
  prev: {
    page: string;
    title: string;
  };
  next: {
    page: string;
    title: string;
  };
  sourceFiles: string;
  order: {
    number: number;
    status: string;
    paid: boolean;
    note: null;
    customer: {
      name: string;
      city: string;
    };
    lines: {
      product: string;
      quantity: number;
      price: number;
      gift?: boolean;
    }[];
  };
}

// examples/typed - its JSON answer, ?examples/typed&padFormat=json
export interface ExamplesTypedAnswer {
  order: {
    number: number;
    status: string;
    paid: boolean;
    note: null;
    customer: {
      name: string;
      city: string;
    };
    lines: {
      product: string;
      quantity: number;
      price: number;
      gift?: boolean;
    }[];
  };
}
{
    "title": "Typed - PAD + islands",
    "navSection": "index",
    "islandsCss": 1791452182,
    "example": {
        "page": "examples/typed",
        "title": "Typed",
        "icon": "",
        "lede": "",
        "server": [],
        "client": [],
        "files": [],
        "groupTitle": "Examples",
        "prev": {
            "page": "",
            "title": ""
        },
        "next": {
            "page": "",
            "title": ""
        }
    },
    "serverChips": [],
    "clientChips": [],
    "prev": {
        "page": "",
        "title": ""
    },
    "next": {
        "page": "",
        "title": ""
    },
    "sourceFiles": "",
    "order": {
        "number": 1042,
        "status": "shipped",
        "paid": true,
        "note": null,
        "customer": {
            "name": "Ada Fernández",
            "city": "Amsterdam"
        },
        "lines": [
            {
                "product": "Aurora Headphones",
                "quantity": 1,
                "price": 189
            },
            {
                "product": "Tactile Keyboard",
                "quantity": 2,
                "price": 119,
                "gift": true
            }
        ]
    }
}