add support for complex raw js
This commit is contained in:
parent
59900b59ea
commit
789b9e9c7c
14 changed files with 4086 additions and 77 deletions
3901
framework/assets/dist/htmgo.js
vendored
3901
framework/assets/dist/htmgo.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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;
|
||||
|
|
|
|||
26
framework/assets/js/htmxextensions/htmgo.ts
Normal file
26
framework/assets/js/htmxextensions/htmgo.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
11
framework/h/extensions.go
Normal 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, ", ")
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
13
framework/internal/util/random.go
Normal file
13
framework/internal/util/random.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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 . .
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue