htmgo/framework/h/lifecycle.go

516 lines
16 KiB
Go
Raw Normal View History

package h
import (
"fmt"
"github.com/google/uuid"
2024-09-21 03:59:07 +00:00
"github.com/maddalax/htmgo/framework/hx"
2024-09-22 15:46:38 +00:00
"github.com/maddalax/htmgo/framework/internal/util"
2024-09-22 16:53:41 +00:00
"strings"
"time"
)
type LifeCycle struct {
2024-09-21 03:59:07 +00:00
handlers map[hx.Event][]Command
}
func NewLifeCycle() *LifeCycle {
return &LifeCycle{
2024-09-21 03:59:07 +00:00
handlers: make(map[hx.Event][]Command),
}
}
2024-09-21 03:59:07 +00:00
func validateCommands(cmds []Command) {
for _, cmd := range cmds {
switch t := cmd.(type) {
2024-09-22 15:46:38 +00:00
case SimpleJsCommand:
break
case ComplexJsCommand:
2024-09-21 03:59:07 +00:00
break
2024-09-30 17:39:48 +00:00
case *AttributeMapOrdered:
2024-09-21 03:59:07 +00:00
break
case *Element:
panic(fmt.Sprintf("element is not allowed in lifecycle events. Got: %v", t))
default:
panic(fmt.Sprintf("type is not allowed in lifecycle events. Got: %v", t))
}
}
}
func (l *LifeCycle) OnEvent(event hx.Event, cmd ...Command) *LifeCycle {
validateCommands(cmd)
2024-09-22 16:53:41 +00:00
if strings.HasPrefix(event, "htmx:") {
event = event[5:]
event = util.ConvertCamelToDash(fmt.Sprintf("hx-on::%s", event))
}
if l.handlers[event] == nil {
2024-09-21 03:59:07 +00:00
l.handlers[event] = []Command{}
}
2024-09-21 03:59:07 +00:00
l.handlers[event] = append(l.handlers[event], cmd...)
return l
}
2024-10-26 02:59:17 +00:00
// OnLoad executes the given commands when the element is loaded into the DOM, it also executes when the element is replaced / swapped in.
// This will work on any element because of the htmgo htmx extension to trigger it, instead of the browser.
func OnLoad(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.LoadDomEvent, cmd...)
}
func (l *LifeCycle) HxBeforeRequest(cmd ...Command) *LifeCycle {
2024-09-21 03:59:07 +00:00
l.OnEvent(hx.BeforeRequestEvent, cmd...)
return l
}
2024-10-26 02:59:17 +00:00
// HxOnLoad executes the given commands when the element is loaded into the DOM.
// Deprecated: Use OnLoad instead.
func HxOnLoad(cmd ...Command) *LifeCycle {
2024-09-21 03:59:07 +00:00
return NewLifeCycle().OnEvent(hx.LoadEvent, cmd...)
}
2024-10-26 02:59:17 +00:00
// HxOnAfterSwap executes the given commands when the element is swapped in.
func HxOnAfterSwap(cmd ...Command) *LifeCycle {
2024-09-21 03:59:07 +00:00
return NewLifeCycle().OnEvent(hx.AfterSwapEvent, cmd...)
}
2024-10-26 02:59:17 +00:00
// OnClick executes the given commands when the element is clicked.
2024-09-21 03:59:07 +00:00
func OnClick(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.ClickEvent, cmd...)
}
2024-10-26 02:59:17 +00:00
// OnEvent executes the given commands when the given event is triggered.
2024-09-21 03:59:07 +00:00
func OnEvent(event hx.Event, cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(event, cmd...)
}
2024-10-26 02:59:17 +00:00
// HxBeforeSseMessage executes the given commands when a message is received from the server via SSE, but before it is processed.
2024-10-02 03:26:03 +00:00
func HxBeforeSseMessage(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SseBeforeMessageEvent, cmd...)
2024-10-01 17:09:22 +00:00
}
2024-10-26 02:59:17 +00:00
// HxAfterSseMessage executes the given commands when a message is received from the server via SSE, and after it is processed.
2024-10-02 03:26:03 +00:00
func HxAfterSseMessage(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SseAfterMessageEvent, cmd...)
2024-10-01 17:09:22 +00:00
}
2024-10-26 02:59:17 +00:00
// OnSubmit executes the given commands when the form is submitted.
2024-10-01 17:09:22 +00:00
func OnSubmit(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SubmitEvent, cmd...)
}
2024-10-26 02:59:17 +00:00
// HxOnSseError executes the given commands when an error occurs while connecting to the server via SSE.
2024-10-02 03:26:03 +00:00
func HxOnSseError(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SseErrorEvent, cmd...)
2024-10-01 17:09:22 +00:00
}
2024-10-26 02:59:17 +00:00
// HxOnSseClose executes the given commands when the connection to the server via SSE is closed.
2024-10-02 03:26:03 +00:00
func HxOnSseClose(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SseClosedEvent, cmd...)
2024-10-01 17:09:22 +00:00
}
2024-10-26 02:59:17 +00:00
// HxOnSseConnecting executes the given commands when the connection to the server via SSE is being established.
2024-10-02 03:26:03 +00:00
func HxOnSseConnecting(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SseConnectingEvent, cmd...)
}
2024-10-26 02:59:17 +00:00
// HxOnSseOpen executes the given commands when the connection to the server via SSE is established.
2024-10-02 03:26:03 +00:00
func HxOnSseOpen(cmd ...Command) *LifeCycle {
return NewLifeCycle().OnEvent(hx.SseConnectedEvent, cmd...)
}
2024-10-26 02:59:17 +00:00
// HxBeforeRequest executes the given commands before the request is sent.
func HxBeforeRequest(cmd ...Command) *LifeCycle {
return NewLifeCycle().HxBeforeRequest(cmd...)
}
2024-10-26 02:59:17 +00:00
// HxAfterRequest executes the given commands after the request is sent.
func HxAfterRequest(cmd ...Command) *LifeCycle {
return NewLifeCycle().HxAfterRequest(cmd...)
}
2024-10-26 02:59:17 +00:00
// HxOnMutationError executes the given commands when a mutation error of a request occurs.
func HxOnMutationError(cmd ...Command) *LifeCycle {
return NewLifeCycle().HxOnMutationError(cmd...)
}
func (l *LifeCycle) HxAfterRequest(cmd ...Command) *LifeCycle {
2024-09-21 03:59:07 +00:00
l.OnEvent(hx.AfterRequestEvent, cmd...)
return l
}
func (l *LifeCycle) HxOnMutationError(cmd ...Command) *LifeCycle {
2024-09-21 03:59:07 +00:00
l.OnEvent(hx.OnMutationErrorEvent, cmd...)
return l
}
2024-09-21 03:59:07 +00:00
type Command = Ren
2024-09-22 15:46:38 +00:00
type SimpleJsCommand struct {
Command string
}
2024-09-22 15:46:38 +00:00
type ComplexJsCommand struct {
Command string
TempFuncName string
2024-09-21 03:59:07 +00:00
}
2024-10-26 02:59:17 +00:00
// NewComplexJsCommand creates a new complex JavaScript command.
func NewComplexJsCommand(command string) ComplexJsCommand {
name := fmt.Sprintf("__eval_%s", util.RandSeq(6))
return ComplexJsCommand{Command: command, TempFuncName: name}
}
2024-10-26 02:59:17 +00:00
// SetText sets the inner text of the element.
2024-09-22 15:46:38 +00:00
func SetText(text string) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).innerText = '%s'", text)}
}
2024-10-26 02:59:17 +00:00
// SetTextOnChildren sets the inner text of all the children of the element that match the selector.
2024-09-29 13:21:58 +00:00
func SetTextOnChildren(selector, text string) ComplexJsCommand {
// language=JavaScript
return EvalJs(fmt.Sprintf(`
var children = self.querySelectorAll('%s');
children.forEach(function(child) {
child.innerText = '%s';
});
`, selector, text))
}
2024-10-26 02:59:17 +00:00
// Increment increments the inner text of the element by the given amount.
2024-09-22 15:46:38 +00:00
func Increment(amount int) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).innerText = parseInt((self || this).innerText) + %d", amount)}
}
2024-10-26 02:59:17 +00:00
// SetInnerHtml sets the inner HTML of the element.
2024-09-22 15:46:38 +00:00
func SetInnerHtml(r Ren) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).innerHTML = `%s`", Render(r))}
}
2024-10-26 02:59:17 +00:00
// SetOuterHtml sets the outer HTML of the element.
2024-09-22 15:46:38 +00:00
func SetOuterHtml(r Ren) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).outerHTML = `%s`", Render(r))}
}
2024-10-26 02:59:17 +00:00
// AddAttribute adds the given attribute to the element.
2024-09-22 15:46:38 +00:00
func AddAttribute(name, value string) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).setAttribute('%s', '%s')", name, value)}
}
2024-10-26 02:59:17 +00:00
// SetDisabled sets the disabled attribute on the element.
2024-09-22 15:46:38 +00:00
func SetDisabled(disabled bool) SimpleJsCommand {
if disabled {
return AddAttribute("disabled", "true")
} else {
return RemoveAttribute("disabled")
}
}
2024-10-26 02:59:17 +00:00
// RemoveAttribute removes the given attribute from the element.
2024-09-22 15:46:38 +00:00
func RemoveAttribute(name string) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).removeAttribute('%s')", name)}
}
2024-10-26 02:59:17 +00:00
// AddClass adds the given class to the element.
2024-09-22 15:46:38 +00:00
func AddClass(class string) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).classList.add('%s')", class)}
}
2024-10-26 02:59:17 +00:00
// RemoveClass removes the given class from the element.
2024-09-22 15:46:38 +00:00
func RemoveClass(class string) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).classList.remove('%s')", class)}
}
2024-10-26 02:59:17 +00:00
// ToggleClass toggles the given class on the element.
2024-09-22 15:46:38 +00:00
func ToggleClass(class string) SimpleJsCommand {
2024-09-19 23:11:12 +00:00
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || 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))
}
2024-10-26 02:59:17 +00:00
// ToggleClassOnElement toggles the given class on the elements returned by the selector.
2024-09-22 15:46:38 +00:00
func ToggleClassOnElement(selector, class string) ComplexJsCommand {
// language=JavaScript
return EvalJs(fmt.Sprintf(`
var el = document.querySelector('%s');
if(el) { el.classList.toggle('%s'); }`,
2024-09-22 16:25:18 +00:00
selector, class,
2024-09-22 15:46:38 +00:00
))
}
2024-09-20 20:15:12 +00:00
2024-10-26 02:59:17 +00:00
// EvalJsOnParent evaluates the given JavaScript code on the parent of the element. Reference the element using 'element'.
func EvalJsOnParent(js string) ComplexJsCommand {
2024-09-29 13:21:58 +00:00
// language=JavaScript
return EvalJs(fmt.Sprintf(`
if(self.parentElement) {
let element = self.parentElement;
%s
}
`, js))
2024-09-29 13:21:58 +00:00
}
2024-10-26 02:59:17 +00:00
// EvalJsOnChildren evaluates the given JavaScript code on the children of the element. Reference the element using 'element'.
func EvalJsOnChildren(selector, js string) ComplexJsCommand {
2024-09-29 13:21:58 +00:00
// language=JavaScript
return EvalJs(fmt.Sprintf(`
let children = self.querySelectorAll('%s');
children.forEach(function(element) {
%s
2024-09-29 13:21:58 +00:00
});
`, selector, js))
2024-09-29 13:21:58 +00:00
}
2024-10-26 02:59:17 +00:00
// EvalJsOnSibling evaluates the given JavaScript code on the siblings of the element. Reference the element using 'element'.
func EvalJsOnSibling(selector, js string) ComplexJsCommand {
2024-09-29 13:21:58 +00:00
// language=JavaScript
return EvalJs(fmt.Sprintf(`
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.
2024-09-29 16:50:30 +00:00
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.
2024-09-29 16:50:30 +00:00
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.
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))
}
2024-10-26 02:59:17 +00:00
// SetClassOnSibling sets the given class on the siblings of the element. Reference the element using 'element'.
func SetClassOnSibling(selector, class string) ComplexJsCommand {
// language=JavaScript
return EvalJsOnSibling(selector, fmt.Sprintf("element.classList.add('%s')", class))
}
2024-10-26 02:59:17 +00:00
// RemoveClassOnSibling removes the given class from the siblings of the element. Reference the element using 'element'.
func RemoveClassOnSibling(selector, class string) ComplexJsCommand {
// language=JavaScript
return EvalJsOnSibling(selector, fmt.Sprintf("element.classList.remove('%s')", class))
2024-09-29 13:21:58 +00:00
}
2024-10-26 02:59:17 +00:00
// RemoveClassOnChildren removes the given class from the children of the element. Reference the element using 'element'.
2024-09-29 13:21:58 +00:00
func RemoveClassOnChildren(selector, class string) ComplexJsCommand {
// language=JavaScript
return EvalJsOnChildren(selector, fmt.Sprintf("element.classList.remove('%s')", class))
2024-09-29 13:21:58 +00:00
}
2024-10-26 02:59:17 +00:00
// Alert displays an alert dialog with the given text.
2024-09-22 15:46:38 +00:00
func Alert(text string) SimpleJsCommand {
2024-09-20 20:15:12 +00:00
// language=JavaScript
2024-09-22 15:46:38 +00:00
return SimpleJsCommand{Command: fmt.Sprintf("alert('%s')", text)}
}
2024-10-26 02:59:17 +00:00
// Remove removes the element from the DOM.
2024-09-26 14:23:22 +00:00
func Remove() SimpleJsCommand {
// language=JavaScript
return SimpleJsCommand{Command: "(self || this).remove()"}
2024-09-26 14:23:22 +00:00
}
2024-10-26 02:59:17 +00:00
// EvalJs evaluates the given JavaScript code.
2024-09-22 15:46:38 +00:00
func EvalJs(js string) ComplexJsCommand {
return NewComplexJsCommand(js)
2024-09-22 15:46:38 +00:00
}
func CombineCommands(cmds ...Command) string {
lines := make([]string, len(cmds))
for i, cmd := range cmds {
lines[i] = Render(cmd)
lines[i] = strings.ReplaceAll(lines[i], "(self || this).", "self.")
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 code
}
func EvalCommandsOnSelector(selector string, cmds ...Command) ComplexJsCommand {
code := CombineCommands(cmds...)
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...)
}
2024-10-26 02:59:17 +00:00
// PreventDefault prevents the default action of the event.
2024-10-01 17:09:22 +00:00
func PreventDefault() SimpleJsCommand {
// language=JavaScript
return SimpleJsCommand{Command: "event.preventDefault()"}
}
2024-10-26 02:59:17 +00:00
// ConsoleLog logs a message to the console.
2024-10-01 17:09:22 +00:00
func ConsoleLog(text string) SimpleJsCommand {
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("console.log('%s')", text)}
}
2024-10-26 02:59:17 +00:00
// SetValue sets the value of the element.
func SetValue(value string) SimpleJsCommand {
// language=JavaScript
return SimpleJsCommand{Command: fmt.Sprintf("(self || this).value = '%s'", value)}
}
2024-10-26 02:59:17 +00:00
// SubmitFormOnEnter submits the form when the user presses the enter key.
2024-09-29 13:47:39 +00:00
func SubmitFormOnEnter() ComplexJsCommand {
// language=JavaScript
return EvalJs(`
2024-10-03 20:35:19 +00:00
if (event.code === 'Enter') { self.form.dispatchEvent(new Event('submit', { cancelable: true })); }
2024-09-29 13:47:39 +00:00
`)
}
2024-10-26 02:59:17 +00:00
// InjectScript injects a script tag into the document.
2024-09-22 15:46:38 +00:00
func InjectScript(src string) ComplexJsCommand {
// language=JavaScript
return NewComplexJsCommand(fmt.Sprintf(`
2024-09-20 20:15:12 +00:00
var script = document.createElement('script');
script.src = '%s';
script.async = true;
2024-09-20 20:15:12 +00:00
document.head.appendChild(script);
`, src))
2024-09-20 20:15:12 +00:00
}
2024-09-22 15:46:38 +00:00
2024-10-26 02:59:17 +00:00
// InjectScriptIfNotExist injects a script tag into the document if it does not already exist.
2024-09-22 15:46:38 +00:00
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))
}
func RunOnInterval(time time.Duration, cmds ...Command) ComplexJsCommand {
code := strings.Builder{}
for _, cmd := range cmds {
code.WriteString(fmt.Sprintf(`
setInterval(function() {
%s
}, %d)
`, Render(cmd), time.Milliseconds()))
}
return EvalJs(code.String())
}
func RunAfterTimeout(time time.Duration, cmds ...Command) ComplexJsCommand {
code := strings.Builder{}
for _, cmd := range cmds {
code.WriteString(fmt.Sprintf(`
setTimeout(function() {
%s
}, %d)
`, Render(cmd), time.Milliseconds()))
}
return EvalJs(code.String())
}