Skip to content

Commit 2893a07

Browse files
committed
small cleanups and add godocs for public vdom funcs...
1 parent f4e100b commit 2893a07

2 files changed

Lines changed: 130 additions & 27 deletions

File tree

tsunami/app/tsunamiapp.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,14 @@ func makeNullVDom() *vdom.VDomElem {
230230
return &vdom.VDomElem{WaveId: uuid.New().String(), Tag: vdom.WaveNullTag}
231231
}
232232

233+
func structToProps(props any) map[string]any {
234+
m, err := util.StructToMap(props)
235+
if err != nil {
236+
return nil
237+
}
238+
return m
239+
}
240+
233241
func DefineComponentEx[P any](client *Client, name string, renderFn func(ctx context.Context, props P) any) vdom.Component[P] {
234242
if name == "" {
235243
panic("Component name cannot be empty")
@@ -242,7 +250,7 @@ func DefineComponentEx[P any](client *Client, name string, renderFn func(ctx con
242250
panic(err)
243251
}
244252
return func(props P) *vdom.VDomElem {
245-
return vdom.H(name, vdom.Props(props))
253+
return vdom.H(name, structToProps(props))
246254
}
247255
}
248256

tsunami/vdom/vdom.go

Lines changed: 121 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ import (
1717

1818
type Component[P any] func(props P) *VDomElem
1919

20+
// Key returns the key property of the VDomElem as a string.
21+
// Returns an empty string if the key is not set, otherwise converts the key value to string using fmt.Sprint.
2022
func (e *VDomElem) Key() string {
2123
keyVal, ok := e.Props[KeyPropKey]
2224
if !ok {
2325
return ""
2426
}
25-
keyStr, ok := keyVal.(string)
26-
if ok {
27-
return keyStr
28-
}
29-
return ""
27+
return fmt.Sprint(keyVal)
3028
}
3129

30+
// WithKey sets the key property of the VDomElem and returns the element.
31+
// This is particularly useful for defined components since their prop types won't include keys.
32+
// Returns nil if the element is nil, otherwise returns the same element for chaining.
3233
func (e *VDomElem) WithKey(key string) *VDomElem {
3334
if e == nil {
3435
return nil
@@ -40,26 +41,67 @@ func (e *VDomElem) WithKey(key string) *VDomElem {
4041
return e
4142
}
4243

43-
func TextElem(text string) VDomElem {
44+
func textElem(text string) VDomElem {
4445
return VDomElem{Tag: TextTag, Text: text}
4546
}
4647

48+
func partToClasses(class any) []string {
49+
if class == nil {
50+
return nil
51+
}
52+
switch c := class.(type) {
53+
case string:
54+
if c != "" {
55+
return []string{c}
56+
}
57+
case []string:
58+
var parts []string
59+
for _, s := range c {
60+
if s != "" {
61+
parts = append(parts, s)
62+
}
63+
}
64+
return parts
65+
case map[string]bool:
66+
var parts []string
67+
for k, v := range c {
68+
if v && k != "" {
69+
parts = append(parts, k)
70+
}
71+
}
72+
return parts
73+
case []any:
74+
var parts []string
75+
for _, item := range c {
76+
parts = append(parts, partToClasses(item)...)
77+
}
78+
return parts
79+
}
80+
return nil
81+
}
82+
83+
// Classes combines multiple class values into a single space-separated string.
84+
// Similar to the JavaScript clsx library, it accepts:
85+
// - strings: added directly if non-empty
86+
// - nil: ignored (useful for vdom.If() statements)
87+
// - []string: all non-empty strings are added
88+
// - map[string]bool: keys with true values are added
89+
// - []any: recursively processed
90+
//
91+
// Returns a space-separated string of all valid class names.
4792
func Classes(classes ...any) string {
4893
var parts []string
4994
for _, class := range classes {
50-
switch c := class.(type) {
51-
case nil:
52-
continue
53-
case string:
54-
if c != "" {
55-
parts = append(parts, c)
56-
}
57-
}
58-
// Ignore any other types
95+
parts = append(parts, partToClasses(class)...)
5996
}
6097
return strings.Join(parts, " ")
6198
}
6299

100+
// H creates a VDomElem with the specified tag, properties, and children.
101+
// This is the primary function for creating virtual DOM elements.
102+
// Children can be strings, VDomElems, *VDomElem, slices, booleans, numeric types,
103+
// or other types which are converted to strings using fmt.Sprint.
104+
// nil children are allowed and removed from the final list.
63105
func H(tag string, props map[string]any, children ...any) *VDomElem {
64106
rtn := &VDomElem{Tag: tag, Props: props}
65107
if len(children) > 0 {
@@ -71,20 +113,27 @@ func H(tag string, props map[string]any, children ...any) *VDomElem {
71113
return rtn
72114
}
73115

116+
// If returns the provided part if the condition is true, otherwise returns nil.
117+
// This is useful for conditional rendering in VDOM children lists, props, and style attributes.
74118
func If(cond bool, part any) any {
75119
if cond {
76120
return part
77121
}
78122
return nil
79123
}
80124

125+
// IfElse returns part if the condition is true, otherwise returns elsePart.
126+
// This provides ternary-like conditional logic for VDOM children, props, and attributes.
127+
// Accepts mixed types - part and elsePart don't need to be the same type, which is especially useful for children.
81128
func IfElse(cond bool, part any, elsePart any) any {
82129
if cond {
83130
return part
84131
}
85132
return elsePart
86133
}
87134

135+
// Ternary returns trueRtn if the condition is true, otherwise returns falseRtn.
136+
// Unlike IfElse, this enforces type safety by requiring both return values to be the same type T.
88137
func Ternary[T any](cond bool, trueRtn T, falseRtn T) T {
89138
if cond {
90139
return trueRtn
@@ -93,6 +142,8 @@ func Ternary[T any](cond bool, trueRtn T, falseRtn T) T {
93142
}
94143
}
95144

145+
// ForEach applies a function to each item in a slice and returns a slice of results.
146+
// The function receives the item and its index, and can return any type for flexible VDOM generation.
96147
func ForEach[T any](items []T, fn func(T, int) any) []any {
97148
elems := make([]any, len(items))
98149
for idx, item := range items {
@@ -102,14 +153,11 @@ func ForEach[T any](items []T, fn func(T, int) any) []any {
102153
return elems
103154
}
104155

105-
func Props(props any) map[string]any {
106-
m, err := util.StructToMap(props)
107-
if err != nil {
108-
return nil
109-
}
110-
return m
111-
}
112-
156+
// UseState is the tsunami analog to React's useState hook.
157+
// It provides persistent state management within a VDOM component, returning the current
158+
// state value, a setter function, and an updater function.
159+
// Setting a new value causes a re-render of the component.
160+
// This hook must be called within a component context.
113161
func UseState[T any](ctx context.Context, initialVal T) (T, func(T), func(func(T) T)) {
114162
rc := vdomctx.GetRenderContext(ctx)
115163
if rc == nil {
@@ -178,18 +226,35 @@ func useAtom[T any](ctx context.Context, hookName string, atomName string) (T, f
178226
return atomVal, typedSetVal, typedSetFuncVal
179227
}
180228

229+
// UseSharedAtom provides access to a shared atom state across components.
230+
// It returns the current atom value, a setter function, and an updater function.
231+
// Setting a new value causes a re-render of any component using this atom.
232+
// This hook must be called within a component context.
181233
func UseSharedAtom[T any](ctx context.Context, atomName string) (T, func(T), func(func(T) T)) {
182234
return useAtom[T](ctx, "UseSharedAtom", "$shared."+atomName)
183235
}
184236

237+
// UseConfig provides access to config values (atom names are global across the app).
238+
// It returns the current config value, a setter function, and an updater function.
239+
// Setting a new value causes a re-render of all components using this config atom.
240+
// This hook must be called within a component context.
185241
func UseConfig[T any](ctx context.Context, atomName string) (T, func(T), func(func(T) T)) {
186242
return useAtom[T](ctx, "UseConfig", "$config."+atomName)
187243
}
188244

245+
// UseData provides access to data values (atom names are global across the app).
246+
// It returns the current data value, a setter function, and an updater function.
247+
// Setting a new value causes a re-render of all components using this data atom.
248+
// This hook must be called within a component context.
189249
func UseData[T any](ctx context.Context, atomName string) (T, func(T), func(func(T) T)) {
190250
return useAtom[T](ctx, "UseData", "$data."+atomName)
191251
}
192252

253+
// UseVDomRef provides a reference to a DOM element in the VDOM tree.
254+
// It returns a VDomRef that can be attached to elements for direct DOM access.
255+
// The ref will not be current on the first render - refs are set and become
256+
// current after client-side mounting.
257+
// This hook must be called within a component context.
193258
func UseVDomRef(ctx context.Context) *VDomRef {
194259
rc := vdomctx.GetRenderContext(ctx)
195260
val := rc.UseVDomRef(ctx)
@@ -200,6 +265,10 @@ func UseVDomRef(ctx context.Context) *VDomRef {
200265
return refVal
201266
}
202267

268+
// UseRef is the tsunami analog to React's useRef hook.
269+
// It provides a mutable ref object that persists across re-renders.
270+
// Unlike UseVDomRef, this is not tied to DOM elements but holds arbitrary values.
271+
// This hook must be called within a component context.
203272
func UseRef[T any](ctx context.Context, val T) *VDomSimpleRef[T] {
204273
rc := vdomctx.GetRenderContext(ctx)
205274
refVal := rc.UseRef(ctx, &VDomSimpleRef[T]{Current: val})
@@ -210,6 +279,10 @@ func UseRef[T any](ctx context.Context, val T) *VDomSimpleRef[T] {
210279
return typedRef
211280
}
212281

282+
// UseId returns the underlying component's unique identifier (UUID).
283+
// The ID persists across re-renders but is recreated when the component
284+
// is recreated, following React component lifecycle.
285+
// This hook must be called within a component context.
213286
func UseId(ctx context.Context) string {
214287
rc := vdomctx.GetRenderContext(ctx)
215288
if rc == nil {
@@ -218,6 +291,8 @@ func UseId(ctx context.Context) string {
218291
return rc.UseId(ctx)
219292
}
220293

294+
// UseRenderTs returns the timestamp of the current render.
295+
// This hook must be called within a component context.
221296
func UseRenderTs(ctx context.Context) int64 {
222297
rc := vdomctx.GetRenderContext(ctx)
223298
if rc == nil {
@@ -226,6 +301,10 @@ func UseRenderTs(ctx context.Context) int64 {
226301
return rc.UseRenderTs(ctx)
227302
}
228303

304+
// UseResync returns whether the current render is a resync operation.
305+
// Resyncs happen on initial app loads or full refreshes, as opposed to
306+
// incremental renders which happen otherwise.
307+
// This hook must be called within a component context.
229308
func UseResync(ctx context.Context) bool {
230309
rc := vdomctx.GetRenderContext(ctx)
231310
if rc == nil {
@@ -234,6 +313,11 @@ func UseResync(ctx context.Context) bool {
234313
return rc.UseResync(ctx)
235314
}
236315

316+
// UseEffect is the tsunami analog to React's useEffect hook.
317+
// It queues effects to run after the render cycle completes.
318+
// The function can return a cleanup function that runs before the next effect
319+
// or when the component unmounts. Dependencies use shallow comparison, just like React.
320+
// This hook must be called within a component context.
237321
func UseEffect(ctx context.Context, fn func() func(), deps []any) {
238322
// note UseEffect never actually runs anything, it just queues the effect to run later
239323
rc := vdomctx.GetRenderContext(ctx)
@@ -243,6 +327,8 @@ func UseEffect(ctx context.Context, fn func() func(), deps []any) {
243327
rc.UseEffect(ctx, fn, deps)
244328
}
245329

330+
// UseSetAppTitle sets the application title for the current component.
331+
// This hook must be called within a component context.
246332
func UseSetAppTitle(ctx context.Context, title string) {
247333
rc := vdomctx.GetRenderContext(ctx)
248334
if rc == nil {
@@ -251,6 +337,10 @@ func UseSetAppTitle(ctx context.Context, title string) {
251337
rc.UseSetAppTitle(ctx, title)
252338
}
253339

340+
// QueueRefOp queues a reference operation to be executed on the DOM element.
341+
// Operations include actions like "focus", "scrollIntoView", etc.
342+
// If the ref is nil or not current, the operation is ignored.
343+
// This function must be called within a component context.
254344
func QueueRefOp(ctx context.Context, ref *VDomRef, op VDomRefOperation) {
255345
if ref == nil || !ref.HasCurrent {
256346
return
@@ -265,17 +355,22 @@ func QueueRefOp(ctx context.Context, ref *VDomRef, op VDomRefOperation) {
265355
vc.QueueRefOp(ctx, op)
266356
}
267357

358+
// PartToElems converts various types into VDomElem slices for use in VDOM children.
359+
// It handles strings, booleans, VDomElems, *VDomElem, slices, and other types
360+
// by converting them to appropriate VDomElem representations.
361+
// nil values are ignored and removed from the final slice.
362+
// This is primarily an internal function and not typically called directly by application code.
268363
func PartToElems(part any) []VDomElem {
269364
if part == nil {
270365
return nil
271366
}
272367
switch partTyped := part.(type) {
273368
case string:
274-
return []VDomElem{TextElem(partTyped)}
369+
return []VDomElem{textElem(partTyped)}
275370
case bool:
276371
// matches react
277372
if partTyped {
278-
return []VDomElem{TextElem("true")}
373+
return []VDomElem{textElem("true")}
279374
}
280375
return nil
281376
case VDomElem:
@@ -294,6 +389,6 @@ func PartToElems(part any) []VDomElem {
294389
}
295390
return rtn
296391
}
297-
return []VDomElem{TextElem(fmt.Sprint(part))}
392+
return []VDomElem{textElem(fmt.Sprint(part))}
298393
}
299394
}

0 commit comments

Comments
 (0)