mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge pull request #110 from johndoe6345789/codex/define-json-event-schema-and-implement-dispatcher
Add JSON event map handling in JSON UI renderer
This commit is contained in:
@@ -91,16 +91,49 @@ Connect UI to data sources:
|
||||
|
||||
### Event Handling
|
||||
|
||||
Respond to user interactions:
|
||||
Respond to user interactions using a JSON event map. Each entry maps an event name to an action definition:
|
||||
|
||||
```json
|
||||
{
|
||||
"events": {
|
||||
"onClick": "save-data"
|
||||
"onClick": {
|
||||
"action": "save-data",
|
||||
"payload": {
|
||||
"source": "profile"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also pass full action arrays when needed:
|
||||
|
||||
```json
|
||||
{
|
||||
"events": {
|
||||
"change": {
|
||||
"actions": [
|
||||
{ "id": "set-name", "type": "set-value", "target": "userName" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Supported events
|
||||
|
||||
Events map directly to React handler props, so common values include:
|
||||
|
||||
- `click` / `onClick`
|
||||
- `change` / `onChange`
|
||||
- `submit` / `onSubmit`
|
||||
- `focus` / `onFocus`
|
||||
- `blur` / `onBlur`
|
||||
- `keyDown` / `onKeyDown`
|
||||
- `keyUp` / `onKeyUp`
|
||||
- `mouseEnter` / `onMouseEnter`
|
||||
- `mouseLeave` / `onMouseLeave`
|
||||
|
||||
### Looping
|
||||
|
||||
Render lists from arrays:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createElement, useMemo, Fragment } from 'react'
|
||||
import { UIComponent, Binding, ComponentRendererProps } from '@/types/json-ui'
|
||||
import { UIComponent, Binding, ComponentRendererProps, EventHandler, JSONEventDefinition, JSONEventMap } from '@/types/json-ui'
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { resolveDataBinding, evaluateCondition } from './utils'
|
||||
|
||||
@@ -9,6 +9,69 @@ function resolveBinding(binding: Binding, data: Record<string, unknown>): unknow
|
||||
|
||||
export function ComponentRenderer({ component, data, context = {}, onEvent }: ComponentRendererProps) {
|
||||
const mergedData = useMemo(() => ({ ...data, ...context }), [data, context])
|
||||
const resolvedEventHandlers = useMemo(() => {
|
||||
const normalizeEventName = (eventName: string) =>
|
||||
eventName.startsWith('on') && eventName.length > 2
|
||||
? `${eventName.charAt(2).toLowerCase()}${eventName.slice(3)}`
|
||||
: eventName
|
||||
|
||||
const normalizeDefinition = (eventName: string, definition: JSONEventDefinition | string): EventHandler | null => {
|
||||
if (!definition) return null
|
||||
const normalizedEventName = normalizeEventName(eventName)
|
||||
if (typeof definition === 'string') {
|
||||
return {
|
||||
event: normalizedEventName,
|
||||
actions: [{ id: definition, type: 'custom' }],
|
||||
}
|
||||
}
|
||||
|
||||
if (definition.actions?.length) {
|
||||
const actions = definition.payload
|
||||
? definition.actions.map((action) => ({
|
||||
...action,
|
||||
params: action.params ?? definition.payload,
|
||||
}))
|
||||
: definition.actions
|
||||
return {
|
||||
event: normalizedEventName,
|
||||
actions,
|
||||
condition: definition.condition,
|
||||
}
|
||||
}
|
||||
|
||||
if (definition.action) {
|
||||
return {
|
||||
event: normalizedEventName,
|
||||
actions: [{ id: definition.action, type: 'custom', params: definition.payload }],
|
||||
condition: definition.condition,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
if (!component.events) {
|
||||
return [] as EventHandler[]
|
||||
}
|
||||
|
||||
if (Array.isArray(component.events)) {
|
||||
return component.events.map((handler) => ({
|
||||
...handler,
|
||||
event: normalizeEventName(handler.event),
|
||||
}))
|
||||
}
|
||||
|
||||
const eventMap = component.events as JSONEventMap
|
||||
return Object.entries(eventMap).flatMap(([eventName, definition]) => {
|
||||
if (Array.isArray(definition)) {
|
||||
return definition
|
||||
.map((entry) => normalizeDefinition(eventName, entry))
|
||||
.filter(Boolean) as EventHandler[]
|
||||
}
|
||||
const normalized = normalizeDefinition(eventName, definition)
|
||||
return normalized ? [normalized] : []
|
||||
})
|
||||
}, [component.events])
|
||||
const resolvedProps = useMemo(() => {
|
||||
const resolved: Record<string, unknown> = { ...component.props }
|
||||
|
||||
@@ -26,8 +89,8 @@ export function ComponentRenderer({ component, data, context = {}, onEvent }: Co
|
||||
}
|
||||
}
|
||||
|
||||
if (component.events && onEvent) {
|
||||
component.events.forEach(handler => {
|
||||
if (resolvedEventHandlers.length > 0 && onEvent) {
|
||||
resolvedEventHandlers.forEach(handler => {
|
||||
resolved[`on${handler.event.charAt(0).toUpperCase()}${handler.event.slice(1)}`] = (e: unknown) => {
|
||||
const conditionMet = !handler.condition
|
||||
|| (typeof handler.condition === 'function'
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import type { Action, EventHandler, JSONFormRendererProps, JSONUIRendererProps, UIComponent } from './types'
|
||||
import type {
|
||||
Action,
|
||||
EventHandler,
|
||||
JSONEventDefinition,
|
||||
JSONEventMap,
|
||||
JSONFormRendererProps,
|
||||
JSONUIRendererProps,
|
||||
UIComponent,
|
||||
} from './types'
|
||||
import { getUIComponent } from './component-registry'
|
||||
import { resolveDataBinding, evaluateCondition } from './utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -79,35 +87,39 @@ export function JSONUIRenderer({
|
||||
? eventName
|
||||
: `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`
|
||||
|
||||
const normalizeLegacyHandler = (eventName: string, handler: any): EventHandler | null => {
|
||||
if (!handler) return null
|
||||
const normalizeEventDefinition = (
|
||||
eventName: string,
|
||||
definition: JSONEventDefinition | string
|
||||
): EventHandler | null => {
|
||||
if (!definition) return null
|
||||
const normalizedEvent = normalizeEventName(eventName)
|
||||
|
||||
if (typeof handler === 'string') {
|
||||
if (typeof definition === 'string') {
|
||||
return {
|
||||
event: normalizeEventName(eventName),
|
||||
actions: [{ id: handler, type: 'custom' }],
|
||||
event: normalizedEvent,
|
||||
actions: [{ id: definition, type: 'custom' }],
|
||||
}
|
||||
}
|
||||
|
||||
if (handler.actions && Array.isArray(handler.actions)) {
|
||||
if (definition.actions && Array.isArray(definition.actions)) {
|
||||
const actions = definition.payload
|
||||
? definition.actions.map((action) => ({
|
||||
...action,
|
||||
params: action.params ?? definition.payload,
|
||||
}))
|
||||
: definition.actions
|
||||
return {
|
||||
event: normalizeEventName(eventName),
|
||||
actions: handler.actions as Action[],
|
||||
condition: handler.condition,
|
||||
event: normalizedEvent,
|
||||
actions,
|
||||
condition: definition.condition,
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof handler === 'object' && handler.action) {
|
||||
if (definition.action) {
|
||||
return {
|
||||
event: normalizeEventName(eventName),
|
||||
actions: [
|
||||
{
|
||||
id: handler.action,
|
||||
type: 'custom',
|
||||
target: handler.target,
|
||||
params: handler.params,
|
||||
},
|
||||
],
|
||||
event: normalizedEvent,
|
||||
actions: [{ id: definition.action, type: 'custom', params: definition.payload }],
|
||||
condition: definition.condition,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +131,20 @@ export function JSONUIRenderer({
|
||||
renderContext: Record<string, unknown>
|
||||
) => {
|
||||
const eventHandlers: EventHandler[] = Array.isArray(component.events)
|
||||
? component.events
|
||||
? component.events.map((handler) => ({
|
||||
...handler,
|
||||
event: normalizeEventName(handler.event),
|
||||
}))
|
||||
: component.events
|
||||
? Object.entries(component.events).map(([eventName, handler]) =>
|
||||
normalizeLegacyHandler(eventName, handler)
|
||||
).filter(Boolean) as EventHandler[]
|
||||
? Object.entries(component.events as JSONEventMap).flatMap(([eventName, handler]) => {
|
||||
if (Array.isArray(handler)) {
|
||||
return handler
|
||||
.map((entry) => normalizeEventDefinition(eventName, entry))
|
||||
.filter(Boolean) as EventHandler[]
|
||||
}
|
||||
const normalized = normalizeEventDefinition(eventName, handler)
|
||||
return normalized ? [normalized] : []
|
||||
})
|
||||
: []
|
||||
|
||||
if (eventHandlers.length > 0) {
|
||||
|
||||
@@ -48,6 +48,22 @@ export const EventHandlerSchema = z.object({
|
||||
condition: z.any().optional(),
|
||||
})
|
||||
|
||||
export const JSONEventDefinitionSchema = z.object({
|
||||
action: z.string().optional(),
|
||||
actions: z.array(ActionSchema).optional(),
|
||||
payload: z.record(z.string(), z.any()).optional(),
|
||||
condition: z.any().optional(),
|
||||
})
|
||||
|
||||
export const JSONEventMapSchema = z.record(
|
||||
z.string(),
|
||||
z.union([
|
||||
z.string(),
|
||||
JSONEventDefinitionSchema,
|
||||
z.array(JSONEventDefinitionSchema),
|
||||
])
|
||||
)
|
||||
|
||||
export const ConditionalSchema = z.object({
|
||||
if: z.string(),
|
||||
then: z.any().optional(),
|
||||
@@ -69,7 +85,7 @@ export const UIComponentSchema: any = z.object({
|
||||
z.string(),
|
||||
DataBindingSchema,
|
||||
]).optional(),
|
||||
events: z.array(EventHandlerSchema).optional(),
|
||||
events: z.union([z.array(EventHandlerSchema), JSONEventMapSchema]).optional(),
|
||||
conditional: ConditionalSchema.optional(),
|
||||
loop: z.object({
|
||||
source: z.string(),
|
||||
@@ -242,6 +258,8 @@ export type UIValue = z.infer<typeof UIValueSchema>
|
||||
export type DataBinding = z.infer<typeof DataBindingSchema>
|
||||
export type Action = z.infer<typeof ActionSchema>
|
||||
export type EventHandler = z.infer<typeof EventHandlerSchema>
|
||||
export type JSONEventDefinition = z.infer<typeof JSONEventDefinitionSchema>
|
||||
export type JSONEventMap = z.infer<typeof JSONEventMapSchema>
|
||||
export type Conditional = z.infer<typeof ConditionalSchema>
|
||||
export type UIComponent = z.infer<typeof UIComponentSchema>
|
||||
export type FormField = z.infer<typeof FormFieldSchema>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Action, EventHandler, FormField, UIComponent } from './schema'
|
||||
import type { Action, EventHandler, FormField, JSONEventDefinition, JSONEventMap, UIComponent } from './schema'
|
||||
|
||||
export type { Action, EventHandler, FormField, UIComponent }
|
||||
export type { Action, EventHandler, FormField, JSONEventDefinition, JSONEventMap, UIComponent }
|
||||
|
||||
export interface JSONUIRendererProps {
|
||||
component: UIComponent
|
||||
|
||||
@@ -56,6 +56,15 @@ export interface EventHandler {
|
||||
condition?: string | ((data: Record<string, any>) => boolean)
|
||||
}
|
||||
|
||||
export interface JSONEventDefinition {
|
||||
action?: string
|
||||
actions?: Action[]
|
||||
payload?: Record<string, any>
|
||||
condition?: string | ((data: Record<string, any>) => boolean)
|
||||
}
|
||||
|
||||
export type JSONEventMap = Record<string, JSONEventDefinition | JSONEventDefinition[] | string>
|
||||
|
||||
export interface Conditional {
|
||||
if: string
|
||||
then?: UIComponent | (UIComponent | string)[] | string
|
||||
@@ -76,7 +85,7 @@ export interface UIComponent {
|
||||
style?: Record<string, any>
|
||||
bindings?: Record<string, Binding>
|
||||
dataBinding?: string | Binding
|
||||
events?: EventHandler[]
|
||||
events?: EventHandler[] | JSONEventMap
|
||||
children?: UIComponent[] | string
|
||||
condition?: Binding
|
||||
conditional?: Conditional
|
||||
|
||||
Reference in New Issue
Block a user