JS Eval Enhancements (#62)

* scripting enhancements

* tests

* cleanup / tests
This commit is contained in:
maddalax 2024-10-29 08:44:52 -05:00 committed by GitHub
parent 10f48af304
commit d85737bfb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 411 additions and 34 deletions

View file

@ -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')
});
}
`))
}

View file

@ -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

View file

@ -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
}
</script>`, 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("</")
context.builder.WriteString(node.tag)
@ -156,11 +168,19 @@ func (node *Element) Render(context *RenderContext) {
}
}
func renderScripts(context *RenderContext) {
for _, script := range context.scripts {
context.builder.WriteString(script)
func renderScripts(context *RenderContext, parent *Element) {
if len(context.scripts) == 0 {
return
}
context.scripts = []string{}
notWritten := make([]ScriptEntry, 0)
for _, script := range context.scripts {
if script.ChildOf == parent {
context.builder.WriteString(script.Body)
} else {
notWritten = append(notWritten, script)
}
}
context.scripts = notWritten
}
func (a *AttributeR) Render(context *RenderContext) {

View file

@ -14,6 +14,11 @@ var SetDisabled = h.SetDisabled
var RemoveClass = h.RemoveClass
var Alert = h.Alert
var SetClassOnChildren = h.SetClassOnChildren
var ToggleClassOnChildren = h.ToggleClassOnChildren
var ToggleClassOnParent = h.ToggleClassOnParent
var SetClassOnParent = h.SetClassOnParent
var RemoveClassOnParent = h.RemoveClassOnParent
var ToggleClassOnSibling = h.ToggleClassOnSibling
var RemoveClassOnChildren = h.RemoveClassOnChildren
var EvalJsOnChildren = h.EvalJsOnChildren
var EvalJsOnSibling = h.EvalJsOnSibling
@ -23,6 +28,8 @@ var RemoveClassOnSibling = h.RemoveClassOnSibling
var Remove = h.Remove
var PreventDefault = h.PreventDefault
var EvalJs = h.EvalJs
var EvalCommands = h.EvalCommands
var EvalCommandsOnSelector = h.EvalCommandsOnSelector
var ConsoleLog = h.ConsoleLog
var SetValue = h.SetValue
var SubmitFormOnEnter = h.SubmitFormOnEnter
@ -36,3 +43,7 @@ var GetWithQs = h.GetWithQs
var PostWithQs = h.PostWithQs
var ToggleClass = h.ToggleClass
var ToggleClassOnElement = h.ToggleClassOnElement
var ToggleText = h.ToggleText
var ToggleTextOnSibling = h.ToggleTextOnSibling
var ToggleTextOnChildren = h.ToggleTextOnChildren
var ToggleTextOnParent = h.ToggleTextOnParent

View file

@ -70,9 +70,31 @@ var ClickToEditSnippet = Snippet{
partial: snippets.ClickToEdit,
}
var JsSetTextOnClick = Snippet{
category: "Interactivity (JS)",
name: "Set Element Text On Click",
description: "A simple example of how to use htmgo with javascript",
sidebarName: "Set Text On Click",
path: "/examples/js-set-text-on-click",
partial: snippets.SetTextOnClick,
}
var JsHideChildrenOnClick = Snippet{
category: "Interactivity (JS)",
name: "Hide / Show Children On Click",
description: "Use JS to hide and show children elements on click",
sidebarName: "Hide / Show Children",
path: "/examples/js-hide-children-on-click",
partial: snippets.JsHideChildrenOnClick,
}
var examples = []Snippet{
FormWithLoadingStateSnippet,
ClickToEditSnippet,
JsSetTextOnClick,
JsHideChildrenOnClick,
UserAuthSnippet,
ChatSnippet,
HackerNewsSnippet,

View file

@ -0,0 +1,10 @@
package examples
import (
"github.com/maddalax/htmgo/framework/h"
)
func JsHideChildrenOnClickPage(ctx *h.RequestContext) *h.Page {
SetSnippet(ctx, &JsHideChildrenOnClick)
return Index(ctx)
}

View file

@ -0,0 +1,10 @@
package examples
import (
"github.com/maddalax/htmgo/framework/h"
)
func JsSetTextOnClickPage(ctx *h.RequestContext) *h.Page {
SetSnippet(ctx, &JsSetTextOnClick)
return Index(ctx)
}

View file

@ -0,0 +1,32 @@
package snippets
import (
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/js"
)
func JsHideChildrenOnClick(ctx *h.RequestContext) *h.Partial {
text := h.Pf("- Parent")
return h.NewPartial(
h.Div(
text,
h.Class("cursor-pointer"),
h.Id("js-test"),
h.OnClick(
js.ToggleClassOnChildren("div", "hidden"),
js.EvalCommands(
text,
js.ToggleText("+ Parent", "- Parent"),
),
),
h.Div(
h.Class("ml-4"),
h.Text("Child 1"),
),
h.Div(
h.Class("ml-4"),
h.Text("Child 2"),
),
),
)
}

View file

@ -0,0 +1,18 @@
package snippets
import (
"github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/js"
)
func SetTextOnClick(ctx *h.RequestContext) *h.Partial {
return h.NewPartial(
h.Button(
h.Text("Click to set text"),
h.Class("bg-slate-900 text-white py-2 px-4 rounded"),
h.OnClick(
js.SetText("Hello World"),
),
),
)
}