spot UI Framework

repository·main·Indexed 23 days ago

https://github.com/roblillack/spot

A component-based UI framework for building and managing UI trees. It features a distinction between pure rendering components and stateful Controls, providing a RenderContext for state management via UseState and side-effect handling via UseEffect. The framework allows developers to define UI using the Component interface or functional RenderFn, build node trees with Build(), and manage the UI lifecycle through Mount and Update operations.

Tokens
2.2K
Snippets
13
Records
15
Agent score
79%

What's inside spot

  1. Manage the component tree with Node

    main

    The Node struct represents a component in a tree structure. Each node contains a Content (which must satisfy the Control interface) and a slice of Children nodes. You can use Node to build and manage a hierarchical tree of components that can be mounted and updated.

    Key behaviors:

    • Mounting: Calling Mount() on a node triggers a recursive mounting process where each component's Mount method is called with its parent as the argument.
    • Updating: The Update method allows you to synchronize an existing node tree with a new one. It performs efficient updates by attempting to call Update on existing components. If a component cannot be updated (i.e., Update returns false), it is unmounted (if it implements Unmountable) and replaced with the new component.
    type Node struct {
    	Content  Control
    	Children []Node
    }
  2. Implement interactive UI elements with the Control interface

    main

    While Component is for pure rendering, Control is an interface for components that can be mounted, updated, or unmounted within the UI tree.

    • Control: Can be mounted into the UI tree via Mount(parent Control) any and updated via Update(next Control) bool.
    • Unmountable: An extension of Control that adds an Unmount() method to allow removal from the tree.
    • Container: A Control that owns other controls and implements BuildNode(ctx *RenderContext) Node to render itself and its children into a tree of nodes.
    type Control interface {
    	Component
    	Mount(parent Control) any
    	Update(next Control) bool
    }
    
    type Unmountable interface {
    	Control
    	Unmount()
    }
    
    type Container interface {
    	Control
    	BuildNode(ctx *RenderContext) Node
    }
  3. Trigger a UI update with RenderContext.TriggerUpdate

    main

    The TriggerUpdate method schedules a re-render of the component tree. It ensures the update runs on the main loop to satisfy UI thread requirements and prevent concurrent render cycles.

    When triggered, it:

    1. Resets the internal render count.
    2. Re-builds the entire node tree using BuildNode starting from the current content.
    3. Calls Update on the existing root node to apply the newTree changes.
    func (ctx *RenderContext) TriggerUpdate()
  4. Render components into a Node tree with Build()

    main

    To transform a Component into a tree of Node objects (which can then be mounted), use the Build function. This initializes a RenderContext and executes the rendering logic.

    rootNode := spot.Build(myComponent)
    func Build(el Component) Node
  5. Mount a Node tree

    main

    To initialize a component tree and trigger the lifecycle methods of its components, call Mount() on the root Node. This method recursively calls Mount(parent) on every Content component in the tree, passing the parent component as the argument to the child.

    func (n Node) Mount()
  6. Create a new node with RenderContext.Make

    main

    Use Make to create a new Node from a render function. This method initializes a sub-context using UseState, allowing the component to manage its own state and lifecycle. The provided render function is executed to produce the initial content, and the resulting node tree is stored in the context's root.

    func (ctx *RenderContext) Make(render func(*RenderContext) Component) Node
  7. Update a Node tree with another Node

    main

    To synchronize a Node tree with a new state represented by another Node tree, call Update(other Node, parent Control).

    This method performs the following logic:

    1. Content Update:
      • If the current content is removed, it calls Unmount() if the component implements Unmountable.
      • If the current content is replaced, it attempts to call Update(new) on the existing component. If Update returns false, the old component is unmounted and the new one is mounted.
    2. Children Update:
      • If the number of children changes, existing children are unmounted and the children slice is rebuilt.
      • If the number of children is the same, it iterates through them and calls updateChild to synchronize each child's content.
    func (n *Node) Update(other Node, parent Control)
  8. Manage component state with UseState

    main

    The UseState[T] function allows you to declare stateful variables within a component. It takes a *RenderContext and an initial value of type T. It returns the current state value and a setter function func(next T) used to update the state.

    When the setter function is called, it updates the value stored in the RenderContext and triggers a re-render of the component via ctx.TriggerUpdate(). The state is persisted across renders using the internal index provided by the RenderContext.

  9. Build a UI tree with RenderContext.BuildNode

    main

    The BuildNode method recursively renders a Component and its children into a tree of Node objects. It handles different component types as follows:

    • Fragment: Flattens its children into a single list of nodes.
    • Container: Calls the container's own BuildNode(ctx) method.
    • Control: Wraps the control directly into a Node.
    • Default: Calls the component's Render(ctx) method and recursively builds nodes from the result.
    func (ctx *RenderContext) BuildNode(component Component) Node
  10. Handle side effects with UseEffect

    main

    The UseEffect function manages side effects by running a provided function fn only when specific dependencies change.

    It accepts:

    • ctx *RenderContext: The current rendering context.
    • fn func(): The effect function to execute.
    • deps []any: A slice of dependencies.

    Behavior:

    1. Initial Mount: The effect always runs on the first call (when dependencies are nil or uninitialized).
    2. Dependency Tracking: On subsequent renders, UseEffect compares the current deps slice with the previous one. If any element in the slice has changed (using != comparison), the effect function fn() is executed.
    3. Panic Condition: The function will panic if the length of the deps slice changes between renders.

    Note: The effect function fn does not take any arguments and does not return a cleanup function.

  11. Mount a component directly to the UI

    main

    To quickly render and mount a component to the active UI, use the Mount or MountFn shortcuts. These functions handle the Build step and the subsequent Mount() call on the resulting node tree.

    Using a Component instance:

    spot.Mount(myComponent)

    Using a Render function:

    spot.MountFn(func(ctx *spot.RenderContext) spot.Component {
        return &MyComponent{}
    })
    func Mount(el Component) {
    	Build(el).Mount()
    }
    
    func MountFn(fn func(ctx *RenderContext) Component) {
    	Build(Make(fn)).Mount()
    }
  12. Create components from render functions with Make()

    main

    If you prefer a functional approach, you can use Make to convert a RenderFn into a Component. A RenderFn is a function with the signature func(ctx *RenderContext) Component.

    myComp := spot.Make(func(ctx *spot.RenderContext) spot.Component {
        return &SomeOtherComponent{}
    })
    func Make(fn RenderFn) Component