add support for complex raw js

This commit is contained in:
maddalax 2024-09-22 10:46:38 -05:00
parent 59900b59ea
commit 789b9e9c7c
14 changed files with 4086 additions and 77 deletions

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@ import "./htmxextensions/debug";
import "./htmxextensions/response-targets"; import "./htmxextensions/response-targets";
import "./htmxextensions/mutation-error"; import "./htmxextensions/mutation-error";
import "./htmxextensions/livereload" import "./htmxextensions/livereload"
import "./htmxextensions/htmgo";
function watchUrl(callback: (oldUrl: string, newUrl: string) => void) { function watchUrl(callback: (oldUrl: string, newUrl: string) => void) {
let lastUrl = window.location.href; let lastUrl = window.location.href;

View file

@ -0,0 +1,26 @@
import htmx from "htmx.org";
const evalFuncRegex = /__eval_[A-Za-z0-9]+\(\)/gm
htmx.defineExtension("htmgo", {
// @ts-ignore
onEvent: function (name, evt) {
if(name === "htmx:beforeCleanupElement" && evt.target) {
removeAssociatedScripts(evt.target as HTMLElement);
}
},
});
function removeAssociatedScripts(element: HTMLElement) {
const attributes = Array.from(element.attributes)
for (let attribute of attributes) {
const matches = attribute.value.match(evalFuncRegex) || []
for (let match of matches) {
const id = match.replace("()", "")
const ele = document.getElementById(id)
if(ele && ele.tagName === "SCRIPT") {
ele.remove()
}
}
}
}

View file

@ -5,7 +5,6 @@ import (
"net/http" "net/http"
"reflect" "reflect"
"runtime" "runtime"
"strings"
) )
type Partial struct { type Partial struct {
@ -13,10 +12,6 @@ type Partial struct {
Root *Element Root *Element
} }
func (p *Partial) Render(builder *strings.Builder) {
p.Root.Render(builder)
}
type Page struct { type Page struct {
Root Ren Root Ren
HttpMethod string HttpMethod string

11
framework/h/extensions.go Normal file
View file

@ -0,0 +1,11 @@
package h
import "strings"
func BaseExtensions() string {
extensions := []string{"path-deps", "response-targets", "mutation-error", "htmgo"}
if IsDevelopment() {
extensions = append(extensions, "livereload")
}
return strings.Join(extensions, ", ")
}

View file

@ -3,7 +3,7 @@ package h
import ( import (
"fmt" "fmt"
"github.com/maddalax/htmgo/framework/hx" "github.com/maddalax/htmgo/framework/hx"
"strings" "github.com/maddalax/htmgo/framework/internal/util"
) )
type LifeCycle struct { type LifeCycle struct {
@ -19,7 +19,9 @@ func NewLifeCycle() *LifeCycle {
func validateCommands(cmds []Command) { func validateCommands(cmds []Command) {
for _, cmd := range cmds { for _, cmd := range cmds {
switch t := cmd.(type) { switch t := cmd.(type) {
case JsCommand: case SimpleJsCommand:
break
case ComplexJsCommand:
break break
case *AttributeMap: case *AttributeMap:
break break
@ -92,40 +94,41 @@ func (l *LifeCycle) OnMutationError(cmd ...Command) *LifeCycle {
type Command = Ren type Command = Ren
type JsCommand struct { type SimpleJsCommand struct {
Command string Command string
} }
func (j JsCommand) Render(builder *strings.Builder) { type ComplexJsCommand struct {
builder.WriteString(j.Command) Command string
TempFuncName string
} }
func SetText(text string) JsCommand { func SetText(text string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.innerText = '%s'", text)} return SimpleJsCommand{Command: fmt.Sprintf("this.innerText = '%s'", text)}
} }
func Increment(amount int) JsCommand { func Increment(amount int) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.innerText = parseInt(this.innerText) + %d", amount)} return SimpleJsCommand{Command: fmt.Sprintf("this.innerText = parseInt(this.innerText) + %d", amount)}
} }
func SetInnerHtml(r Ren) JsCommand { func SetInnerHtml(r Ren) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.innerHTML = `%s`", Render(r))} return SimpleJsCommand{Command: fmt.Sprintf("this.innerHTML = `%s`", Render(r))}
} }
func SetOuterHtml(r Ren) JsCommand { func SetOuterHtml(r Ren) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.outerHTML = `%s`", Render(r))} return SimpleJsCommand{Command: fmt.Sprintf("this.outerHTML = `%s`", Render(r))}
} }
func AddAttribute(name, value string) JsCommand { func AddAttribute(name, value string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.setAttribute('%s', '%s')", name, value)} return SimpleJsCommand{Command: fmt.Sprintf("this.setAttribute('%s', '%s')", name, value)}
} }
func SetDisabled(disabled bool) JsCommand { func SetDisabled(disabled bool) SimpleJsCommand {
if disabled { if disabled {
return AddAttribute("disabled", "true") return AddAttribute("disabled", "true")
} else { } else {
@ -133,36 +136,62 @@ func SetDisabled(disabled bool) JsCommand {
} }
} }
func RemoveAttribute(name string) JsCommand { func RemoveAttribute(name string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.removeAttribute('%s')", name)} return SimpleJsCommand{Command: fmt.Sprintf("this.removeAttribute('%s')", name)}
} }
func AddClass(class string) JsCommand { func AddClass(class string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.classList.add('%s')", class)} return SimpleJsCommand{Command: fmt.Sprintf("this.classList.add('%s')", class)}
} }
func RemoveClass(class string) JsCommand { func RemoveClass(class string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("this.classList.remove('%s')", class)} return SimpleJsCommand{Command: fmt.Sprintf("this.classList.remove('%s')", class)}
} }
func Alert(text string) JsCommand { func ToggleClass(class string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf("alert('%s')", text)} return SimpleJsCommand{Command: fmt.Sprintf("this.classList.toggle('%s')", class)}
} }
func EvalJs(js string) JsCommand { func ToggleClassOnElement(selector, class string) ComplexJsCommand {
return JsCommand{Command: js} // language=JavaScript
return EvalJs(fmt.Sprintf(`
var el = document.querySelector('%s');
if(el) { el.classList.toggle('%s'); }`,
))
} }
func InjectScript(src string) JsCommand { func Alert(text string) SimpleJsCommand {
// language=JavaScript // language=JavaScript
return JsCommand{Command: fmt.Sprintf(` return SimpleJsCommand{Command: fmt.Sprintf("alert('%s')", text)}
}
func EvalJs(js string) ComplexJsCommand {
name := fmt.Sprintf("__eval_%s", util.RandSeq(6))
return ComplexJsCommand{Command: js, TempFuncName: name}
}
func InjectScript(src string) ComplexJsCommand {
// language=JavaScript
return ComplexJsCommand{Command: fmt.Sprintf(`
var script = document.createElement('script'); var script = document.createElement('script');
script.src = '%s'; script.src = '%s';
src.async = true; src.async = true;
document.head.appendChild(script); document.head.appendChild(script);
`, src)} `, src)}
} }
func InjectScriptIfNotExist(src string) ComplexJsCommand {
// language=JavaScript
return EvalJs(fmt.Sprintf(`
if(!document.querySelector('script[src="%s"]')) {
var script = document.createElement('script');
script.src = '%s';
script.async = true;
document.head.appendChild(script);
}
`, src, src))
}

View file

@ -7,13 +7,16 @@ import (
) )
type Ren interface { type Ren interface {
Render(builder *strings.Builder) Render(context *RenderContext)
} }
func Render(node Ren) string { func Render(node Ren) string {
start := time.Now() start := time.Now()
builder := &strings.Builder{} builder := &strings.Builder{}
node.Render(builder) context := &RenderContext{
builder: builder,
}
node.Render(context)
duration := time.Since(start) duration := time.Since(start)
fmt.Printf("render took %d microseconds\n", duration.Microseconds()) fmt.Printf("render took %d microseconds\n", duration.Microseconds())
return builder.String() return builder.String()

View file

@ -6,15 +6,30 @@ import (
"strings" "strings"
) )
func (node *Element) Render(builder *strings.Builder) { type RenderContext struct {
builder *strings.Builder
scripts []string
}
func (ctx *RenderContext) AddScript(funcName string, body string) {
script := fmt.Sprintf(`
<script id="%s">
function %s() {
%s
}
</script>`, funcName, funcName, body)
ctx.scripts = append(ctx.scripts, script)
}
func (node *Element) Render(context *RenderContext) {
// some elements may not have a tag, such as a Fragment // some elements may not have a tag, such as a Fragment
if node.tag != "" { if node.tag != "" {
builder.WriteString("<" + node.tag) context.builder.WriteString("<" + node.tag)
builder.WriteString(" ") context.builder.WriteString(" ")
for name, value := range node.attributes { for name, value := range node.attributes {
NewAttribute(name, value).Render(builder) NewAttribute(name, value).Render(context)
} }
} }
@ -35,15 +50,15 @@ func (node *Element) Render(builder *strings.Builder) {
for _, child := range node.children { for _, child := range node.children {
switch child.(type) { switch child.(type) {
case *AttributeMap: case *AttributeMap:
child.Render(builder) child.Render(context)
case *LifeCycle: case *LifeCycle:
child.Render(builder) child.Render(context)
} }
} }
// close the tag // close the tag
if node.tag != "" { if node.tag != "" {
builder.WriteString(">") context.builder.WriteString(">")
} }
// render the children elements that are not attributes // render the children elements that are not attributes
@ -54,64 +69,88 @@ func (node *Element) Render(builder *strings.Builder) {
case *LifeCycle: case *LifeCycle:
continue continue
default: default:
child.Render(builder) child.Render(context)
} }
} }
if node.tag != "" { if node.tag != "" {
builder.WriteString("</" + node.tag + ">") renderScripts(context)
context.builder.WriteString("</" + node.tag + ">")
} }
} }
func (a *AttributeR) Render(builder *strings.Builder) { func renderScripts(context *RenderContext) {
builder.WriteString(fmt.Sprintf(`%s="%s"`, a.Name, a.Value)) for _, script := range context.scripts {
context.builder.WriteString(script)
}
context.scripts = []string{}
} }
func (t *TextContent) Render(builder *strings.Builder) { func (a *AttributeR) Render(context *RenderContext) {
builder.WriteString(t.Content) context.builder.WriteString(fmt.Sprintf(`%s="%s"`, a.Name, a.Value))
} }
func (r *RawContent) Render(builder *strings.Builder) { func (t *TextContent) Render(context *RenderContext) {
builder.WriteString(r.Content) context.builder.WriteString(t.Content)
} }
func (c *ChildList) Render(builder *strings.Builder) { func (r *RawContent) Render(context *RenderContext) {
context.builder.WriteString(r.Content)
}
func (c *ChildList) Render(context *RenderContext) {
for _, child := range c.Children { for _, child := range c.Children {
child.Render(builder) child.Render(context)
} }
} }
func (m *AttributeMap) Render(builder *strings.Builder) { func (j SimpleJsCommand) Render(context *RenderContext) {
context.builder.WriteString(j.Command)
}
func (j ComplexJsCommand) Render(context *RenderContext) {
context.builder.WriteString(j.Command)
}
func (p *Partial) Render(context *RenderContext) {
p.Root.Render(context)
}
func (m *AttributeMap) Render(context *RenderContext) {
m2 := m.ToMap() m2 := m.ToMap()
for k, v := range m2 { for k, v := range m2 {
builder.WriteString(" ") context.builder.WriteString(" ")
NewAttribute(k, v).Render(builder) NewAttribute(k, v).Render(context)
} }
} }
func (l *LifeCycle) fromAttributeMap(event string, key string, value string, builder *strings.Builder) { func (l *LifeCycle) fromAttributeMap(event string, key string, value string, context *RenderContext) {
if key == hx.GetAttr || key == hx.PatchAttr || key == hx.PostAttr { if key == hx.GetAttr || key == hx.PatchAttr || key == hx.PostAttr {
TriggerString(hx.ToHtmxTriggerName(event)).Render(builder) TriggerString(hx.ToHtmxTriggerName(event)).Render(context)
} }
Attribute(key, value).Render(builder) Attribute(key, value).Render(context)
} }
func (l *LifeCycle) Render(builder *strings.Builder) { func (l *LifeCycle) Render(context *RenderContext) {
m := make(map[string]string) m := make(map[string]string)
for event, commands := range l.handlers { for event, commands := range l.handlers {
m[event] = "" m[event] = ""
for _, command := range commands { for _, command := range commands {
switch c := command.(type) { switch c := command.(type) {
case JsCommand: case SimpleJsCommand:
eventName := hx.FormatEventName(event, true) eventName := hx.FormatEventName(event, true)
m[eventName] += fmt.Sprintf("%s;", c.Command) m[eventName] += fmt.Sprintf("%s;", c.Command)
case ComplexJsCommand:
eventName := hx.FormatEventName(event, true)
context.AddScript(c.TempFuncName, c.Command)
m[eventName] += fmt.Sprintf("%s();", c.TempFuncName)
case *AttributeMap: case *AttributeMap:
for k, v := range c.ToMap() { for k, v := range c.ToMap() {
l.fromAttributeMap(event, k, v, builder) l.fromAttributeMap(event, k, v, context)
} }
} }
} }
@ -129,5 +168,5 @@ func (l *LifeCycle) Render(builder *strings.Builder) {
return return
} }
Children(children...).Render(builder) Children(children...).Render(context)
} }

View file

@ -0,0 +1,13 @@
package util
import "math/rand"
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func RandSeq(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

View file

@ -14,6 +14,9 @@ COPY go.mod go.sum ./
# Download and cache the Go modules # Download and cache the Go modules
RUN go mod download RUN go mod download
# Always download the latest version of the framework
RUN go get github.com/maddalax/htmgo/framework@latest
# Copy the source code into the container # Copy the source code into the container
COPY . . COPY . .

View file

@ -4,7 +4,7 @@ go 1.23.0
require ( require (
github.com/labstack/echo/v4 v4.12.0 github.com/labstack/echo/v4 v4.12.0
github.com/maddalax/htmgo/framework v0.0.0-20240921174901-797c439244a5 github.com/maddalax/htmgo/framework v0.0.0-20240921191008-59900b59eaaa
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/yuin/goldmark v1.7.4 github.com/yuin/goldmark v1.7.4
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc

View file

@ -20,6 +20,8 @@ github.com/maddalax/htmgo/framework v0.0.0-20240921172455-97affb99b5dd h1:zA5itp
github.com/maddalax/htmgo/framework v0.0.0-20240921172455-97affb99b5dd/go.mod h1:WRIlLlHJG/xB+RR84LgNFq3hwYFKXvLfEEG8RzTUH50= github.com/maddalax/htmgo/framework v0.0.0-20240921172455-97affb99b5dd/go.mod h1:WRIlLlHJG/xB+RR84LgNFq3hwYFKXvLfEEG8RzTUH50=
github.com/maddalax/htmgo/framework v0.0.0-20240921174901-797c439244a5 h1:TX+YMeHPi2hVbDKuYmTRzPUSc3RD/w0WHML+MymsYPs= github.com/maddalax/htmgo/framework v0.0.0-20240921174901-797c439244a5 h1:TX+YMeHPi2hVbDKuYmTRzPUSc3RD/w0WHML+MymsYPs=
github.com/maddalax/htmgo/framework v0.0.0-20240921174901-797c439244a5/go.mod h1:WRIlLlHJG/xB+RR84LgNFq3hwYFKXvLfEEG8RzTUH50= github.com/maddalax/htmgo/framework v0.0.0-20240921174901-797c439244a5/go.mod h1:WRIlLlHJG/xB+RR84LgNFq3hwYFKXvLfEEG8RzTUH50=
github.com/maddalax/htmgo/framework v0.0.0-20240921191008-59900b59eaaa h1:YKSx3JUpJfwjl9pF0eDd8OyVIlTp24BPvBNHtv/Vmns=
github.com/maddalax/htmgo/framework v0.0.0-20240921191008-59900b59eaaa/go.mod h1:WRIlLlHJG/xB+RR84LgNFq3hwYFKXvLfEEG8RzTUH50=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=

View file

@ -2,7 +2,6 @@ package main
import ( import (
"embed" "embed"
"fmt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"github.com/maddalax/htmgo/framework/htmgo/service" "github.com/maddalax/htmgo/framework/htmgo/service"
@ -30,8 +29,6 @@ func main() {
panic(err) panic(err)
} }
fmt.Println("test")
h.Start(h.AppOpts{ h.Start(h.AppOpts{
ServiceLocator: locator, ServiceLocator: locator,
LiveReload: true, LiveReload: true,

View file

@ -3,20 +3,11 @@ package base
import ( import (
"github.com/maddalax/htmgo/framework/h" "github.com/maddalax/htmgo/framework/h"
"htmgo-site/partials" "htmgo-site/partials"
"strings"
) )
func Extensions() string {
extensions := []string{"path-deps", "response-targets", "mutation-error"}
if h.IsDevelopment() {
extensions = append(extensions, "livereload")
}
return strings.Join(extensions, ", ")
}
func RootPage(children ...h.Ren) *h.Element { func RootPage(children ...h.Ren) *h.Element {
return h.Html( return h.Html(
h.HxExtension(Extensions()), h.HxExtension(h.BaseExtensions()),
h.Head( h.Head(
h.Meta("viewport", "width=device-width, initial-scale=1"), h.Meta("viewport", "width=device-width, initial-scale=1"),
h.Link("/public/main.css", "stylesheet"), h.Link("/public/main.css", "stylesheet"),