Skip to content

Component packages

The extension points assume a component lives in your own app. To distribute one — a signature pad, a chart widget, a domain-specific field — you would normally also publish an npm package for its React renderer, and keep it in lockstep with the PHP side. Lattice removes that second release pipeline: a component package ships its React renderer as source inside its Composer package, and the lattice() Vite plugin compiles it straight into your app’s existing build — resolving @lattice-php/lattice against the copy you already installed. A plain composer require is enough.

Four things — or let the generator write them. Point --package at a directory that has no composer.json yet and Lattice scaffolds the whole package on first use (the name and PSR-4 namespace are derived from the folder — packages/acme-signatureacme/signature, Acme\Signature\), then creates the component inside it:

Terminal window
php artisan lattice:component Signature --package=packages/acme-signature

1. A composer.json declaring the two Lattice entry points:

{
"name": "acme/lattice-signature",
"autoload": { "psr-4": { "Acme\\Signature\\": "src/" } },
"extra": {
"lattice": {
"plugin": "resources/js/plugin.ts",
"discover": ["src"]
}
}
}

2. The PHP component, carrying its wire type:

use Lattice\Lattice\Attributes\AsComponent;
use Lattice\Lattice\Core\Components\Component;
#[AsComponent('signature')]
final class Signature extends Component
{
public string $label = 'Sign here';
}

3. The React renderer for that type:

import type { RendererComponent } from "@lattice-php/lattice/core/types";
const Signature: RendererComponent<"signature"> = ({ node }) => (
<div className="rounded-lt-sm border border-lt-border p-4 text-lt-fg">
{String(node.props?.label ?? "Sign here")}
</div>
);
export default Signature;

4. The plugin entry that registers it:

import { createPlugin, lazyComponent } from "@lattice-php/lattice";
export default createPlugin({
name: "acme-signature",
components: {
signature: lazyComponent(() => import("./signature")),
},
});

The two extra.lattice keys are each read by one side of the stack:

  • plugin — the lattice() Vite plugin scans vendor/composer/installed.json, and for every package that declares it, grants Vite filesystem access to the package directory and exposes its plugin under the virtual module virtual:lattice/plugins. The package’s TSX compiles straight into the consumer’s bundle: no separate build step, one shared React instance, full tree-shaking.
  • discover — Lattice’s PHP discovery merges these roots into lattice.discover, so the package’s #[AsComponent] classes are picked up by php artisan lattice:typescript (which types node.props for the package’s components) and by definition discovery for any forms, tables, or pages the package also ships.

For the consumer, it is a single dependency:

Terminal window
composer require acme/lattice-signature

If you followed the installation guide, the discovered plugins are already registered — the standard bootstrap passes virtual:lattice/plugins to createLatticeApp, so an installed package registers itself with no further wiring:

import latticePlugins from "virtual:lattice/plugins";
createLatticeApp({ plugins: latticePlugins });

(Or merge them onto the registry yourself with extendRegistry(registry, ...latticePlugins).) Then use the component like any built-in:

Signature::make('signature')->label('Sign the contract');

See Registry and types for how the type string couples the two sides, and how generated types keep node.props sound.