waveterm/tsunami/vdom/vdom.go
Mike Sawka e7cd584659
tsunami framework (waveapps v2) (#2315)
Huge PR.  135 commits here to rebuild waveapps into the "Tsunami" framework.

* Simplified API
* Updated system.md prompt
* Basic applications building and running
* /api/config and /api/data support
* tailwind styling
* no need for async updates
* goroutine/timer primitives for async routing handling
* POC for integrating 3rd party react frameworks (recharts)
* POC for server side components (table.go)
* POC for interacting with apps via /api/config (tsunamiconfig)

Checkpoint.  Still needs to be tightly integrated with Wave (lifecycle, AI interaction, etc.) but looking very promising 🚀
2025-09-11 14:25:07 -07:00

178 lines
4.9 KiB
Go

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package vdom
import (
"fmt"
"reflect"
"strings"
)
// ReactNode types = nil | string | Elem
type Component[P any] func(props P) *VDomElem
// WithKey sets the key property of the VDomElem and returns the element.
// This is particularly useful for defined components since their prop types won't include keys.
// Returns nil if the element is nil, otherwise returns the same element for chaining.
func (e *VDomElem) WithKey(key any) *VDomElem {
if e == nil {
return nil
}
if e.Props == nil {
e.Props = make(map[string]any)
}
e.Props[KeyPropKey] = fmt.Sprint(key)
return e
}
func textElem(text string) VDomElem {
return VDomElem{Tag: TextTag, Text: text}
}
func partToClasses(class any) []string {
if class == nil {
return nil
}
switch c := class.(type) {
case string:
if c != "" {
return []string{c}
}
case []string:
var parts []string
for _, s := range c {
if s != "" {
parts = append(parts, s)
}
}
return parts
case map[string]bool:
var parts []string
for k, v := range c {
if v && k != "" {
parts = append(parts, k)
}
}
return parts
case []any:
var parts []string
for _, item := range c {
parts = append(parts, partToClasses(item)...)
}
return parts
}
return nil
}
// Classes combines multiple class values into a single space-separated string.
// Similar to the JavaScript clsx library, it accepts:
// - strings: added directly if non-empty
// - nil: ignored (useful for vdom.If() statements)
// - []string: all non-empty strings are added
// - map[string]bool: keys with true values are added
// - []any: recursively processed
//
// Returns a space-separated string of all valid class names.
func Classes(classes ...any) string {
var parts []string
for _, class := range classes {
parts = append(parts, partToClasses(class)...)
}
return strings.Join(parts, " ")
}
// H creates a VDomElem with the specified tag, properties, and children.
// This is the primary function for creating virtual DOM elements.
// Children can be strings, VDomElems, *VDomElem, slices, booleans, numeric types,
// or other types which are converted to strings using fmt.Sprint.
// nil children are allowed and removed from the final list.
func H(tag string, props map[string]any, children ...any) *VDomElem {
rtn := &VDomElem{Tag: tag, Props: props}
if len(children) > 0 {
for _, part := range children {
elems := ToElems(part)
rtn.Children = append(rtn.Children, elems...)
}
}
return rtn
}
// If returns the provided part if the condition is true, otherwise returns nil.
// This is useful for conditional rendering in VDOM children lists, props, and style attributes.
func If(cond bool, part any) any {
if cond {
return part
}
return nil
}
// IfElse returns part if the condition is true, otherwise returns elsePart.
// This provides ternary-like conditional logic for VDOM children, props, and attributes.
// Accepts mixed types - part and elsePart don't need to be the same type, which is especially useful for children.
func IfElse(cond bool, part any, elsePart any) any {
if cond {
return part
}
return elsePart
}
// Ternary returns trueRtn if the condition is true, otherwise returns falseRtn.
// Unlike IfElse, this enforces type safety by requiring both return values to be the same type T.
func Ternary[T any](cond bool, trueRtn T, falseRtn T) T {
if cond {
return trueRtn
} else {
return falseRtn
}
}
// ForEach applies a function to each item in a slice and returns a slice of results.
// The function receives the item and its index, and can return any type for flexible VDOM generation.
func ForEach[T any](items []T, fn func(T, int) any) []any {
elems := make([]any, 0, len(items))
for idx, item := range items {
fnResult := fn(item, idx)
elems = append(elems, fnResult)
}
return elems
}
// ToElems converts various types into VDomElem slices for use in VDOM children.
// It handles strings, booleans, VDomElems, *VDomElem, slices, and other types
// by converting them to appropriate VDomElem representations.
// nil values are ignored and removed from the final slice.
// This is primarily an internal function and not typically called directly by application code.
func ToElems(part any) []VDomElem {
if part == nil {
return nil
}
switch partTyped := part.(type) {
case string:
return []VDomElem{textElem(partTyped)}
case bool:
// matches react
if partTyped {
return []VDomElem{textElem("true")}
}
return nil
case VDomElem:
return []VDomElem{partTyped}
case *VDomElem:
if partTyped == nil {
return nil
}
return []VDomElem{*partTyped}
default:
partVal := reflect.ValueOf(part)
if partVal.Kind() == reflect.Slice {
var rtn []VDomElem
for i := 0; i < partVal.Len(); i++ {
rtn = append(rtn, ToElems(partVal.Index(i).Interface())...)
}
return rtn
}
return []VDomElem{textElem(fmt.Sprint(part))}
}
}