diff --git a/cli/htmgo/internal/dirutil/dir.go b/cli/htmgo/internal/dirutil/dir.go index 0fcc84c..2715277 100644 --- a/cli/htmgo/internal/dirutil/dir.go +++ b/cli/htmgo/internal/dirutil/dir.go @@ -17,6 +17,12 @@ func HasFileFromRoot(file string) bool { return err == nil } +func CreateHtmgoDir() { + if !HasFileFromRoot("__htmgo") { + CreateDirFromRoot("__htmgo") + } +} + func CreateDirFromRoot(dir string) error { cwd := process.GetWorkingDir() path := filepath.Join(cwd, dir) diff --git a/cli/htmgo/runner.go b/cli/htmgo/runner.go index 075a4d7..ab27c44 100644 --- a/cli/htmgo/runner.go +++ b/cli/htmgo/runner.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "github.com/maddalax/htmgo/cli/htmgo/internal" - "github.com/maddalax/htmgo/cli/htmgo/internal/dirutil" "github.com/maddalax/htmgo/cli/htmgo/tasks/astgen" "github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets" "github.com/maddalax/htmgo/cli/htmgo/tasks/css" @@ -57,10 +56,6 @@ func main() { slog.Debug("Running task:", slog.String("task", taskName)) slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir())) - if !dirutil.HasFileFromRoot("__htmgo") { - dirutil.CreateDirFromRoot("__htmgo") - } - if taskName == "watch" { fmt.Printf("Running in watch mode\n") os.Setenv("ENV", "development") diff --git a/cli/htmgo/tasks/copyassets/bundle.go b/cli/htmgo/tasks/copyassets/bundle.go index 75fed1c..a24da76 100644 --- a/cli/htmgo/tasks/copyassets/bundle.go +++ b/cli/htmgo/tasks/copyassets/bundle.go @@ -36,6 +36,8 @@ func getModuleVersion(modulePath string) (string, error) { } func CopyAssets() { + dirutil.CreateHtmgoDir() + moduleName := "github.com/maddalax/htmgo/framework" modulePath := module.GetDependencyPath(moduleName) diff --git a/framework/h/attribute.go b/framework/h/attribute.go index 92e3907..9651607 100644 --- a/framework/h/attribute.go +++ b/framework/h/attribute.go @@ -3,27 +3,54 @@ package h import ( "fmt" "github.com/maddalax/htmgo/framework/hx" + "github.com/maddalax/htmgo/framework/internal/datastructure" "strings" ) -type AttributeMap map[string]any +type AttributeMap = map[string]any -func (m *AttributeMap) ToMap() map[string]string { - result := make(map[string]string) - for k, v := range *m { - switch v.(type) { - case AttributeMap: - m2 := v.(*AttributeMap).ToMap() - for _, a := range m2 { - result[k] = a - } - case string: - result[k] = v.(string) - default: - result[k] = fmt.Sprintf("%v", v) +type AttributeMapOrdered struct { + data *datastructure.OrderedMap[string, string] +} + +func (m *AttributeMapOrdered) Set(key string, value any) { + switch v := value.(type) { + case string: + m.data.Set(key, v) + case *AttributeMap: + for k, v2 := range *v { + m.Set(k, v2) + } + case *AttributeMapOrdered: + v.Each(func(k string, v2 string) { + m.Set(k, v2) + }) + case *AttributeR: + m.data.Set(v.Name, v.Value) + default: + m.data.Set(key, fmt.Sprintf("%v", value)) + } +} + +func (m *AttributeMapOrdered) Each(cb func(key string, value string)) { + m.data.Each(func(key string, value string) { + cb(key, value) + }) +} + +func (m *AttributeMapOrdered) Entries() []datastructure.MapEntry[string, string] { + return m.data.Entries() +} + +func NewAttributeMap(pairs ...string) *AttributeMapOrdered { + m := datastructure.NewOrderedMap[string, string]() + if len(pairs)%2 == 0 { + for i := 0; i < len(pairs); i++ { + m.Set(pairs[i], pairs[i+1]) + i++ } } - return result + return &AttributeMapOrdered{data: m} } func Attribute(key string, value string) *AttributeR { @@ -33,28 +60,24 @@ func Attribute(key string, value string) *AttributeR { } } -func AttributeList(children ...*AttributeR) *AttributeMap { - m := make(AttributeMap) - for _, child := range children { - m[child.Name] = child.Value +func AttributeList(children ...*AttributeR) *AttributeMapOrdered { + m := NewAttributeMap() + for _, c := range children { + m.Set(c.Name, c.Value) } - return &m + return m } -func Attributes(attrs *AttributeMap) *AttributeMap { - return attrs +func Attributes(attributes *AttributeMap) *AttributeMapOrdered { + m := NewAttributeMap() + for k, v := range *attributes { + m.Set(k, v) + } + return m } -func AttributePairs(pairs ...string) *AttributeMap { - if len(pairs)%2 != 0 { - return &AttributeMap{} - } - m := make(AttributeMap) - for i := 0; i < len(pairs); i++ { - m[pairs[i]] = pairs[i+1] - i++ - } - return &m +func AttributePairs(pairs ...string) *AttributeMapOrdered { + return NewAttributeMap(pairs...) } func Checked() Ren { diff --git a/framework/h/command_test.go b/framework/h/command_test.go index c184ceb..3743381 100644 --- a/framework/h/command_test.go +++ b/framework/h/command_test.go @@ -100,12 +100,12 @@ func TestIncrement(t *testing.T) { func TestSetInnerHtml(t *testing.T) { htmlContent := Div(Span(UnsafeRaw("inner content"))) - compareIgnoreSpaces(t, renderJs(t, SetInnerHtml(htmlContent)), "this.innerHTML = `
inner content
`;") + compareIgnoreSpaces(t, renderJs(t, SetInnerHtml(htmlContent)), "this.innerHTML = `
inner content
`;") } func TestSetOuterHtml(t *testing.T) { htmlContent := Div(Span(UnsafeRaw("outer content"))) - compareIgnoreSpaces(t, renderJs(t, SetOuterHtml(htmlContent)), "this.outerHTML = `
outer content
`;") + compareIgnoreSpaces(t, renderJs(t, SetOuterHtml(htmlContent)), "this.outerHTML = `
outer content
`;") } func TestAddAttribute(t *testing.T) { diff --git a/framework/h/lifecycle.go b/framework/h/lifecycle.go index 49ee7c0..01a3f82 100644 --- a/framework/h/lifecycle.go +++ b/framework/h/lifecycle.go @@ -24,7 +24,7 @@ func validateCommands(cmds []Command) { break case ComplexJsCommand: break - case *AttributeMap: + case *AttributeMapOrdered: break case *Element: panic(fmt.Sprintf("element is not allowed in lifecycle events. Got: %v", t)) diff --git a/framework/h/render_test.go b/framework/h/render_test.go index 3d5792f..10a6662 100644 --- a/framework/h/render_test.go +++ b/framework/h/render_test.go @@ -1,11 +1,8 @@ package h import ( - "bytes" "github.com/google/uuid" "github.com/stretchr/testify/assert" - "golang.org/x/net/html" - "sort" "strconv" "strings" "sync" @@ -13,43 +10,12 @@ import ( "time" ) -// Sort attributes of a node by attribute name -func sortAttributes(node *html.Node) { - if node.Type == html.ElementNode && len(node.Attr) > 1 { - sort.SliceStable(node.Attr, func(i, j int) bool { - return node.Attr[i].Key < node.Attr[j].Key - }) - } -} - -// Traverse and sort attributes in the entire HTML tree -func traverseAndSortAttributes(node *html.Node) { - sortAttributes(node) - for child := node.FirstChild; child != nil; child = child.NextSibling { - traverseAndSortAttributes(child) - } -} - -// Parse HTML, sort attributes, and render back to a string -func sortHtmlAttributes(input string) string { - // Parse the HTML string into a node tree - doc, err := html.Parse(strings.NewReader(input)) - if err != nil { - return "" - } - - // Traverse and sort attributes for each node - traverseAndSortAttributes(doc) - - // Use a buffer to capture the rendered HTML - var buf bytes.Buffer - err = html.Render(&buf, doc) - if err != nil { - return "" - } - - // Return the rendered HTML as a string - return buf.String() +func TestSimpleRender(t *testing.T) { + t.Parallel() + result := Render( + Div(Attribute("id", "my-div"), Attribute("class", "my-class")), + ) + assert.Equal(t, `
`, result) } func TestRender(t *testing.T) { @@ -73,17 +39,64 @@ func TestRender(t *testing.T) { Text("hello, child"), ) - div.attributes["data-attr-1"] = "value" + div.attributes.Set("data-attr-1", "value") - expectedRaw := `
hello, world
hello, child
` - expected := sortHtmlAttributes(expectedRaw) - result := sortHtmlAttributes(Render(div)) + expected := `
hello, world
hello, child
` + result := Render(div) assert.Equal(t, expected, result) } +func TestRenderAttributes_1(t *testing.T) { + t.Parallel() + div := Div( + AttributePairs("class", "bg-red-500"), + Attributes(&AttributeMap{ + "id": Id("my-div"), + }), + Attribute("disabled", "true"), + ) + assert.Equal(t, + `
`, + Render(div), + ) +} + +func TestRenderAttributes_2(t *testing.T) { + div := Div( + AttributePairs("class", "bg-red-500", "id", "my-div"), + Button( + AttributePairs("class", "bg-blue-500", "id", "my-button"), + Text("Click me"), + Attribute("disabled", "true"), + Attribute("data-attr", "value"), + ), + ) + + assert.Equal(t, + `
`, + Render(div)) +} + +func TestRenderEmptyDiv(t *testing.T) { + t.Parallel() + assert.Equal(t, + `
`, + Render(Div()), + ) +} + +func TestRenderVoidElement(t *testing.T) { + t.Parallel() + assert.Equal(t, + ``, + Render(Input("text")), + ) + assert.Equal(t, ``, Render(Tag("input"))) +} + func TestRawContent(t *testing.T) { t.Parallel() str := "
hello, world
" @@ -98,14 +111,14 @@ func TestConditional(t *testing.T) { Ternary(true, Text("true"), Text("false")), ), ) - assert.Equal(t, "
true
", result) + assert.Equal(t, "
true
", result) result = Render( Div( If(false, Text("true")), ), ) - assert.Equal(t, "
", result) + assert.Equal(t, "
", result) } func TestTagSelfClosing(t *testing.T) { @@ -121,7 +134,7 @@ func TestTagSelfClosing(t *testing.T) { assert.Equal(t, `
`, Render( Div(Id("test")), )) - assert.Equal(t, `
`, Render( + assert.Equal(t, `
`, Render( Div(Id("test"), Div()), )) } @@ -134,12 +147,12 @@ func TestCached(t *testing.T) { return ComplexPage() }) - firstRender := sortHtmlAttributes(Render(page())) - secondRender := sortHtmlAttributes(Render(page())) + firstRender := Render(page()) + secondRender := Render(page()) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) - assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) + assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT(t *testing.T) { @@ -150,12 +163,12 @@ func TestCachedT(t *testing.T) { return ComplexPage() }) - firstRender := sortHtmlAttributes(Render(page("a"))) - secondRender := sortHtmlAttributes(Render(page("a"))) + firstRender := Render(page("a")) + secondRender := Render(page("a")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) - assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) + assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT2(t *testing.T) { @@ -166,12 +179,12 @@ func TestCachedT2(t *testing.T) { return ComplexPage() }) - firstRender := sortHtmlAttributes(Render(page("a", "b"))) - secondRender := sortHtmlAttributes(Render(page("a", "b"))) + firstRender := Render(page("a", "b")) + secondRender := Render(page("a", "b")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) - assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) + assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT3(t *testing.T) { @@ -182,12 +195,12 @@ func TestCachedT3(t *testing.T) { return ComplexPage() }) - firstRender := sortHtmlAttributes(Render(page("a", "b", "c"))) - secondRender := sortHtmlAttributes(Render(page("a", "b", "c"))) + firstRender := Render(page("a", "b", "c")) + secondRender := Render(page("a", "b", "c")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) - assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) + assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedT4(t *testing.T) { @@ -198,12 +211,12 @@ func TestCachedT4(t *testing.T) { return ComplexPage() }) - firstRender := sortHtmlAttributes(Render(page("a", "b", "c", "d"))) - secondRender := sortHtmlAttributes(Render(page("a", "b", "c", "d"))) + firstRender := Render(page("a", "b", "c", "d")) + secondRender := Render(page("a", "b", "c", "d")) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 1, count) - assert.Equal(t, firstRender, sortHtmlAttributes(Render(ComplexPage()))) + assert.Equal(t, firstRender, Render(ComplexPage())) } func TestCachedExpired(t *testing.T) { @@ -214,9 +227,9 @@ func TestCachedExpired(t *testing.T) { return ComplexPage() }) - firstRender := sortHtmlAttributes(Render(page())) + firstRender := Render(page()) time.Sleep(time.Millisecond * 5) - secondRender := sortHtmlAttributes(Render(page())) + secondRender := Render(page()) assert.Equal(t, firstRender, secondRender) assert.Equal(t, 2, count) @@ -409,9 +422,9 @@ func TestCacheByKeyT1_2(t *testing.T) { } }) - assert.Equal(t, "

one

", Render(cachedItem("one"))) - assert.Equal(t, "

two

", Render(cachedItem("two"))) - assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

one

", Render(cachedItem("one"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, 2, renderCount) } @@ -425,10 +438,10 @@ func TestCacheByKeyT1Expired(t *testing.T) { } }) - assert.Equal(t, "

one

", Render(cachedItem("one"))) - assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

one

", Render(cachedItem("one"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) time.Sleep(time.Millisecond * 2) - assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, 3, renderCount) } @@ -442,14 +455,14 @@ func TestCacheByKeyT1Expired_2(t *testing.T) { } }) - assert.Equal(t, "

one

", Render(cachedItem("one"))) + assert.Equal(t, "

one

", Render(cachedItem("one"))) time.Sleep(time.Millisecond * 3) - assert.Equal(t, "

two

", Render(cachedItem("two"))) - assert.Equal(t, "

two

", Render(cachedItem("two"))) - assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) time.Sleep(time.Millisecond * 3) - assert.Equal(t, "

one

", Render(cachedItem("one"))) - assert.Equal(t, "

two

", Render(cachedItem("two"))) + assert.Equal(t, "

one

", Render(cachedItem("one"))) + assert.Equal(t, "

two

", Render(cachedItem("two"))) assert.Equal(t, 3, renderCount) } @@ -529,7 +542,7 @@ func TestBackgroundCleaner(t *testing.T) { func TestEscapeHtml(t *testing.T) { t.Parallel() assert.Equal(t, "<script>alert(1)</script>", Render(Text(""))) - assert.Equal(t, "

<script>alert(1)</script>

", Render(Pf(""))) + assert.Equal(t, "

<script>alert(1)</script>

", Render(Pf(""))) } diff --git a/framework/h/renderables.go b/framework/h/renderables.go index b34ec91..510e954 100644 --- a/framework/h/renderables.go +++ b/framework/h/renderables.go @@ -5,6 +5,11 @@ type AttributeR struct { Value string } +type KeyValue[T any] struct { + Key string + Value T +} + type TextContent struct { Content string } diff --git a/framework/h/renderer.go b/framework/h/renderer.go index 2459a34..e31240f 100644 --- a/framework/h/renderer.go +++ b/framework/h/renderer.go @@ -68,11 +68,9 @@ func (node *Element) Render(context *RenderContext) { if node.tag != "" { context.builder.WriteString("<") context.builder.WriteString(node.tag) - context.builder.WriteString(" ") - - for name, value := range node.attributes { - NewAttribute(name, value).Render(context) - } + node.attributes.Each(func(key string, value string) { + NewAttribute(key, value).Render(context) + }) } totalChildren := 0 @@ -110,7 +108,7 @@ func (node *Element) Render(context *RenderContext) { // second pass, render any attributes within the tag for _, child := range node.children { switch child.(type) { - case *AttributeMap: + case *AttributeMapOrdered: child.Render(context) case *AttributeR: child.Render(context) @@ -132,7 +130,7 @@ func (node *Element) Render(context *RenderContext) { // render the children elements that are not attributes for _, child := range node.children { switch child.(type) { - case *AttributeMap: + case *AttributeMapOrdered: continue case *AttributeR: continue @@ -162,14 +160,13 @@ func renderScripts(context *RenderContext) { } func (a *AttributeR) Render(context *RenderContext) { + context.builder.WriteString(" ") context.builder.WriteString(a.Name) if a.Value != "" { context.builder.WriteString(`=`) context.builder.WriteString(`"`) context.builder.WriteString(html.EscapeString(a.Value)) context.builder.WriteString(`"`) - } else { - context.builder.WriteString(" ") } } @@ -199,13 +196,10 @@ func (p *Partial) Render(context *RenderContext) { p.Root.Render(context) } -func (m *AttributeMap) Render(context *RenderContext) { - m2 := m.ToMap() - - for k, v := range m2 { - context.builder.WriteString(" ") - NewAttribute(k, v).Render(context) - } +func (m *AttributeMapOrdered) Render(context *RenderContext) { + m.Each(func(key string, value string) { + NewAttribute(key, value).Render(context) + }) } func (l *LifeCycle) fromAttributeMap(event string, key string, value string, context *RenderContext) { @@ -222,6 +216,7 @@ func (l *LifeCycle) Render(context *RenderContext) { for event, commands := range l.handlers { m[event] = "" + for _, command := range commands { switch c := command.(type) { case SimpleJsCommand: @@ -229,14 +224,15 @@ func (l *LifeCycle) Render(context *RenderContext) { case ComplexJsCommand: context.AddScript(c.TempFuncName, c.Command) m[event] += fmt.Sprintf("%s(this);", c.TempFuncName) - case *AttributeMap: - for k, v := range c.ToMap() { - l.fromAttributeMap(event, k, v, context) - } + case *AttributeMapOrdered: + c.Each(func(key string, value string) { + l.fromAttributeMap(event, key, value, context) + }) case *AttributeR: l.fromAttributeMap(event, c.Name, c.Value, context) } } + } children := make([]Ren, 0) diff --git a/framework/h/tag.go b/framework/h/tag.go index 76e6e85..23fd3b4 100644 --- a/framework/h/tag.go +++ b/framework/h/tag.go @@ -11,7 +11,7 @@ type PartialFunc = func(ctx *RequestContext) *Partial type Element struct { tag string - attributes map[string]string + attributes *AttributeMapOrdered meta any children []Ren } @@ -50,7 +50,7 @@ func Tag(tag string, children ...Ren) *Element { return &Element{ tag: tag, children: children, - attributes: make(map[string]string), + attributes: NewAttributeMap(), } } @@ -69,57 +69,53 @@ func Body(children ...Ren) *Element { func Meta(name string, content string) *Element { return &Element{ tag: "meta", - attributes: map[string]string{ - "name": name, - "content": content, - }, + attributes: AttributePairs( + "name", name, + "content", content, + ), children: make([]Ren, 0), } } func LinkWithVersion(href string, rel string, version string) *Element { - attributeMap := AttributeMap{ - "href": href + "?v=" + version, - "rel": rel, - } return &Element{ - tag: "link", - attributes: attributeMap.ToMap(), - children: make([]Ren, 0), + tag: "link", + attributes: AttributePairs( + "href", href+"?v="+version, + "rel", rel, + ), + children: make([]Ren, 0), } } func Link(href string, rel string) *Element { - attributeMap := AttributeMap{ - "href": href, - "rel": rel, - } return &Element{ - tag: "link", - attributes: attributeMap.ToMap(), - children: make([]Ren, 0), + tag: "link", + attributes: AttributePairs( + "href", href, + "rel", rel, + ), + children: make([]Ren, 0), } } func ScriptWithVersion(url string, version string) *Element { - attributeMap := AttributeMap{ - "src": url + "?v=" + version, - } return &Element{ - tag: "script", - attributes: attributeMap.ToMap(), - children: make([]Ren, 0), + tag: "script", + attributes: AttributePairs( + "src", url+"?v="+version, + ), + children: make([]Ren, 0), } } func Script(url string) *Element { - attributeMap := AttributeMap{ - "src": url, - } return &Element{ - tag: "script", - attributes: attributeMap.ToMap(), - children: make([]Ren, 0), + tag: "script", + attributes: AttributePairs( + "src", url, + ), + children: make([]Ren, 0), } } @@ -177,13 +173,12 @@ func Value(value any) *AttributeR { } func Input(inputType string, children ...Ren) *Element { - attributeMap := AttributeMap{ - "type": inputType, - } return &Element{ - tag: "input", - attributes: attributeMap.ToMap(), - children: children, + tag: "input", + attributes: AttributePairs( + "type", inputType, + ), + children: children, } } @@ -257,7 +252,7 @@ func TagF(tag string, format string, args ...interface{}) *Element { An invocation can look like h.H3F("build simple and scalable systems with %s", "go + htmx", h.Class("-mt-4")), - where the args may be a mix of strings, *Element, *AttributeMap, *ChildList, *AttributeR + where the args may be a mix of strings, *Element, *AttributeMapOrdered, *ChildList, *AttributeR We need to separate the children from the format arguments */ children := make([]Ren, 0) @@ -266,7 +261,7 @@ func TagF(tag string, format string, args ...interface{}) *Element { switch d := arg.(type) { case *Element: children = append(children, d) - case *AttributeMap: + case *AttributeMapOrdered: children = append(children, d) case *ChildList: for _, child := range d.Children { diff --git a/framework/h/xhr.go b/framework/h/xhr.go index 0636f38..72f2ad0 100644 --- a/framework/h/xhr.go +++ b/framework/h/xhr.go @@ -2,46 +2,46 @@ package h import "github.com/maddalax/htmgo/framework/hx" -func Get(path string, trigger ...string) *AttributeMap { +func Get(path string, trigger ...string) *AttributeMapOrdered { return AttributeList(Attribute(hx.GetAttr, path), HxTriggerString(trigger...)) } -func GetPartial(partial PartialFunc, trigger ...string) *AttributeMap { +func GetPartial(partial PartialFunc, trigger ...string) *AttributeMapOrdered { return Get(GetPartialPath(partial), trigger...) } -func GetPartialWithQs(partial PartialFunc, qs *Qs, trigger string) *AttributeMap { +func GetPartialWithQs(partial PartialFunc, qs *Qs, trigger string) *AttributeMapOrdered { return Get(GetPartialPathWithQs(partial, qs), trigger) } -func GetWithQs(path string, qs *Qs, trigger string) *AttributeMap { +func GetWithQs(path string, qs *Qs, trigger string) *AttributeMapOrdered { return Get(SetQueryParams(path, qs), trigger) } -func PostPartial(partial PartialFunc, triggers ...string) *AttributeMap { +func PostPartial(partial PartialFunc, triggers ...string) *AttributeMapOrdered { return Post(GetPartialPath(partial), triggers...) } -func PostPartialWithQs(partial PartialFunc, qs *Qs, trigger ...string) *AttributeMap { +func PostPartialWithQs(partial PartialFunc, qs *Qs, trigger ...string) *AttributeMapOrdered { return Post(GetPartialPathWithQs(partial, qs), trigger...) } -func Post(url string, trigger ...string) *AttributeMap { +func Post(url string, trigger ...string) *AttributeMapOrdered { return AttributeList(Attribute(hx.PostAttr, url), HxTriggerString(trigger...)) } -func PostWithQs(url string, qs *Qs, trigger string) *AttributeMap { +func PostWithQs(url string, qs *Qs, trigger string) *AttributeMapOrdered { return Post(SetQueryParams(url, qs), trigger) } -func PostOnClick(url string) *AttributeMap { +func PostOnClick(url string) *AttributeMapOrdered { return Post(url, hx.ClickEvent) } -func PostPartialOnClick(partial PartialFunc) *AttributeMap { +func PostPartialOnClick(partial PartialFunc) *AttributeMapOrdered { return PostOnClick(GetPartialPath(partial)) } -func PostPartialOnClickQs(partial PartialFunc, qs *Qs) *AttributeMap { +func PostPartialOnClickQs(partial PartialFunc, qs *Qs) *AttributeMapOrdered { return PostOnClick(GetPartialPathWithQs(partial, qs)) } diff --git a/framework/internal/datastructure/map.go b/framework/internal/datastructure/map.go new file mode 100644 index 0000000..e4741d2 --- /dev/null +++ b/framework/internal/datastructure/map.go @@ -0,0 +1,84 @@ +package datastructure + +type MapEntry[K comparable, V any] struct { + Key K + Value V +} + +// OrderedMap is a generic data structure that maintains the order of keys. +type OrderedMap[K comparable, V any] struct { + keys []K + values map[K]V +} + +func (om *OrderedMap[K, V]) Each(cb func(key K, value V)) { + for _, key := range om.keys { + cb(key, om.values[key]) + } +} + +// Entries returns the key-value pairs in the order they were added. +func (om *OrderedMap[K, V]) Entries() []MapEntry[K, V] { + entries := make([]MapEntry[K, V], len(om.keys)) + for i, key := range om.keys { + entries[i] = MapEntry[K, V]{ + Key: key, + Value: om.values[key], + } + } + return entries +} + +// NewOrderedMap creates a new OrderedMap. +func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] { + return &OrderedMap[K, V]{ + keys: []K{}, + values: make(map[K]V), + } +} + +// Set adds or updates a key-value pair in the OrderedMap. +func (om *OrderedMap[K, V]) Set(key K, value V) { + // Check if the key already exists + if _, exists := om.values[key]; !exists { + om.keys = append(om.keys, key) // Append key to the keys slice if it's a new key + } + om.values[key] = value +} + +// Get retrieves a value by key. +func (om *OrderedMap[K, V]) Get(key K) (V, bool) { + value, exists := om.values[key] + return value, exists +} + +// Keys returns the keys in the order they were added. +func (om *OrderedMap[K, V]) Keys() []K { + return om.keys +} + +// Values returns the values in the order of their keys. +func (om *OrderedMap[K, V]) Values() []V { + values := make([]V, len(om.keys)) + for i, key := range om.keys { + values[i] = om.values[key] + } + + return values +} + +// Delete removes a key-value pair from the OrderedMap. +func (om *OrderedMap[K, V]) Delete(key K) { + if _, exists := om.values[key]; exists { + // Remove the key from the map + delete(om.values, key) + + // Remove the key from the keys slice + for i, k := range om.keys { + if k == key { + om.keys = append(om.keys[:i], om.keys[i+1:]...) + break + } + } + } +}