Skip to content
AzuAzu

Customising

Sooner or later a client wants something Azu doesn’t do. A SKU that’s always upper case, say, or a Print button. You get three levels of control. Start with the smallest one that works.

Labels, icons, groups, page size, and branding live in the resource or in config/azu.ts. No React needed. See Resources and config/azu.ts.

Point any field or column at your own React component. The demo upper-cases its SKUs as you type:

app/admin/resources/product_resource.ts (fields)
// inertia/admin/components/sku_input.tsx
field.text('sku').component('SkuInput').section('Details'),
inertia/admin/components/sku_input.tsx
// inertia/admin/components/sku_input.tsx
import { useId } from 'react'
import type { FieldProps } from '@mezielabs/azu-react'
export default function SkuInput({ field, value, onChange, errors }: FieldProps<string>) {
const id = useId()
const errorId = `${id}-error`
return (
<div className="azu-field">
<label className="azu-label" htmlFor={id}>
{field.label}
{field.required && (
<span className="azu-required" aria-hidden>
{' '}
*
</span>
)}
</label>
<input
id={id}
name={field.name}
className="azu-input"
value={value ?? ''}
onChange={(e) => onChange(e.target.value.toUpperCase())}
required={field.required}
aria-invalid={errors.length > 0}
aria-describedby={errors.length > 0 ? errorId : undefined}
/>
{errors.length > 0 && (
<span id={errorId} className="azu-field-error">
{errors.join(' ')}
</span>
)}
</div>
)
}

A field component gets FieldProps:

  • the value and onChange,
  • the field’s errors,
  • the field itself: its name, label, and the validator’s required hint,
  • the form’s other values, as record.

Note: a field component is responsible for its own label and errors. The demo’s renders both, with Azu’s classes, so it passes the same accessibility checks as Azu’s own controls.

A column works the same way. The demo shows stock as a coloured count:

app/admin/resources/product_resource.ts (columns)
// inertia/admin/components/stock_bar.tsx
column.custom('stock', 'StockBar').label('In stock'),
inertia/admin/components/stock_bar.tsx
// inertia/admin/components/stock_bar.tsx
import type { CellProps } from '@mezielabs/azu-react'
export default function StockBar({ value }: CellProps<number>) {
const stock = value ?? 0
return (
<span style={{ color: stock < 10 ? 'var(--azu-danger)' : undefined }}>{stock} in stock</span>
)
}

A cell gets CellProps: its value, the row as record, and the column. .component() works on every column kind. column.custom() is a column with no formatting of its own.

Components in inertia/admin/components are registered by file name, so sku_input.tsx becomes SkuInput.

Each one loads only when a page that uses it opens. So they add nothing to the admin’s initial JavaScript.

To add something to a page without replacing it, name a component for one of a resource’s slots. The demo adds a Print button to orders:

app/admin/resources/order_resource.ts (part)
// inertia/admin/components/print_order.tsx
slots: { toolbar: 'PrintOrder' },
inertia/admin/components/print_order.tsx
// inertia/admin/components/print_order.tsx
import type { SlotProps } from '@mezielabs/azu-react'
export default function PrintOrder({ page }: SlotProps) {
// The toolbar slot is on every Orders page; printing is for one order
if (page !== 'show') return null
return (
<button type="button" className="azu-button" onClick={() => window.print()}>
Print
</button>
)
}

Azu’s stylesheet leaves the sidebar, the top bar, and the page’s actions out of print. So the page prints the order alone.

The slots are:

Slot Where
header Under the page title, on every page of the resource.
toolbar Beside the page’s own buttons.
beforeTable Above the list.
afterForm Under the create and edit forms.

A slot component gets SlotProps:

  • the resource,
  • the page: index, create, edit, or show,
  • the record on the edit and detail pages,
  • the resource’s list URL, as path.

Copy any of Azu’s pages into your app and edit it however you like:

Terminal window
node ace azu:publish resource/index

This copies the page to inertia/admin/pages/resource/index.tsx. A comment at the top records the Azu version it came from.

  • Page names are snake_case paths, like your app’s own Inertia pages. The older Resource/Index spelling works too.
  • Run node ace azu:publish without a page to list them.
  • It won’t replace a page you’ve already published unless you pass --force.

Azu renders your copy from then on. It’s the most flexible option, but you now own that page through upgrades.