Skip to content

Registry and types

Before registering custom components or columns, publish the scaffold file:

Terminal window
php artisan vendor:publish --tag=lattice-js

This writes a single resources/js/registry.ts. It defines an app plugin with empty component and table-column registries and merges it onto the built-in registry with extendRegistry, exporting the result as registry:

import { extendRegistry, registry as packageRegistry } from "@lattice-php/lattice";
import type { Plugin } from "@lattice-php/lattice";
export const registry = extendRegistry(packageRegistry, {
name: "app",
components: {}, // custom fields and UI components
extensions: {
"table.columns": {}, // custom column cells
},
} satisfies Plugin);

The generators (lattice:field, lattice:component, lattice:column) append their entries to this file automatically — fields and components under components, columns under extensions["table.columns"]. You only need to publish once, and you pass the exported registry to Provider.

components contains complete wire nodes. They have type, props, and optional children, and the core renderer owns their lifecycle. Form fields belong here because they are full nodes rendered in the form tree.

extensions contains named registries owned by a feature. A table column contributes a cell renderer to table.columns; a rich-editor extension contributes Tiptap behavior to form.rich-editor. Core only merges these registries—the table or editor decides how to use them.

The node registry maps type strings to RendererComponent functions. Imports come from @lattice-php/lattice.

A plugin is a plain object that bundles one or more component registrations. Use satisfies Plugin to check its shape without changing the inferred component keys:

import { eagerComponent } from "@lattice-php/lattice";
import type { Plugin } from "@lattice-php/lattice";
import { ColorPickerComponent } from "./fields/color-picker";
import { RatingComponent } from "./components/rating";
export const appPlugin = {
name: "app",
components: {
"field.color-picker": eagerComponent(ColorPickerComponent),
rating: eagerComponent(RatingComponent),
},
} satisfies Plugin;

loadPluginModules imports and validates precompiled plugin URLs. It is a core API, so both regular apps and the standalone build can load plugins before creating the app:

import { createLatticeApp, loadPluginModules } from "@lattice-php/lattice";
const plugins = await loadPluginModules(["/vendor/acme/plugin.js"]);
createLatticeApp({ plugins });

Merges a plugin into an existing registry, returning a new registry without mutating the original. The published resources/js/registry.ts already calls it for you — this is the pattern it uses:

import { extendRegistry, registry as packageRegistry } from "@lattice-php/lattice";
import type { Plugin } from "@lattice-php/lattice";
export const registry = extendRegistry(packageRegistry, {
name: "app",
components: {},
extensions: {
"table.columns": {},
},
} satisfies Plugin);

packageRegistry is Lattice’s built-in registry. Pass the extended registry to Provider. Call extendRegistry again yourself only if you keep additional plugins in their own files.

The built-in registry is a single flat map of eager components; the few heavy ones (the rich editor, chart, and date inputs) code-split their dependency from inside the component, so you never choose between an eager and a lazy variant. See Bundle size for the details.

Creates a registry from scratch (no built-ins). Only use this if you want to replace the entire built-in component set:

import { createRegistry } from "@lattice-php/lattice";
const minimalRegistry = createRegistry(appPlugin);

Components can be registered eagerly (imported at module load time) or lazily (code-split on first render):

import { eagerComponent, lazyComponent } from "@lattice-php/lattice";
import type { Plugin } from "@lattice-php/lattice";
import { RatingComponent } from "./components/rating";
export const appPlugin = {
name: "app",
components: {
// Eager — bundled with the entry point.
rating: eagerComponent(RatingComponent),
// Lazy — splits into a separate chunk loaded on demand.
"field.color-picker": lazyComponent(async () => ({
default: (await import("./fields/color-picker")).ColorPickerComponent,
})),
},
} satisfies Plugin;

Provider supplies the registry to every Lattice component below it in the tree:

import { Provider } from "@lattice-php/lattice";
createRoot(el).render(
<Provider registry={appRegistry}>
<App {...props} />
</Provider>,
);

A custom renderer receives its already-rendered child nodes as children. When you need the active component registry directly, use useComponentRegistry:

import { useComponentRegistry } from "@lattice-php/lattice";
const components = useComponentRegistry();

The column-cell registry maps type strings to ColumnCellComponent functions.

Column cell renderers use the same plugin object as components. They go in the named table.columns extension registry in resources/js/registry.ts (registered bare — columnCell() is optional, see below):

import { extendRegistry, registry as packageRegistry } from "@lattice-php/lattice";
import type { Plugin } from "@lattice-php/lattice";
import { StatusBadgeCell } from "./columns/status-badge";
export const registry = extendRegistry(packageRegistry, {
name: "app",
components: {},
extensions: {
"table.columns": {
"column.status-badge": StatusBadgeCell,
},
},
} satisfies Plugin);

The same exported registry carries both your components and your column cells — there is no second registry to merge.

Returns the current column registry from inside any component rendered by Lattice:

import { useColumnRegistry } from "@lattice-php/lattice";
const columnRegistry = useColumnRegistry();

@lattice-php/core exports the shared wire-type interfaces:

  • ComponentProps — maps a type string to its props shape for fields and UI components.
  • ColumnProps — maps a type string to its props shape for column cells.
  • FilterProps — maps a filter control to its props shape.
  • EffectProps — maps an effect type to its props shape.

EditorExtensionProps remains in @lattice-php/lattice because it belongs to the rich editor.

All of them use TypeScript’s declaration merging. You can augment them manually or let lattice:typescript do it.

Run this command whenever your PHP classes gain or lose public properties. It discovers #[AsComponent] components, #[AsColumn] columns, #[AsFilter] filters, #[AsEffect] effects, and #[AsEditorExtension] rich-editor extensions:

Terminal window
php artisan lattice:typescript

It scans the paths listed under discover in config/lattice.php:

config/lattice.php
'discover' => [
base_path('app'),
],

And writes an augmentation file to the path configured under typescript.output (default: resources/js/lattice/generated.d.ts):

// This file is generated by `php artisan lattice:typescript`. Do not edit.
declare module "@lattice-php/core" {
interface ComponentProps {
"field.color-picker": {
swatches: string | null;
};
}
interface ColumnProps {
"column.status-badge": {
colorMap: Record<string, string> | null;
};
}
}
export {};

Without this file, node.props and column.props fall back to Record<string, unknown>. The renderers still work — types are just not narrowed.

See Artisan commands for the full command reference.

If you prefer not to run the generator, augment the interfaces directly in any .d.ts file included in your tsconfig.json:

import "@lattice-php/core";
declare module "@lattice-php/core" {
interface ComponentProps {
"field.color-picker": {
swatches: string | null;
};
}
}