| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- <script module lang="ts">
- import { getContext } from "svelte";
- import type {
- ActionBinding,
- ActionConfirm,
- ActionHandler,
- ResolvedAction,
- } from "@json-render/core";
- const ACTION_KEY = Symbol.for("json-render-actions");
- /**
- * Pending confirmation state
- */
- export interface PendingConfirmation {
- action: ResolvedAction;
- handler: ActionHandler;
- resolve: () => void;
- reject: () => void;
- }
- export interface CurrentValue<T> {
- readonly current: T;
- }
- /**
- * Action context value
- */
- export interface ActionContext {
- /** Registered action handlers */
- handlers: Record<string, ActionHandler>;
- /** Currently loading action names */
- loadingActions: Set<string>;
- /** Pending confirmation dialog */
- pendingConfirmation: PendingConfirmation | null;
- /** Execute an action binding */
- execute: (binding: ActionBinding) => Promise<void>;
- /** Confirm the pending action */
- confirm: () => void;
- /** Cancel the pending action */
- cancel: () => void;
- /** Register an action handler */
- registerHandler: (name: string, handler: ActionHandler) => void;
- }
- /**
- * Get the action context from component tree
- */
- export function getActionContext(): ActionContext {
- const ctx = getContext<ActionContext>(ACTION_KEY);
- if (!ctx) {
- throw new Error(
- "getActionContext must be called within an ActionProvider",
- );
- }
- return ctx;
- }
- /**
- * Convenience helper to get a registered action handler by name
- */
- export function getAction(
- name: string,
- ): CurrentValue<ActionHandler | undefined> {
- const context = getActionContext();
- return {
- get current() {
- return context.handlers[name];
- },
- };
- }
- /**
- * Props for ConfirmDialog component
- */
- export interface ConfirmDialogProps {
- /** The confirmation config */
- confirm: ActionConfirm;
- /** Called when confirmed */
- onConfirm: () => void;
- /** Called when cancelled */
- onCancel: () => void;
- }
- /**
- * Generate a unique ID for use with the "$id" token.
- */
- let idCounter = 0;
- function generateUniqueId(): string {
- idCounter += 1;
- return `${Date.now()}-${idCounter}`;
- }
- /**
- * Deep-resolve dynamic value references within an object.
- */
- function deepResolveValue(
- value: unknown,
- get: (path: string) => unknown,
- ): unknown {
- if (value === null || value === undefined) return value;
- if (value === "$id") {
- return generateUniqueId();
- }
- if (typeof value === "object" && !Array.isArray(value)) {
- const obj = value as Record<string, unknown>;
- const keys = Object.keys(obj);
- if (keys.length === 1 && typeof obj.$state === "string") {
- return get(obj.$state as string);
- }
- if (keys.length === 1 && "$id" in obj) {
- return generateUniqueId();
- }
- }
- if (Array.isArray(value)) {
- return value.map((item) => deepResolveValue(item, get));
- }
- if (typeof value === "object") {
- const resolved: Record<string, unknown> = {};
- for (const [key, val] of Object.entries(
- value as Record<string, unknown>,
- )) {
- resolved[key] = deepResolveValue(val, get);
- }
- return resolved;
- }
- return value;
- }
- </script>
- <script lang="ts">
- import { setContext, type Snippet } from "svelte";
- import {
- resolveAction,
- executeAction,
- nextActionDispatchId,
- notifyActionDispatch,
- notifyActionSettle,
- type ActionBinding as CoreActionBinding,
- type ActionHandler as CoreActionHandler,
- } from "@json-render/core";
- import { getStateContext } from "./StateProvider.svelte";
- import { getOptionalValidationContext } from "./ValidationProvider.svelte";
- import { SvelteSet } from "svelte/reactivity";
- interface Props {
- handlers?: Record<string, CoreActionHandler>;
- navigate?: (path: string) => void;
- children?: Snippet;
- }
- let { handlers = {}, navigate, children }: Props = $props();
- const stateCtx = getStateContext();
- const validation = getOptionalValidationContext();
- let registeredHandlers = $state.raw<Record<string, CoreActionHandler>>({});
- let loadingActions = new SvelteSet<string>();
- let pendingConfirmation = $state.raw<PendingConfirmation | null>(null);
- const execute = async (binding: CoreActionBinding): Promise<void> => {
- const resolved = resolveAction(binding, stateCtx.getSnapshot());
- // --- devtools / observer hooks ---
- const dispatchId = nextActionDispatchId();
- const dispatchedAt = Date.now();
- notifyActionDispatch({
- id: dispatchId,
- name: resolved.action,
- params: resolved.params,
- at: dispatchedAt,
- });
- let __ok = true;
- let __error: unknown = undefined;
- try {
- if (resolved.action === "setState" && resolved.params) {
- const statePath = resolved.params.statePath as string;
- const value = resolved.params.value;
- if (statePath) {
- stateCtx.set(statePath, value);
- }
- return;
- }
- if (resolved.action === "pushState" && resolved.params) {
- const statePath = resolved.params.statePath as string;
- const rawValue = resolved.params.value;
- if (statePath) {
- const resolvedValue = deepResolveValue(rawValue, stateCtx.get);
- const arr = (stateCtx.get(statePath) as unknown[] | undefined) ?? [];
- stateCtx.set(statePath, [...arr, resolvedValue]);
- const clearStatePath = resolved.params.clearStatePath as
- | string
- | undefined;
- if (clearStatePath) {
- stateCtx.set(clearStatePath, "");
- }
- }
- return;
- }
- if (resolved.action === "removeState" && resolved.params) {
- const statePath = resolved.params.statePath as string;
- const index = resolved.params.index as number;
- if (statePath !== undefined && index !== undefined) {
- const arr = (stateCtx.get(statePath) as unknown[] | undefined) ?? [];
- stateCtx.set(
- statePath,
- arr.filter((_, i) => i !== index),
- );
- }
- return;
- }
- if (resolved.action === "validateForm") {
- if (!validation?.validateAll) {
- console.warn(
- "validateForm action was dispatched but no ValidationProvider is connected. " +
- "Ensure ValidationProvider is rendered inside the provider tree.",
- );
- return;
- }
- const valid = validation.validateAll();
- const errors: Record<string, string[]> = {};
- for (const [path, fieldState] of Object.entries(validation.fieldStates)) {
- if (fieldState.result && !fieldState.result.valid) {
- errors[path] = fieldState.result.errors;
- }
- }
- const statePath =
- (resolved.params?.statePath as string) || "/formValidation";
- stateCtx.set(statePath, { valid, errors });
- return;
- }
- if (resolved.action === "push" && resolved.params) {
- const screen = resolved.params.screen as string;
- if (screen) {
- const currentScreen = stateCtx.get("/currentScreen") as
- | string
- | undefined;
- const navStack =
- (stateCtx.get("/navStack") as string[] | undefined) ?? [];
- if (currentScreen) {
- stateCtx.set("/navStack", [...navStack, currentScreen]);
- } else {
- stateCtx.set("/navStack", [...navStack, ""]);
- }
- stateCtx.set("/currentScreen", screen);
- }
- return;
- }
- if (resolved.action === "pop") {
- const navStack =
- (stateCtx.get("/navStack") as string[] | undefined) ?? [];
- if (navStack.length > 0) {
- const previousScreen = navStack[navStack.length - 1];
- stateCtx.set("/navStack", navStack.slice(0, -1));
- if (previousScreen) {
- stateCtx.set("/currentScreen", previousScreen);
- } else {
- stateCtx.set("/currentScreen", undefined);
- }
- }
- return;
- }
- const handler =
- registeredHandlers[resolved.action] ?? handlers[resolved.action];
- if (!handler) {
- console.warn(`No handler registered for action: ${resolved.action}`);
- return;
- }
- if (resolved.confirm) {
- return new Promise<void>((resolve, reject) => {
- pendingConfirmation = {
- action: resolved,
- handler,
- resolve: () => {
- pendingConfirmation = null;
- resolve();
- },
- reject: () => {
- pendingConfirmation = null;
- reject(new Error("Action cancelled"));
- },
- };
- }).then(async () => {
- loadingActions.add(resolved.action);
- try {
- await executeAction({
- action: resolved,
- handler,
- setState: stateCtx.set,
- navigate,
- executeAction: async (name) => {
- const subBinding: CoreActionBinding = { action: name };
- await execute(subBinding);
- },
- });
- } finally {
- loadingActions.delete(resolved.action);
- }
- });
- }
- loadingActions.add(resolved.action);
- try {
- await executeAction({
- action: resolved,
- handler,
- setState: stateCtx.set,
- navigate,
- executeAction: async (name) => {
- const subBinding: CoreActionBinding = { action: name };
- await execute(subBinding);
- },
- });
- } finally {
- loadingActions.delete(resolved.action);
- }
- } catch (err) {
- __ok = false;
- __error = err;
- throw err;
- } finally {
- const now = Date.now();
- notifyActionSettle({
- id: dispatchId,
- name: resolved.action,
- ok: __ok,
- at: now,
- durationMs: now - dispatchedAt,
- error: __error,
- });
- }
- };
- const ctx: ActionContext = {
- get handlers() {
- return { ...handlers, ...registeredHandlers };
- },
- get loadingActions() {
- return loadingActions;
- },
- get pendingConfirmation() {
- return pendingConfirmation;
- },
- execute,
- confirm: () => {
- pendingConfirmation?.resolve();
- },
- cancel: () => {
- pendingConfirmation?.reject();
- },
- registerHandler: (name: string, handler: CoreActionHandler) => {
- registeredHandlers = { ...registeredHandlers, [name]: handler };
- },
- };
- setContext(ACTION_KEY, ctx);
- </script>
- {@render children?.()}
|