Skip to content

feat(nano): Implement cross-platform action dispatch system for NanoRenderer #489

@phodal

Description

@phodal

Problem

Currently, NanoAction definitions in NanoIR are not functional. Actions like on_click, AddTask, DeleteTask exist in the DSL but cannot be executed by renderers across platforms.

Current State

// NanoAction.kt - Well-defined action types
sealed class NanoAction {
    data class StateMutation(val path: String, val operation: MutationOp, val value: String) : NanoAction()
    data class Navigate(val to: String) : NanoAction()
    data class Fetch(val url: String, val method: HttpMethod, ...) : NanoAction()
    data class ShowToast(val message: String) : NanoAction()
    data class Sequence(val actions: List<NanoAction>) : NanoAction()
}

But renderers only store actions as data attributes without execution:

// HtmlRenderer.kt
if (onSubmit != null) append(" data-action=\"$onSubmit\"")

Use Case

component TaskList(tasks: List[Task]):
    state:
        new_task: str = ""
    
    Card:
        HStack(spacing="sm"):
            Input(value := state.new_task, placeholder="Add new task...")
            Button("+", intent="primary"):
                on_click:
                    AddTask(title=state.new_task)  # Custom action
                    state.new_task = ""            # State mutation
        
        for task in tasks:
            HStack:
                Checkbox(checked := task.done)
                Text(task.title)
                Button("Delete", intent="danger"):
                    on_click: DeleteTask(id=task.id)

Proposed Solution

1. Action Handler Interface

Create a platform-agnostic action handler interface:

// NanoActionHandler.kt
interface NanoActionHandler {
    fun handleAction(action: NanoAction, context: NanoContext): ActionResult
    
    // Built-in action handlers
    fun handleStateMutation(mutation: StateMutation, context: NanoContext): ActionResult
    fun handleNavigate(navigate: Navigate, context: NanoContext): ActionResult
    fun handleFetch(fetch: Fetch, context: NanoContext): ActionResult
    fun handleShowToast(toast: ShowToast, context: NanoContext): ActionResult
    fun handleSequence(sequence: Sequence, context: NanoContext): ActionResult
    
    // Custom action hook - for user-defined actions like AddTask, DeleteTask
    fun handleCustomAction(name: String, payload: Map<String, Any>, context: NanoContext): ActionResult
}

sealed class ActionResult {
    object Success : ActionResult()
    data class Error(val message: String) : ActionResult()
    data class Async(val promise: Deferred<ActionResult>) : ActionResult()
}

2. State Management

Add reactive state container:

// NanoState.kt
class NanoState(initialState: Map<String, Any>) {
    private val _state = MutableStateFlow(initialState)
    val state: StateFlow<Map<String, Any>> = _state.asStateFlow()
    
    fun mutate(path: String, op: MutationOp, value: Any) {
        // Apply mutation and emit new state
    }
    
    fun get(path: String): Any?
    fun set(path: String, value: Any)
}

3. Platform Implementations

Compose (Kotlin)

class ComposeActionHandler : NanoActionHandler {
    private val navController: NavController? = null
    private val snackbarHost: SnackbarHostState? = null
    
    override fun handleNavigate(navigate: Navigate, context: NanoContext): ActionResult {
        navController?.navigate(navigate.to)
        return ActionResult.Success
    }
    
    override fun handleShowToast(toast: ShowToast, context: NanoContext): ActionResult {
        scope.launch { snackbarHost?.showSnackbar(toast.message) }
        return ActionResult.Success
    }
    
    override fun handleCustomAction(name: String, payload: Map<String, Any>, context: NanoContext): ActionResult {
        // Dispatch to user-provided custom action registry
        return customActionRegistry[name]?.invoke(payload, context) ?: ActionResult.Error("Unknown action: $name")
    }
}

React (TypeScript)

interface NanoActionHandler {
  handleAction(action: NanoAction, context: NanoContext): Promise<ActionResult>;
  onCustomAction?: (name: string, payload: Record<string, any>) => Promise<ActionResult>;
}

const ReactActionHandler: NanoActionHandler = {
  async handleAction(action, context) {
    switch (action.type) {
      case "StateMutation":
        context.setState(prev => applyMutation(prev, action));
        return { success: true };
      case "Navigate":
        window.location.href = action.to;
        return { success: true };
      case "Fetch":
        return await fetch(action.url, { method: action.method, body: action.body });
      case "ShowToast":
        toast(action.message);
        return { success: true };
      default:
        return this.onCustomAction?.(action.type, action.payload) ?? { error: "Unknown action" };
    }
  }
};

4. Renderer Integration

Update NanoRenderer to accept action handler:

interface NanoRenderer<T> {
    // Existing methods...
    
    // New: Action binding
    fun bindActionHandler(handler: NanoActionHandler)
    
    // Or pass in context
    fun render(ir: NanoIR, context: NanoRenderContext): T
}

data class NanoRenderContext(
    val state: NanoState,
    val actionHandler: NanoActionHandler,
    val theme: NanoTheme
)

5. Custom Action Registry

For user-defined actions like AddTask, DeleteTask:

// User code
val customActions = mapOf(
    "AddTask" to { payload, context ->
        val title = payload["title"] as String
        taskRepository.add(Task(title))
        ActionResult.Success
    },
    "DeleteTask" to { payload, context ->
        val id = payload["id"] as String
        taskRepository.delete(id)
        ActionResult.Success
    }
)

// Pass to renderer
val actionHandler = ComposeActionHandler(customActions)
NanoRenderer(ir, NanoRenderContext(state, actionHandler, theme))

Implementation Plan

  1. Create NanoActionHandler interface in xuiper-ui
  2. Create NanoState reactive state container
  3. Create NanoRenderContext to bundle state + actions + theme
  4. Implement ComposeActionHandler for Compose
  5. Implement ReactActionHandler for VSCode/React
  6. Update HtmlRenderer to generate JavaScript action bindings
  7. Add custom action registry mechanism
  8. Write integration tests

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions