renderer.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import type {
  2. Catalog,
  3. ComputedFunction,
  4. DirectiveDefinition,
  5. SchemaDefinition,
  6. Spec,
  7. StateStore,
  8. UIElement,
  9. } from "@json-render/core";
  10. import type { Component, Snippet } from "svelte";
  11. import type {
  12. BaseComponentProps,
  13. EventHandle,
  14. SetState,
  15. StateModel,
  16. } from "./catalog-types.js";
  17. import CatalogRenderer from "./CatalogRenderer.svelte";
  18. /**
  19. * Props passed to component renderers
  20. */
  21. export interface ComponentRenderProps<P = Record<string, unknown>> {
  22. /** The element being rendered */
  23. element: UIElement<string, P>;
  24. /** Rendered children snippet */
  25. children?: Snippet;
  26. /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. */
  27. emit: (event: string) => void;
  28. /** Get an event handle with metadata */
  29. on: (event: string) => EventHandle;
  30. /**
  31. * Two-way binding paths resolved from `$bindState` / `$bindItem` expressions.
  32. * Maps prop name → absolute state path for write-back.
  33. */
  34. bindings?: Record<string, string>;
  35. /** Whether the parent is loading */
  36. loading?: boolean;
  37. }
  38. /**
  39. * Component renderer type - a Svelte component that receives ComponentRenderProps
  40. */
  41. export type ComponentRenderer<P = Record<string, unknown>> = Component<
  42. ComponentRenderProps<P>
  43. >;
  44. /**
  45. * Registry of component renderers.
  46. * Maps component type names to Svelte components.
  47. */
  48. export type ComponentRegistry = Record<string, ComponentRenderer<any>>;
  49. /**
  50. * Action handler function for defineRegistry
  51. */
  52. type DefineRegistryActionFn = (
  53. params: Record<string, unknown> | undefined,
  54. setState: SetState,
  55. state: StateModel,
  56. ) => Promise<void>;
  57. /**
  58. * Result returned by defineRegistry
  59. */
  60. export interface DefineRegistryResult {
  61. /** Component registry for Renderer */
  62. registry: ComponentRegistry;
  63. /**
  64. * Create ActionProvider-compatible handlers.
  65. */
  66. handlers: (
  67. getSetState: () => SetState | undefined,
  68. getState: () => StateModel,
  69. ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;
  70. /**
  71. * Execute an action by name imperatively
  72. */
  73. executeAction: (
  74. actionName: string,
  75. params: Record<string, unknown> | undefined,
  76. setState: SetState,
  77. state?: StateModel,
  78. ) => Promise<void>;
  79. }
  80. /**
  81. * Create a registry from a catalog with Svelte components and/or actions.
  82. *
  83. * Components must accept `BaseComponentProps` as their props interface.
  84. *
  85. * @example
  86. * ```ts
  87. * import { defineRegistry } from "@json-render/svelte";
  88. * import Card from "./components/Card.svelte";
  89. * import Button from "./components/Button.svelte";
  90. * import { myCatalog } from "./catalog";
  91. *
  92. * const { registry, handlers } = defineRegistry(myCatalog, {
  93. * components: {
  94. * Card,
  95. * Button,
  96. * },
  97. * actions: {
  98. * submit: async (params, setState) => {
  99. * // handle action
  100. * },
  101. * },
  102. * });
  103. * ```
  104. */
  105. export function defineRegistry<
  106. C extends Catalog,
  107. TComponents extends Record<string, Component<BaseComponentProps<any>>>,
  108. >(
  109. _catalog: C,
  110. options: {
  111. /** Svelte components that accept BaseComponentProps */
  112. components?: TComponents;
  113. /** Action handlers */
  114. actions?: Record<string, DefineRegistryActionFn>;
  115. },
  116. ): DefineRegistryResult {
  117. const registry: ComponentRegistry = {};
  118. if (options.components) {
  119. for (const [name, componentFn] of Object.entries(options.components)) {
  120. registry[name] = (_, props) =>
  121. (componentFn as Component<BaseComponentProps<any>>)(_, {
  122. get props() {
  123. return props.element.props;
  124. },
  125. get children() {
  126. return props.children;
  127. },
  128. get emit() {
  129. return props.emit;
  130. },
  131. get on() {
  132. return props.on;
  133. },
  134. get bindings() {
  135. return props.bindings;
  136. },
  137. get loading() {
  138. return props.loading;
  139. },
  140. });
  141. }
  142. }
  143. // Build action helpers
  144. const actionMap = options.actions
  145. ? (Object.entries(options.actions) as Array<
  146. [string, DefineRegistryActionFn]
  147. >)
  148. : [];
  149. const handlers = (
  150. getSetState: () => SetState | undefined,
  151. getState: () => StateModel,
  152. ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {
  153. const result: Record<
  154. string,
  155. (params: Record<string, unknown>) => Promise<void>
  156. > = {};
  157. for (const [name, actionFn] of actionMap) {
  158. result[name] = async (params) => {
  159. const setState = getSetState();
  160. const state = getState();
  161. if (setState) {
  162. await actionFn(params, setState, state);
  163. }
  164. };
  165. }
  166. return result;
  167. };
  168. const executeAction = async (
  169. actionName: string,
  170. params: Record<string, unknown> | undefined,
  171. setState: SetState,
  172. state: StateModel = {},
  173. ): Promise<void> => {
  174. const entry = actionMap.find(([name]) => name === actionName);
  175. if (entry) {
  176. await entry[1](params, setState, state);
  177. } else {
  178. console.warn(`Unknown action: ${actionName}`);
  179. }
  180. };
  181. return { registry, handlers, executeAction };
  182. }
  183. // ============================================================================
  184. // createRenderer
  185. // ============================================================================
  186. /**
  187. * Props for renderers created with createRenderer
  188. */
  189. export interface CreateRendererProps {
  190. spec: Spec | null;
  191. store?: StateStore;
  192. state?: Record<string, unknown>;
  193. onAction?: (actionName: string, params?: Record<string, unknown>) => void;
  194. onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
  195. /** Named functions for `$computed` expressions in props */
  196. functions?: Record<string, ComputedFunction>;
  197. /** Custom directives for user-defined `$`-prefixed dynamic values */
  198. directives?: DirectiveDefinition[];
  199. loading?: boolean;
  200. fallback?: Component;
  201. }
  202. /**
  203. * Component map type — maps component names to Svelte components
  204. */
  205. export type ComponentMap<
  206. TComponents extends Record<string, { props: unknown }>,
  207. > = {
  208. [K in keyof TComponents]: Component<any, any, string>;
  209. };
  210. /**
  211. * Create a renderer from a catalog
  212. *
  213. * @example
  214. * ```typescript
  215. * const DashboardRenderer = createRenderer(dashboardCatalog, {
  216. * Card,
  217. * Metric,
  218. * });
  219. *
  220. * // Usage in template
  221. * <DashboardRenderer spec={aiGeneratedSpec} {state} />
  222. * ```
  223. */
  224. export function createRenderer<
  225. TDef extends SchemaDefinition,
  226. TCatalog extends { components: Record<string, { props: unknown }> },
  227. >(
  228. _catalog: Catalog<TDef, TCatalog>,
  229. components: ComponentMap<TCatalog["components"]>,
  230. ): Component<CreateRendererProps> {
  231. const registry: ComponentRegistry =
  232. components as unknown as ComponentRegistry;
  233. return (_, props: CreateRendererProps) =>
  234. CatalogRenderer(_, {
  235. registry,
  236. get spec() {
  237. return props.spec;
  238. },
  239. get store() {
  240. return props.store;
  241. },
  242. get state() {
  243. return props.state;
  244. },
  245. get onAction() {
  246. return props.onAction;
  247. },
  248. get onStateChange() {
  249. return props.onStateChange;
  250. },
  251. get functions() {
  252. return props.functions;
  253. },
  254. get directives() {
  255. return props.directives;
  256. },
  257. get loading() {
  258. return props.loading;
  259. },
  260. get fallback() {
  261. return props.fallback;
  262. },
  263. });
  264. }