import type {
Catalog,
ComputedFunction,
DirectiveDefinition,
SchemaDefinition,
Spec,
StateStore,
UIElement,
} from "@json-render/core";
import type { Component, Snippet } from "svelte";
import type {
BaseComponentProps,
EventHandle,
SetState,
StateModel,
} from "./catalog-types.js";
import CatalogRenderer from "./CatalogRenderer.svelte";
/**
* Props passed to component renderers
*/
export interface ComponentRenderProps
> {
/** The element being rendered */
element: UIElement;
/** Rendered children snippet */
children?: Snippet;
/** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. */
emit: (event: string) => void;
/** Get an event handle with metadata */
on: (event: string) => EventHandle;
/**
* Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
* Maps prop name → absolute state path for write-back.
*/
bindings?: Record;
/** Whether the parent is loading */
loading?: boolean;
}
/**
* Component renderer type - a Svelte component that receives ComponentRenderProps
*/
export type ComponentRenderer> = Component<
ComponentRenderProps
>;
/**
* Registry of component renderers.
* Maps component type names to Svelte components.
*/
export type ComponentRegistry = Record>;
/**
* Action handler function for defineRegistry
*/
type DefineRegistryActionFn = (
params: Record | undefined,
setState: SetState,
state: StateModel,
) => Promise;
/**
* Result returned by defineRegistry
*/
export interface DefineRegistryResult {
/** Component registry for Renderer */
registry: ComponentRegistry;
/**
* Create ActionProvider-compatible handlers.
*/
handlers: (
getSetState: () => SetState | undefined,
getState: () => StateModel,
) => Record) => Promise>;
/**
* Execute an action by name imperatively
*/
executeAction: (
actionName: string,
params: Record | undefined,
setState: SetState,
state?: StateModel,
) => Promise;
}
/**
* Create a registry from a catalog with Svelte components and/or actions.
*
* Components must accept `BaseComponentProps` as their props interface.
*
* @example
* ```ts
* import { defineRegistry } from "@json-render/svelte";
* import Card from "./components/Card.svelte";
* import Button from "./components/Button.svelte";
* import { myCatalog } from "./catalog";
*
* const { registry, handlers } = defineRegistry(myCatalog, {
* components: {
* Card,
* Button,
* },
* actions: {
* submit: async (params, setState) => {
* // handle action
* },
* },
* });
* ```
*/
export function defineRegistry<
C extends Catalog,
TComponents extends Record>>,
>(
_catalog: C,
options: {
/** Svelte components that accept BaseComponentProps */
components?: TComponents;
/** Action handlers */
actions?: Record;
},
): DefineRegistryResult {
const registry: ComponentRegistry = {};
if (options.components) {
for (const [name, componentFn] of Object.entries(options.components)) {
registry[name] = (_, props) =>
(componentFn as Component>)(_, {
get props() {
return props.element.props;
},
get children() {
return props.children;
},
get emit() {
return props.emit;
},
get on() {
return props.on;
},
get bindings() {
return props.bindings;
},
get loading() {
return props.loading;
},
});
}
}
// Build action helpers
const actionMap = options.actions
? (Object.entries(options.actions) as Array<
[string, DefineRegistryActionFn]
>)
: [];
const handlers = (
getSetState: () => SetState | undefined,
getState: () => StateModel,
): Record) => Promise> => {
const result: Record<
string,
(params: Record) => Promise
> = {};
for (const [name, actionFn] of actionMap) {
result[name] = async (params) => {
const setState = getSetState();
const state = getState();
if (setState) {
await actionFn(params, setState, state);
}
};
}
return result;
};
const executeAction = async (
actionName: string,
params: Record | undefined,
setState: SetState,
state: StateModel = {},
): Promise => {
const entry = actionMap.find(([name]) => name === actionName);
if (entry) {
await entry[1](params, setState, state);
} else {
console.warn(`Unknown action: ${actionName}`);
}
};
return { registry, handlers, executeAction };
}
// ============================================================================
// createRenderer
// ============================================================================
/**
* Props for renderers created with createRenderer
*/
export interface CreateRendererProps {
spec: Spec | null;
store?: StateStore;
state?: Record;
onAction?: (actionName: string, params?: Record) => void;
onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
/** Named functions for `$computed` expressions in props */
functions?: Record;
/** Custom directives for user-defined `$`-prefixed dynamic values */
directives?: DirectiveDefinition[];
loading?: boolean;
fallback?: Component;
}
/**
* Component map type — maps component names to Svelte components
*/
export type ComponentMap<
TComponents extends Record,
> = {
[K in keyof TComponents]: Component;
};
/**
* Create a renderer from a catalog
*
* @example
* ```typescript
* const DashboardRenderer = createRenderer(dashboardCatalog, {
* Card,
* Metric,
* });
*
* // Usage in template
*
* ```
*/
export function createRenderer<
TDef extends SchemaDefinition,
TCatalog extends { components: Record },
>(
_catalog: Catalog,
components: ComponentMap,
): Component {
const registry: ComponentRegistry =
components as unknown as ComponentRegistry;
return (_, props: CreateRendererProps) =>
CatalogRenderer(_, {
registry,
get spec() {
return props.spec;
},
get store() {
return props.store;
},
get state() {
return props.state;
},
get onAction() {
return props.onAction;
},
get onStateChange() {
return props.onStateChange;
},
get functions() {
return props.functions;
},
get directives() {
return props.directives;
},
get loading() {
return props.loading;
},
get fallback() {
return props.fallback;
},
});
}