Skip to content

Commit efcac22

Browse files
committed
feat: Add Groups to composition APIs
1 parent 7dd31b9 commit efcac22

File tree

6 files changed

+231
-34
lines changed

6 files changed

+231
-34
lines changed

docs/articles/api/math.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,6 @@ Use when performing 2D or 3D transformations in WebGL components.
4444
3. Combine them via `Mat4.Mul`.
4545
4. Convert the matrix to a typed array with `js.Float32Array().New(len(mvp))` and upload it via `Context.Call("uniformMatrix4fv", ...)`.
4646

47-
### APIs used
48-
49-
- `Vec2`, `Vec3`
50-
- `Mat4`, `Identity`, `Translation`, `Scale`, `Perspective`, `Orthographic`
51-
- `webgl.Context.Call`
52-
- `js.Float32Array`
53-
5447
### Notes and Limitations
5548

5649
- Matrices are column-major and use `float32` precision.

docs/articles/api/webgl.md

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,6 @@ if !ctx.Get("createVertexArray").IsUndefined() {
120120
ctx.DrawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0)
121121
```
122122

123-
#### APIs used
124-
125-
- `Context.Viewport`
126-
- `Context.Enable`
127-
- `Context.DepthFunc`
128-
- `Context.CreateVertexArray`
129-
- `Context.BindVertexArray`
130-
- `Context.DrawElements`
131-
- constants `ELEMENT_ARRAY_BUFFER`, `DEPTH_TEST`, `UNSIGNED_SHORT`, `NEVER`, `LESS`, `EQUAL`, `LEQUAL`, `GREATER`, `NOTEQUAL`, `GEQUAL`, `ALWAYS`
132-
133123
#### Limitations
134124

135125
- `Context.CreateVertexArray` and `Context.BindVertexArray` require WebGL2 or

docs/articles/essentials/composition.md

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,7 @@ cmp.SetOnMount(func(*core.HTMLComponent) {
113113
})
114114
```
115115

116-
### APIs Used
117-
118-
- `(*core.HTMLComponent).GetRef(name string) dom.Element`
119-
- `dom.Element.SetHTML(html string)`
120-
- `dom.Element.AppendChild(child dom.Element)`
121-
- `composition.Div() *divNode`
122-
- `(*divNode).Text(t string) *divNode`
123-
124-
### End-to-End Example
116+
### Example
125117

126118
```go
127119
tpl := []byte("<root><div [greet]></div></root>")
@@ -142,6 +134,59 @@ cmp.SetOnMount(func(*core.HTMLComponent) {
142134

143135
- [dom](../api/dom)
144136

137+
## Element Groups
138+
139+
### Context / Why
140+
141+
Handling several nodes created via the composition builders often requires
142+
manual loops. `Group` collects those nodes and provides helpers to mutate all
143+
of them at once.
144+
145+
### Prerequisites / When
146+
147+
Use when multiple elements created with constructors need similar updates
148+
outside the template system.
149+
150+
### How
151+
152+
1. Create an empty group with `composition.NewGroup` or collect nodes with `composition.Group`.
153+
2. Build nodes with constructors like `Div()` and add them to groups via the `Group` method.
154+
3. Merge groups with `(composition.Elements).Group` when needed.
155+
4. Apply bulk operations such as `AddClass`, `SetAttr`, or iterate with `ForEach`.
156+
157+
```go
158+
import (
159+
"github.com/rfwlab/rfw/v1/composition"
160+
"github.com/rfwlab/rfw/v1/dom"
161+
)
162+
163+
cards := composition.Group(
164+
composition.Div().Class("card"),
165+
composition.Div().Class("card"),
166+
)
167+
cards.AddClass("active").SetStyle("color", "red")
168+
```
169+
170+
### Example
171+
172+
```go
173+
cards := composition.Group(
174+
composition.Div().Text("A"),
175+
composition.Div().Text("B"),
176+
)
177+
cards.AddClass("card").SetAttr("data-role", "item")
178+
```
179+
180+
### Notes and Limitations
181+
182+
- `Group` panics when called without nodes.
183+
- `ForEach` panics if the callback is nil.
184+
- Selectors and IDs are ignored; pass nodes explicitly.
185+
186+
### Related Links
187+
188+
- [dom](../api/dom)
189+
145190
## Stores and History
146191

147192
### Context / Why

docs/articles/guide/pathfinding.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,6 @@ path := <-ch
3232
0 0 0
3333
```
3434

35-
### APIs used
36-
37-
- `pathfinding.New`
38-
- `(*Pathfinder).RequestGridPath`
39-
- `(*Pathfinder).RequestNavMeshPath`
40-
- `(*Pathfinder).Cancel`
41-
- `math.Vec2`
42-
4335
## Example
4436

4537
Request a navmesh path between polygons:

v1/composition/node.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,106 @@ func (e elWrap) Append(nodes ...Node) {
2727
}
2828
}
2929

30+
// Elements groups a collection of DOM elements for bulk operations.
31+
type Elements struct{ els []dom.Element }
32+
33+
// NewGroup creates an empty Elements collection.
34+
func NewGroup() *Elements { return &Elements{} }
35+
36+
// Group collects provided nodes into an Elements wrapper without relying on selectors.
37+
func Group(nodes ...Node) *Elements {
38+
if len(nodes) == 0 {
39+
panic("composition.Group: no nodes")
40+
}
41+
g := NewGroup()
42+
g.add(nodes...)
43+
return g
44+
}
45+
46+
// Group merges the current group with other groups.
47+
func (g *Elements) Group(gs ...*Elements) *Elements {
48+
for _, other := range gs {
49+
if other != nil {
50+
g.els = append(g.els, other.els...)
51+
}
52+
}
53+
return g
54+
}
55+
56+
// ForEach invokes fn for each element in the group.
57+
func (g *Elements) ForEach(fn func(dom.Element)) {
58+
if fn == nil {
59+
panic("composition.Elements.ForEach: nil fn")
60+
}
61+
for _, el := range g.els {
62+
fn(el)
63+
}
64+
}
65+
66+
// AddClass adds a class to every element in the group.
67+
func (g *Elements) AddClass(name string) *Elements {
68+
for _, el := range g.els {
69+
el.AddClass(name)
70+
}
71+
return g
72+
}
73+
74+
// RemoveClass removes a class from every element in the group.
75+
func (g *Elements) RemoveClass(name string) *Elements {
76+
for _, el := range g.els {
77+
el.RemoveClass(name)
78+
}
79+
return g
80+
}
81+
82+
// ToggleClass toggles a class on every element in the group.
83+
func (g *Elements) ToggleClass(name string) *Elements {
84+
for _, el := range g.els {
85+
el.ToggleClass(name)
86+
}
87+
return g
88+
}
89+
90+
// SetAttr sets an attribute on every element in the group.
91+
func (g *Elements) SetAttr(name, value string) *Elements {
92+
for _, el := range g.els {
93+
el.SetAttr(name, value)
94+
}
95+
return g
96+
}
97+
98+
// SetStyle sets an inline style property on every element in the group.
99+
func (g *Elements) SetStyle(prop, value string) *Elements {
100+
for _, el := range g.els {
101+
el.SetStyle(prop, value)
102+
}
103+
return g
104+
}
105+
106+
// SetText sets the text content of every element in the group.
107+
func (g *Elements) SetText(t string) *Elements {
108+
for _, el := range g.els {
109+
el.SetText(t)
110+
}
111+
return g
112+
}
113+
114+
// SetHTML replaces the HTML of every element in the group.
115+
func (g *Elements) SetHTML(html string) *Elements {
116+
for _, el := range g.els {
117+
el.SetHTML(html)
118+
}
119+
return g
120+
}
121+
122+
func (g *Elements) add(nodes ...Node) {
123+
for _, n := range nodes {
124+
if n != nil {
125+
g.els = append(g.els, n.Element())
126+
}
127+
}
128+
}
129+
30130
// BindEl invokes fn with a wrapper exposing Clear and Append helpers for the
31131
// provided element.
32132
func BindEl(el dom.Element, fn func(El)) {
@@ -111,3 +211,11 @@ func (d *divNode) Text(t string) *divNode {
111211
d.el.SetText(t)
112212
return d
113213
}
214+
215+
// Group adds the node to the provided group.
216+
func (d *divNode) Group(g *Elements) *divNode {
217+
if g != nil {
218+
g.add(d)
219+
}
220+
return d
221+
}

v1/composition/node_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,42 @@ func TestFor(t *testing.T) {
5959
}
6060
}
6161

62+
func TestGroupForEach(t *testing.T) {
63+
cards := NewGroup()
64+
Div().Text("a").Group(cards)
65+
Div().Text("b").Group(cards)
66+
67+
var texts []string
68+
cards.ForEach(func(el dom.Element) {
69+
texts = append(texts, el.Text())
70+
})
71+
72+
if len(texts) != 2 || texts[0] != "a" || texts[1] != "b" {
73+
t.Fatalf("unexpected texts %v", texts)
74+
}
75+
}
76+
77+
func TestGroupMerge(t *testing.T) {
78+
g1 := Group(Div().Text("a"))
79+
g2 := Group(Div().Text("b"))
80+
g1.Group(g2)
81+
82+
var texts []string
83+
g1.ForEach(func(el dom.Element) {
84+
texts = append(texts, el.Text())
85+
})
86+
87+
if len(texts) != 2 || texts[0] != "a" || texts[1] != "b" {
88+
t.Fatalf("unexpected texts %v", texts)
89+
}
90+
}
91+
92+
func TestGroupInvalidArgs(t *testing.T) {
93+
assertPanics(t, func() { Group() })
94+
cards := Group(Div())
95+
assertPanics(t, func() { cards.ForEach(nil) })
96+
}
97+
6298
func TestDivBuilder(t *testing.T) {
6399
d := Div().Class("c").Style("color", "red").Text("hi")
64100
el := d.Element()
@@ -72,3 +108,36 @@ func TestDivBuilder(t *testing.T) {
72108
t.Fatalf("expected style color red, got %q", v)
73109
}
74110
}
111+
112+
func TestGroupMutators(t *testing.T) {
113+
g := Group(Div(), Div())
114+
115+
g.SetText("hi")
116+
g.ForEach(func(el dom.Element) {
117+
if el.Text() != "hi" {
118+
t.Fatalf("expected text hi")
119+
}
120+
})
121+
122+
g.SetHTML("<span>ok</span>")
123+
g.AddClass("x").ToggleClass("y").RemoveClass("x")
124+
g.SetAttr("data-x", "1").SetStyle("color", "red")
125+
126+
g.ForEach(func(el dom.Element) {
127+
if el.HTML() != "<span>ok</span>" {
128+
t.Fatalf("unexpected html %q", el.HTML())
129+
}
130+
if el.HasClass("x") {
131+
t.Fatalf("class x should be removed")
132+
}
133+
if !el.HasClass("y") {
134+
t.Fatalf("expected class y")
135+
}
136+
if v := el.Attr("data-x"); v != "1" {
137+
t.Fatalf("expected attr data-x=1, got %q", v)
138+
}
139+
if v := el.Get("style").Call("getPropertyValue", "color").String(); v != "red" {
140+
t.Fatalf("expected style color red, got %q", v)
141+
}
142+
})
143+
}

0 commit comments

Comments
 (0)