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/mutation-error";
import "./htmxextensions/livereload"
import "./htmxextensions/htmgo";
function watchUrl(callback: (oldUrl: string, newUrl: string) => void) {
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"
"reflect"
"runtime"
"strings"
)
type Partial struct {
@ -13,10 +12,6 @@ type Partial struct {
Root *Element
}
func (p *Partial) Render(builder *strings.Builder) {
p.Root.Render(builder)
}
type Page struct {
Root Ren
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 (
"fmt"
"github.com/maddalax/htmgo/framework/hx"
"strings"
"github.com/maddalax/htmgo/framework/internal/util"
)
type LifeCycle struct {
@ -19,7 +19,9 @@ func NewLifeCycle() *LifeCycle {
func validateCommands(cmds []Command) {
for _, cmd := range cmds {
switch t := cmd.(type) {
case JsCommand:
case SimpleJsCommand:
break
case ComplexJsCommand:
break
case *AttributeMap:
break
@ -92,40 +94,41 @@ func (l *LifeCycle) OnMutationError(cmd ...Command) *LifeCycle {
type Command = Ren
type JsCommand struct {
type SimpleJsCommand struct {
Command string
}
func (j JsCommand) Render(builder *strings.Builder) {
builder.WriteString(j.Command)
type ComplexJsCommand struct {
Command string
TempFuncName string
}
func SetText(text string) JsCommand {
func SetText(text string) SimpleJsCommand {
// 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
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
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
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
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 {
return AddAttribute("disabled", "true")
} else {
@ -133,36 +136,62 @@ func SetDisabled(disabled bool) JsCommand {
}
}
func RemoveAttribute(name string) JsCommand {
func RemoveAttribute(name string) SimpleJsCommand {
// 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
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
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
return JsCommand{Command: fmt.Sprintf("alert('%s')", text)}
return SimpleJsCommand{Command: fmt.Sprintf("this.classList.toggle('%s')", class)}
}
func EvalJs(js string) JsCommand {
return JsCommand{Command: js}
func ToggleClassOnElement(selector, class string) ComplexJsCommand {
// 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
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');
script.src = '%s';
src.async = true;
document.head.appendChild(script);
`, 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 {
Render(builder *strings.Builder)
Render(context *RenderContext)
}
func Render(node Ren) string {
start := time.Now()
builder := &strings.Builder{}
node.Render(builder)
context := &RenderContext{
builder: builder,
}
node.Render(context)
duration := time.Since(start)
fmt.Printf("render took %d microseconds\n", duration.Microseconds())
return builder.String()

View file

@ -6,15 +6,30 @@ import (
"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
if node.tag != "" {
builder.WriteString("<" + node.tag)
builder.WriteString(" ")
context.builder.WriteString("<" + node.tag)
context.builder.WriteString(" ")
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 {
switch child.(type) {
case *AttributeMap:
child.Render(builder)
child.Render(context)
case *LifeCycle:
child.Render(builder)
child.Render(context)
}
}
// close the tag
if node.tag != "" {
builder.WriteString(">")
context.builder.WriteString(">")
}
// render the children elements that are not attributes
@ -54,64 +69,88 @@ func (node *Element) Render(builder *strings.Builder) {
case *LifeCycle:
continue
default:
child.Render(builder)
child.Render(context)
}
}
if node.tag != "" {
builder.WriteString("</" + node.tag + ">")
renderScripts(context)
context.builder.WriteString("</" + node.tag + ">")
}
}
func (a *AttributeR) Render(builder *strings.Builder) {
builder.WriteString(fmt.Sprintf(`%s="%s"`, a.Name, a.Value))
func renderScripts(context *RenderContext) {
for _, script := range context.scripts {
context.builder.WriteString(script)
}
context.scripts = []string{}
}
func (t *TextContent) Render(builder *strings.Builder) {
builder.WriteString(t.Content)
func (a *AttributeR) Render(context *RenderContext) {
context.builder.WriteString(fmt.Sprintf(`%s="%s"`, a.Name, a.Value))
}
func (r *RawContent) Render(builder *strings.Builder) {
builder.WriteString(r.Content)
func (t *TextContent) Render(context *RenderContext) {
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 {
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()
for k, v := range m2 {
builder.WriteString(" ")
NewAttribute(k, v).Render(builder)
context.builder.WriteString(" ")
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 {
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)
for event, commands := range l.handlers {
m[event] = ""
for _, command := range commands {
switch c := command.(type) {
case JsCommand:
case SimpleJsCommand:
eventName := hx.FormatEventName(event, true)
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:
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
}
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
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 . .

View file

@ -4,7 +4,7 @@ go 1.23.0
require (
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/yuin/goldmark v1.7.4
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-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-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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=

View file

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

View file

@ -3,20 +3,11 @@ package base
import (
"github.com/maddalax/htmgo/framework/h"
"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 {
return h.Html(
h.HxExtension(Extensions()),
h.HxExtension(h.BaseExtensions()),
h.Head(
h.Meta("viewport", "width=device-width, initial-scale=1"),
h.Link("/public/main.css", "stylesheet"),