diff --git a/framework/h/command_test.go b/framework/h/command_test.go index 0da8afb..11b42fa 100644 --- a/framework/h/command_test.go +++ b/framework/h/command_test.go @@ -55,10 +55,12 @@ var re = regexp.MustCompile(`\s+`) func compareIgnoreSpaces(t *testing.T, actual, expected string) { expected = strings.ReplaceAll(expected, "\n", "") expected = strings.ReplaceAll(expected, "\t", "") - expected = re.ReplaceAllString(expected, " ") actual = strings.ReplaceAll(actual, "\n", "") actual = strings.ReplaceAll(actual, "\t", "") actual = re.ReplaceAllString(actual, " ") + spaceRegex := regexp.MustCompile(`\s+`) + actual = strings.TrimSpace(spaceRegex.ReplaceAllString(actual, "")) + expected = strings.TrimSpace(spaceRegex.ReplaceAllString(expected, "")) assert.Equal(t, expected, actual) } @@ -75,11 +77,11 @@ func TestJsEval(t *testing.T) { } compareIgnoreSpaces(t, renderJs(t, EvalJsOnParent("element.style.display = 'none'")), ` - if(!self.parentElement) { return; } let element = self.parentElement; element.style.display = 'none' + if(self.parentElement) { let element = self.parentElement; element.style.display = 'none' } `) compareIgnoreSpaces(t, renderJs(t, EvalJsOnSibling("button", "element.style.display = 'none'")), ` - if(!self.parentElement) { return; }let siblings = self.parentElement.querySelectorAll('button');siblings.forEach(function(element) {element.style.display = 'none'}); + if(self.parentElement) { let siblings = self.parentElement.querySelectorAll('button');siblings.forEach(function(element) {element.style.display = 'none'}); } `) } @@ -145,13 +147,13 @@ func TestToggleClassOnElement(t *testing.T) { func TestSetClassOnParent(t *testing.T) { compareIgnoreSpaces(t, renderJs(t, SetClassOnParent("active")), ` - if(!self.parentElement) { return; } let element = self.parentElement; element.classList.add('active') + if(self.parentElement) { let element = self.parentElement; element.classList.add('active') } `) } func TestRemoveClassOnParent(t *testing.T) { compareIgnoreSpaces(t, renderJs(t, RemoveClassOnParent("active")), ` - if(!self.parentElement) { return; } let element = self.parentElement; element.classList.remove('active') + if(self.parentElement) { let element = self.parentElement; element.classList.remove('active') } `) } @@ -174,20 +176,28 @@ func TestRemoveClassOnChildren(t *testing.T) { } func TestSetClassOnSibling(t *testing.T) { - compareIgnoreSpaces(t, renderJs(t, SetClassOnSibling("button", "selected")), ` - if(!self.parentElement) { return; }let siblings = self.parentElement.querySelectorAll('button'); - siblings.forEach(function(element) { - element.classList.add('selected') - }); + compareIgnoreSpaces(t, renderJs(t, SetClassOnSibling("button", "selected")), + // language=JavaScript + ` + if(self.parentElement) { + let siblings = self.parentElement.querySelectorAll('button'); + siblings.forEach(function(element) { + element.classList.add('selected') + }); + } `) } func TestRemoveClassOnSibling(t *testing.T) { - compareIgnoreSpaces(t, renderJs(t, RemoveClassOnSibling("button", "selected")), ` - if(!self.parentElement) { return; }let siblings = self.parentElement.querySelectorAll('button'); - siblings.forEach(function(element) { - element.classList.remove('selected') - }); + compareIgnoreSpaces(t, renderJs(t, RemoveClassOnSibling("button", "selected")), + // language=JavaScript + ` + if(self.parentElement) { + let siblings = self.parentElement.querySelectorAll('button'); + siblings.forEach(function(element) { + element.classList.remove('selected') + }); + } `) } @@ -226,3 +236,148 @@ func TestInjectScriptIfNotExist(t *testing.T) { } `) } + +func TestEvalCommands(t *testing.T) { + t.Parallel() + div := Div(Id("test")) + result := Render(EvalCommands(div, + SetText("hello"), + EvalJs(` + alert('test') + `), + SetClassOnParent("myclass"), + SetClassOnSibling("div", "myclass"), + )) + + evalId := "" + for _, child := range div.children { + switch child.(type) { + case *AttributeR: + attr := child.(*AttributeR) + if attr.Name == "data-eval-commands-id" { + evalId = attr.Value + break + } + } + } + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + let element = document.querySelector("[data-eval-commands-id='%s']"); + if(!element) {return;} + self = element; + self.innerText = 'hello' + alert('test') + if(self.parentElement) { + element = self.parentElement; + element.classList.add('myclass') + } + if(self.parentElement) { + let siblings = self.parentElement.querySelectorAll('div'); + siblings.forEach(function(element) { + element.classList.add('myclass') + }); + } + `, evalId)) +} + +func TestToggleText(t *testing.T) { + t.Parallel() + result := Render(ToggleText("hello", "world")) + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + if(self.innerText === "hello") { + self.innerText = "world"; + } else { + self.innerText = "hello"; + } + `)) +} + +func TestToggleTextOnSibling(t *testing.T) { + t.Parallel() + result := Render(ToggleTextOnSibling("div", "hello", "world")) + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + if(self.parentElement) { + let siblings = self.parentElement.querySelectorAll('div'); + siblings.forEach(function(element){ + if(element.innerText === "hello"){ + element.innerText= "world"; + } else { + element.innerText= "hello"; + } + }); + } + `)) +} + +func TestToggleTextOnChildren(t *testing.T) { + t.Parallel() + result := Render(ToggleTextOnChildren("div", "hello", "world")) + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + let children = self.querySelectorAll('div'); + children.forEach(function(element) { + if(element.innerText === "hello") { + element.innerText = "world"; + } else { + element.innerText = "hello"; + } + }); + `)) +} + +func TestToggleTextOnParent(t *testing.T) { + t.Parallel() + result := Render(ToggleTextOnParent("hello", "world")) + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + if(self.parentElement) { + let element = self.parentElement; + + if(element.innerText === "hello") { + element.innerText = "world"; + } else { + element.innerText = "hello"; + } + } + `)) +} + +func TestToggleClassOnChildren(t *testing.T) { + t.Parallel() + result := Render(ToggleClassOnChildren("div", "hidden")) + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + let children = self.querySelectorAll('div'); + children.forEach(function(element) { + element.classList.toggle('hidden') + }); + `)) +} + +func TestToggleClassOnParent(t *testing.T) { + t.Parallel() + result := Render(ToggleClassOnParent("hidden")) + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + if(self.parentElement) { + let element = self.parentElement; + element.classList.toggle('hidden') + } + `)) +} + +func TestToggleClassOnSibling(t *testing.T) { + t.Parallel() + result := Render(ToggleClassOnSibling("div", "hidden")) + //language=JavaScript + compareIgnoreSpaces(t, result, fmt.Sprintf(` + if(self.parentElement) { + let siblings = self.parentElement.querySelectorAll('div'); + siblings.forEach(function(element) { + element.classList.toggle('hidden') + }); + } + `)) +} diff --git a/framework/h/lifecycle.go b/framework/h/lifecycle.go index b1e4fee..1aed70b 100644 --- a/framework/h/lifecycle.go +++ b/framework/h/lifecycle.go @@ -2,6 +2,7 @@ package h import ( "fmt" + "github.com/google/uuid" "github.com/maddalax/htmgo/framework/hx" "github.com/maddalax/htmgo/framework/internal/util" "strings" @@ -233,6 +234,54 @@ func ToggleClass(class string) SimpleJsCommand { return SimpleJsCommand{Command: fmt.Sprintf("this.classList.toggle('%s')", class)} } +// ToggleText toggles the given text on the element. +func ToggleText(text string, textTwo string) Command { + // language=JavaScript + return EvalJs(fmt.Sprintf(` + if(self.innerText === "%s") { + self.innerText = "%s"; + } else { + self.innerText = "%s"; + } + `, text, textTwo, text)) +} + +// ToggleTextOnSibling toggles the given text on the siblings of the element. +func ToggleTextOnSibling(selector, text string, textTwo string) Command { + // language=JavaScript + return EvalJsOnSibling(selector, fmt.Sprintf(` + if(element.innerText === "%s") { + element.innerText = "%s"; + } else { + element.innerText = "%s"; + } + `, text, textTwo, text)) +} + +// ToggleTextOnChildren toggles the given text on the children of the element. +func ToggleTextOnChildren(selector, text string, textTwo string) Command { + // language=JavaScript + return EvalJsOnChildren(selector, fmt.Sprintf(` + if(element.innerText === "%s") { + element.innerText = "%s"; + } else { + element.innerText = "%s"; + } + `, text, textTwo, text)) +} + +// ToggleTextOnParent toggles the given text on the parent of the element. +func ToggleTextOnParent(text string, textTwo string) Command { + // language=JavaScript + return EvalJsOnParent(fmt.Sprintf(` + if(element.innerText === "%s") { + element.innerText = "%s"; + } else { + element.innerText = "%s"; + } + `, text, textTwo, text)) +} + // ToggleClassOnElement toggles the given class on the elements returned by the selector. func ToggleClassOnElement(selector, class string) ComplexJsCommand { // language=JavaScript @@ -247,9 +296,10 @@ func ToggleClassOnElement(selector, class string) ComplexJsCommand { func EvalJsOnParent(js string) ComplexJsCommand { // language=JavaScript return EvalJs(fmt.Sprintf(` - if(!self.parentElement) { return; } - let element = self.parentElement; - %s + if(self.parentElement) { + let element = self.parentElement; + %s + } `, js)) } @@ -268,32 +318,51 @@ func EvalJsOnChildren(selector, js string) ComplexJsCommand { func EvalJsOnSibling(selector, js string) ComplexJsCommand { // language=JavaScript return EvalJs(fmt.Sprintf(` - if(!self.parentElement) { return; } - let siblings = self.parentElement.querySelectorAll('%s'); - siblings.forEach(function(element) { - %s - }); + if(self.parentElement) { + let siblings = self.parentElement.querySelectorAll('%s'); + siblings.forEach(function(element) { + %s + }); + } `, selector, js)) } -// SetClassOnParent sets the given class on the parent of the element. Reference the element using 'element'. +// SetClassOnParent sets the given class on the parent of the element. func SetClassOnParent(class string) ComplexJsCommand { // language=JavaScript return EvalJsOnParent(fmt.Sprintf("element.classList.add('%s')", class)) } -// RemoveClassOnParent removes the given class from the parent of the element. Reference the element using 'element'. +// RemoveClassOnParent removes the given class from the parent of the element. func RemoveClassOnParent(class string) ComplexJsCommand { // language=JavaScript return EvalJsOnParent(fmt.Sprintf("element.classList.remove('%s')", class)) } -// SetClassOnChildren sets the given class on the children of the element. Reference the element using 'element'. +// SetClassOnChildren sets the given class on the children of the element. func SetClassOnChildren(selector, class string) ComplexJsCommand { // language=JavaScript return EvalJsOnChildren(selector, fmt.Sprintf("element.classList.add('%s')", class)) } +// ToggleClassOnChildren toggles the given class on the children of the element. +func ToggleClassOnChildren(selector, class string) ComplexJsCommand { + // language=JavaScript + return EvalJsOnChildren(selector, fmt.Sprintf("element.classList.toggle('%s')", class)) +} + +// ToggleClassOnParent toggles the given class on the parent of the element. +func ToggleClassOnParent(class string) ComplexJsCommand { + // language=JavaScript + return EvalJsOnParent(fmt.Sprintf("element.classList.toggle('%s')", class)) +} + +// ToggleClassOnSibling toggles the given class on the siblings of the element. +func ToggleClassOnSibling(selector, class string) ComplexJsCommand { + // language=JavaScript + return EvalJsOnSibling(selector, fmt.Sprintf("element.classList.toggle('%s')", class)) +} + // SetClassOnSibling sets the given class on the siblings of the element. Reference the element using 'element'. func SetClassOnSibling(selector, class string) ComplexJsCommand { // language=JavaScript @@ -330,6 +399,36 @@ func EvalJs(js string) ComplexJsCommand { return NewComplexJsCommand(js) } +func EvalCommandsOnSelector(selector string, cmds ...Command) ComplexJsCommand { + lines := make([]string, len(cmds)) + for i, cmd := range cmds { + lines[i] = Render(cmd) + lines[i] = strings.ReplaceAll(lines[i], "this.", "self.") + // some commands set the element we need to fix it so we arent redeclaring it + lines[i] = strings.ReplaceAll(lines[i], "let element =", "element =") + } + code := strings.Join(lines, "\n") + return EvalJs(fmt.Sprintf(` + let element = document.querySelector("%s"); + + if(!element) { + return; + } + + self = element; + %s + `, selector, code)) +} + +func EvalCommands(element *Element, cmds ...Command) ComplexJsCommand { + id := strings.ReplaceAll(uuid.NewString(), "-", "") + element.AppendChildren( + Attribute("data-eval-commands-id", id), + ) + return EvalCommandsOnSelector( + fmt.Sprintf(`[data-eval-commands-id='%s']`, id), cmds...) +} + // PreventDefault prevents the default action of the event. func PreventDefault() SimpleJsCommand { // language=JavaScript diff --git a/framework/h/renderer.go b/framework/h/renderer.go index 83efa4f..b0969a9 100644 --- a/framework/h/renderer.go +++ b/framework/h/renderer.go @@ -35,9 +35,15 @@ var voidTags = map[string]bool{ "wbr": true, } +type ScriptEntry struct { + Body string + ChildOf *Element +} + type RenderContext struct { - builder *strings.Builder - scripts []string + builder *strings.Builder + scripts []ScriptEntry + currentElement *Element } func (ctx *RenderContext) AddScript(funcName string, body string) { @@ -48,7 +54,11 @@ func (ctx *RenderContext) AddScript(funcName string, body string) { %s } `, funcName, funcName, body) - ctx.scripts = append(ctx.scripts, script) + + ctx.scripts = append(ctx.scripts, ScriptEntry{ + Body: script, + ChildOf: ctx.currentElement, + }) } func (node *Element) Render(context *RenderContext) { @@ -56,6 +66,8 @@ func (node *Element) Render(context *RenderContext) { return } + context.currentElement = node + if node.tag == CachedNodeTag { meta := node.meta.(*CachedNode) meta.Render(context) @@ -147,7 +159,7 @@ func (node *Element) Render(context *RenderContext) { } if node.tag != "" { - renderScripts(context) + renderScripts(context, node) if !voidTags[node.tag] { context.builder.WriteString("