so much stuff
This commit is contained in:
parent
35ee5959b3
commit
3d18f2d89b
23 changed files with 677 additions and 212 deletions
|
|
@ -2,6 +2,7 @@ package h
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"html"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
@ -14,6 +15,10 @@ type Partial struct {
|
||||||
Root *Node
|
Root *Node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Partial) ToNode() *Node {
|
||||||
|
return p.Root
|
||||||
|
}
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
Root *Node
|
Root *Node
|
||||||
HttpMethod string
|
HttpMethod string
|
||||||
|
|
@ -55,5 +60,5 @@ func GetPartialPath(partial func(ctx *fiber.Ctx) *Partial) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPartialPathWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) string {
|
func GetPartialPathWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) string {
|
||||||
return GetPartialPath(partial) + "?" + qs
|
return html.EscapeString(GetPartialPath(partial) + "?" + qs)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ func LiveReloadHandler(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func LiveReload() *Node {
|
func LiveReload() *Node {
|
||||||
return Div(Get("/livereload"), Trigger("every 100ms"))
|
return Div(Get("/livereload"), Trigger("every 2s"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddLiveReloadHandler(path string, app *fiber.App) {
|
func AddLiveReloadHandler(path string, app *fiber.App) {
|
||||||
|
|
|
||||||
18
h/render.go
18
h/render.go
|
|
@ -9,7 +9,8 @@ import (
|
||||||
const FlagSkip = "skip"
|
const FlagSkip = "skip"
|
||||||
const FlagText = "text"
|
const FlagText = "text"
|
||||||
const FlagRaw = "raw"
|
const FlagRaw = "raw"
|
||||||
const FlagAttributeList = "attribute-list"
|
const FlagAttributeList = "x-attribute-list"
|
||||||
|
const FlagChildrenList = "x-children-list"
|
||||||
|
|
||||||
type Builder struct {
|
type Builder struct {
|
||||||
builder *strings.Builder
|
builder *strings.Builder
|
||||||
|
|
@ -38,6 +39,21 @@ func (page Builder) renderNode(node *Node) {
|
||||||
node.attributes = map[string]string{}
|
node.attributes = map[string]string{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flatChildren := make([]*Node, 0)
|
||||||
|
for _, child := range node.children {
|
||||||
|
flatChildren = append(flatChildren, child)
|
||||||
|
if child.tag == FlagChildrenList {
|
||||||
|
for _, gc := range child.children {
|
||||||
|
flatChildren = append(flatChildren, gc)
|
||||||
|
}
|
||||||
|
child.tag = FlagSkip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(flatChildren) > 0 {
|
||||||
|
node.children = flatChildren
|
||||||
|
}
|
||||||
|
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
|
|
||||||
if child == nil {
|
if child == nil {
|
||||||
|
|
|
||||||
256
h/tag.go
256
h/tag.go
|
|
@ -2,6 +2,7 @@ package h
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"html"
|
"html"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
@ -18,17 +19,6 @@ type Node struct {
|
||||||
changed bool
|
changed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNode(tag string) Node {
|
|
||||||
return Node{
|
|
||||||
tag: tag,
|
|
||||||
attributes: nil,
|
|
||||||
children: nil,
|
|
||||||
text: "",
|
|
||||||
value: "",
|
|
||||||
id: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Action struct {
|
type Action struct {
|
||||||
Type string
|
Type string
|
||||||
Target *Node
|
Target *Node
|
||||||
|
|
@ -76,22 +66,39 @@ func MergeClasses(classes ...string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Id(value string) *Node {
|
func Id(value string) *Node {
|
||||||
|
if strings.HasPrefix(value, "#") {
|
||||||
|
value = value[1:]
|
||||||
|
}
|
||||||
return Attribute("id", value)
|
return Attribute("id", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Attribute(key string, value string) *Node {
|
func Attributes(attrs map[string]string) *Node {
|
||||||
return &Node{
|
return &Node{
|
||||||
tag: "attribute",
|
tag: "attribute",
|
||||||
attributes: map[string]string{
|
attributes: attrs,
|
||||||
key: value,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Attribute(key string, value string) *Node {
|
||||||
|
return Attributes(map[string]string{key: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Disabled() *Node {
|
||||||
|
return Attribute("disabled", "")
|
||||||
|
}
|
||||||
|
|
||||||
func Get(path string) *Node {
|
func Get(path string) *Node {
|
||||||
return Attribute("hx-get", path)
|
return Attribute("hx-get", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetPartial(partial func(ctx *fiber.Ctx) *Partial) *Node {
|
||||||
|
return Get(GetPartialPath(partial))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPartialWithQs(partial func(ctx *fiber.Ctx) *Partial, qs string) *Node {
|
||||||
|
return Get(GetPartialPathWithQs(partial, qs))
|
||||||
|
}
|
||||||
|
|
||||||
func CreateTriggers(triggers ...string) []string {
|
func CreateTriggers(triggers ...string) []string {
|
||||||
return triggers
|
return triggers
|
||||||
}
|
}
|
||||||
|
|
@ -100,14 +107,24 @@ type ReloadParams struct {
|
||||||
Triggers []string
|
Triggers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ViewOnLoad(partial func(ctx *fiber.Ctx) *Partial) *Node {
|
||||||
|
return View(partial, ReloadParams{
|
||||||
|
Triggers: CreateTriggers("load"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func View(partial func(ctx *fiber.Ctx) *Partial, params ReloadParams) *Node {
|
func View(partial func(ctx *fiber.Ctx) *Partial, params ReloadParams) *Node {
|
||||||
return &Node{
|
return Div(Attributes(map[string]string{
|
||||||
tag: "attribute",
|
"hx-get": GetPartialPath(partial),
|
||||||
attributes: map[string]string{
|
"hx-trigger": strings.Join(params.Triggers, ", "),
|
||||||
"hx-get": GetPartialPath(partial),
|
}))
|
||||||
"hx-trigger": strings.Join(params.Triggers, ", "),
|
}
|
||||||
},
|
|
||||||
}
|
func ViewWithTriggers(partial func(ctx *fiber.Ctx) *Partial, triggers ...string) *Node {
|
||||||
|
return Div(Attributes(map[string]string{
|
||||||
|
"hx-get": GetPartialPath(partial),
|
||||||
|
"hx-trigger": strings.Join(triggers, ", "),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWithQs(path string, qs map[string]string) *Node {
|
func GetWithQs(path string, qs map[string]string) *Node {
|
||||||
|
|
@ -142,6 +159,10 @@ func Text(text string) *Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Pf(format string, args ...interface{}) *Node {
|
||||||
|
return P(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
func Target(target string) *Node {
|
func Target(target string) *Node {
|
||||||
return Attribute("hx-target", target)
|
return Attribute("hx-target", target)
|
||||||
}
|
}
|
||||||
|
|
@ -166,37 +187,32 @@ func Placeholder(placeholder string) *Node {
|
||||||
return Attribute("placeholder", placeholder)
|
return Attribute("placeholder", placeholder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Swap(swap string) *Node {
|
func OutOfBandSwap(selector string) *Node {
|
||||||
return Attribute("hx-swap", swap)
|
return Attribute("hx-swap-oob",
|
||||||
|
Ternary(selector == "", "true", selector))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Click(value string) *Node {
|
func Click(value string) *Node {
|
||||||
return Attribute("onclick", value)
|
return Attribute("onclick", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OnClickWs(handler string) *Node {
|
func Tag(tag string, children ...*Node) *Node {
|
||||||
return Attribute("data-ws-click", handler)
|
return &Node{
|
||||||
|
tag: tag,
|
||||||
|
children: children,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Html(children ...*Node) *Node {
|
func Html(children ...*Node) *Node {
|
||||||
return &Node{
|
return Tag("html", children...)
|
||||||
tag: "html",
|
|
||||||
children: children,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Head(children ...*Node) *Node {
|
func Head(children ...*Node) *Node {
|
||||||
return &Node{
|
return Tag("head", children...)
|
||||||
tag: "head",
|
|
||||||
children: children,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Body(children ...*Node) *Node {
|
func Body(children ...*Node) *Node {
|
||||||
return &Node{
|
return Tag("body", children...)
|
||||||
tag: "body",
|
|
||||||
children: children,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Script(url string) *Node {
|
func Script(url string) *Node {
|
||||||
|
|
@ -222,10 +238,7 @@ func RawScript(text string) *Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Div(children ...*Node) *Node {
|
func Div(children ...*Node) *Node {
|
||||||
return &Node{
|
return Tag("div", children...)
|
||||||
tag: "div",
|
|
||||||
children: children,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Input(inputType string, children ...*Node) *Node {
|
func Input(inputType string, children ...*Node) *Node {
|
||||||
|
|
@ -238,26 +251,6 @@ func Input(inputType string, children ...*Node) *Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HStack(children ...*Node) *Node {
|
|
||||||
return &Node{
|
|
||||||
tag: "div",
|
|
||||||
attributes: map[string]string{
|
|
||||||
"class": "flex gap-2",
|
|
||||||
},
|
|
||||||
children: children,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func VStack(children ...*Node) *Node {
|
|
||||||
return &Node{
|
|
||||||
tag: "div",
|
|
||||||
attributes: map[string]string{
|
|
||||||
"class": "flex flex-col gap-2",
|
|
||||||
},
|
|
||||||
children: children,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func List[T any](items []T, mapper func(item T) *Node) *Node {
|
func List[T any](items []T, mapper func(item T) *Node) *Node {
|
||||||
node := &Node{
|
node := &Node{
|
||||||
tag: "",
|
tag: "",
|
||||||
|
|
@ -290,10 +283,7 @@ func AppendChildren(node *Node, children ...*Node) *Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Button(children ...*Node) *Node {
|
func Button(children ...*Node) *Node {
|
||||||
return &Node{
|
return Tag("button", children...)
|
||||||
tag: "button",
|
|
||||||
children: children,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Indicator(tag string) *Node {
|
func Indicator(tag string) *Node {
|
||||||
|
|
@ -316,6 +306,10 @@ func A(text string, children ...*Node) *Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Nav(children ...*Node) *Node {
|
||||||
|
return Tag("nav", children...)
|
||||||
|
}
|
||||||
|
|
||||||
func Empty() *Node {
|
func Empty() *Node {
|
||||||
return &Node{
|
return &Node{
|
||||||
tag: "",
|
tag: "",
|
||||||
|
|
@ -327,11 +321,59 @@ func BeforeRequestSetHtml(children ...*Node) *Node {
|
||||||
return Attribute("hx-on::before-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
|
return Attribute("hx-on::before-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BeforeRequestSetAttribute(key string, value string) *Node {
|
||||||
|
return Attribute("hx-on::before-request", `this.setAttribute('`+key+`', '`+value+`')`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BeforeRequestSetText(text string) *Node {
|
||||||
|
return Attribute("hx-on::before-request", `this.innerText = '`+text+`'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AfterRequestRemoveAttribute(key string, value string) *Node {
|
||||||
|
return Attribute("hx-on::after-request", `this.removeAttribute('`+key+`')`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IfQueryParam(key string, node *Node) *Node {
|
||||||
|
return Fragment(Attribute("hx-if-qp:"+key, "true"), node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Hidden() *Node {
|
||||||
|
return Attribute("style", "display:none")
|
||||||
|
}
|
||||||
|
|
||||||
|
func MatchQueryParam(defaultValue string, active string, m map[string]*Node) *Node {
|
||||||
|
|
||||||
|
rendered := make(map[string]string)
|
||||||
|
for s, node := range m {
|
||||||
|
rendered[s] = Render(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := Tag("span",
|
||||||
|
m[active],
|
||||||
|
Trigger("url"),
|
||||||
|
Attribute("hx-match-qp", "true"),
|
||||||
|
Attribute("hx-match-qp-default", defaultValue),
|
||||||
|
)
|
||||||
|
|
||||||
|
for s, node := range rendered {
|
||||||
|
root = AppendChildren(root, Attribute("hx-match-qp-mapping:"+s, ``+html.EscapeString(node)+``))
|
||||||
|
}
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
func AfterRequestSetHtml(children ...*Node) *Node {
|
func AfterRequestSetHtml(children ...*Node) *Node {
|
||||||
serialized := Render(Fragment(children...))
|
serialized := Render(Fragment(children...))
|
||||||
return Attribute("hx-on::after-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
|
return Attribute("hx-on::after-request", `this.innerHTML = '`+html.EscapeString(serialized)+`'`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Children(children []*Node) *Node {
|
||||||
|
return &Node{
|
||||||
|
tag: FlagChildrenList,
|
||||||
|
children: children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func If(condition bool, node *Node) *Node {
|
func If(condition bool, node *Node) *Node {
|
||||||
if condition {
|
if condition {
|
||||||
return node
|
return node
|
||||||
|
|
@ -340,19 +382,6 @@ func If(condition bool, node *Node) *Node {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func JsIf(condition string, node *Node) *Node {
|
|
||||||
node1 := &Node{tag: "template"}
|
|
||||||
node1.AppendChild(Attribute("x-if", condition))
|
|
||||||
node1.AppendChild(node)
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
func JsIfElse(condition string, node *Node, node2 *Node) *Node {
|
|
||||||
node1Template := Div(Attribute("x-show", condition), node)
|
|
||||||
node2Template := Div(Attribute("x-show", "!("+condition+")"), node2)
|
|
||||||
return Fragment(node1Template, node2Template)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IfElse(condition bool, node *Node, node2 *Node) *Node {
|
func IfElse(condition bool, node *Node, node2 *Node) *Node {
|
||||||
if condition {
|
if condition {
|
||||||
return node
|
return node
|
||||||
|
|
@ -360,3 +389,70 @@ func IfElse(condition bool, node *Node, node2 *Node) *Node {
|
||||||
return node2
|
return node2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IfElseLazy(condition bool, cb1 func() *Node, cb2 func() *Node) *Node {
|
||||||
|
if condition {
|
||||||
|
return cb1()
|
||||||
|
} else {
|
||||||
|
return cb2()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IfHtmxRequest(ctx *fiber.Ctx, node *Node) *Node {
|
||||||
|
if ctx.Get("HX-Request") != "" {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
return Empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
type SwapArg struct {
|
||||||
|
Selector string
|
||||||
|
Content *Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSwap(selector string, content *Node) SwapArg {
|
||||||
|
return SwapArg{
|
||||||
|
Selector: selector,
|
||||||
|
Content: content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Swap(ctx *fiber.Ctx, content *Node) *Node {
|
||||||
|
return SwapWithSelector(ctx, "", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SwapWithSelector(ctx *fiber.Ctx, selector string, content *Node) *Node {
|
||||||
|
if ctx == nil || ctx.Get("HX-Request") == "" {
|
||||||
|
return Empty()
|
||||||
|
}
|
||||||
|
return content.AppendChild(OutOfBandSwap(selector))
|
||||||
|
}
|
||||||
|
|
||||||
|
func SwapMany(ctx *fiber.Ctx, args ...SwapArg) *Node {
|
||||||
|
if ctx.Get("HX-Request") == "" {
|
||||||
|
return Empty()
|
||||||
|
}
|
||||||
|
for _, arg := range args {
|
||||||
|
arg.Content.AppendChild(OutOfBandSwap(arg.Selector))
|
||||||
|
}
|
||||||
|
return Fragment(Map(args, func(arg SwapArg) *Node {
|
||||||
|
return arg.Content
|
||||||
|
})...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnRequestSwapArgs struct {
|
||||||
|
Target string
|
||||||
|
Get string
|
||||||
|
Default *Node
|
||||||
|
BeforeRequest *Node
|
||||||
|
AfterRequest *Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func OnRequestSwap(args OnRequestSwapArgs) *Node {
|
||||||
|
return Div(args.Default,
|
||||||
|
BeforeRequestSetHtml(args.BeforeRequest),
|
||||||
|
AfterRequestSetHtml(args.AfterRequest),
|
||||||
|
Get(args.Get),
|
||||||
|
Target(args.Target),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
44
h/util.go
Normal file
44
h/util.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package h
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Ternary[T any](value bool, a T, b T) T {
|
||||||
|
if value {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func Map[T any, U any](items []T, fn func(T) U) []U {
|
||||||
|
var result []U
|
||||||
|
for _, item := range items {
|
||||||
|
result = append(result, fn(item))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func JsonSerialize(data any) string {
|
||||||
|
serialized, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(serialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetQueryParam(ctx *fiber.Ctx, key string) string {
|
||||||
|
value := ctx.Query(key)
|
||||||
|
if value == "" {
|
||||||
|
current := ctx.Get("Hx-Current-Url")
|
||||||
|
if current != "" {
|
||||||
|
u, err := url.Parse(current)
|
||||||
|
if err == nil {
|
||||||
|
return u.Query().Get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
66
js/mhtml.js
66
js/mhtml.js
|
|
@ -1,10 +1,62 @@
|
||||||
// Replace 'ws://example.com/socket' with the URL of your WebSocket server
|
|
||||||
|
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
document.querySelectorAll('[m\\:onclick]').forEach(element => {
|
// htmx.logger = function(elt, event, data) {
|
||||||
const value = element.getAttribute('m:onclick')
|
// if(console) {
|
||||||
element.addEventListener('click', () => {
|
// console.log(event);
|
||||||
fetch('/click/' + value).catch()
|
// }
|
||||||
})
|
// }
|
||||||
|
// onUrlChange(window.location.href);
|
||||||
|
|
||||||
|
|
||||||
|
window.history.pushState = new Proxy(window.history.pushState, {
|
||||||
|
apply: (target, thisArg, argArray) => {
|
||||||
|
if(argArray.length > 2) {
|
||||||
|
onUrlChange(window.location.origin + argArray[2]);
|
||||||
|
}
|
||||||
|
return target.apply(thisArg, argArray);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUrlChange(newUrl) {
|
||||||
|
let url = new URL(newUrl);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelectorAll('[hx-trigger]').forEach(function(element) {
|
||||||
|
const triggers = element.getAttribute('hx-trigger');
|
||||||
|
const split = triggers.split(", ");
|
||||||
|
console.log(split)
|
||||||
|
if(split.find(s => s === 'url')) {
|
||||||
|
htmx.trigger(element, "url");
|
||||||
|
} else {
|
||||||
|
for (let [key, values] of url.searchParams) {
|
||||||
|
let eventName = "qs:" + key
|
||||||
|
if (triggers.includes(eventName)) {
|
||||||
|
htmx.trigger(element, eventName);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
document.querySelectorAll('[hx-match-qp]').forEach((el) => {
|
||||||
|
let hasMatch = false;
|
||||||
|
for (let name of el.getAttributeNames()) {
|
||||||
|
if(name.startsWith("hx-match-qp-mapping:")) {
|
||||||
|
let match = name.replace("hx-match-qp-mapping:", "");
|
||||||
|
let value = url.searchParams.get(match);
|
||||||
|
if(value) {
|
||||||
|
htmx.swap(el, el.getAttribute(name), {swapStyle: 'innerHTML'})
|
||||||
|
hasMatch = true;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!hasMatch) {
|
||||||
|
let defaultKey = el.getAttribute("hx-match-qp-default")
|
||||||
|
if(defaultKey) {
|
||||||
|
htmx.swap(el, el.getAttribute("hx-match-qp-mapping:" + defaultKey), {swapStyle: 'innerHTML'})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
23
main.go
23
main.go
|
|
@ -3,9 +3,11 @@ package main
|
||||||
import (
|
import (
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"log"
|
||||||
"mhtml/h"
|
"mhtml/h"
|
||||||
"mhtml/pages"
|
"mhtml/pages"
|
||||||
"mhtml/partials"
|
"mhtml/partials"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
@ -25,8 +27,25 @@ func main() {
|
||||||
return ctx.Next()
|
return ctx.Next()
|
||||||
})
|
})
|
||||||
|
|
||||||
f.Get("/mhtml/partials.*", func(ctx *fiber.Ctx) error {
|
f.Use(func(ctx *fiber.Ctx) error {
|
||||||
return h.PartialView(ctx, partials.GetPartialFromContext(ctx))
|
if ctx.Path() == "/livereload" {
|
||||||
|
return ctx.Next()
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
err := ctx.Next()
|
||||||
|
duration := time.Since(now)
|
||||||
|
ctx.Set("X-Response-Time", duration.String())
|
||||||
|
// Log or print the request method, URL, and duration
|
||||||
|
log.Printf("Request: %s %s took %v", ctx.Method(), ctx.OriginalURL(), duration)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
f.Get("/mhtml/partials*", func(ctx *fiber.Ctx) error {
|
||||||
|
partial := partials.GetPartialFromContext(ctx)
|
||||||
|
if partial == nil {
|
||||||
|
return ctx.SendStatus(404)
|
||||||
|
}
|
||||||
|
return h.PartialView(ctx, partial)
|
||||||
})
|
})
|
||||||
|
|
||||||
pages.RegisterPages(f)
|
pages.RegisterPages(f)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"mhtml/database"
|
"mhtml/database"
|
||||||
"mhtml/h"
|
"mhtml/h"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StoryList() *h.Node {
|
func StoryList() *h.Node {
|
||||||
|
|
@ -13,12 +14,14 @@ func StoryList() *h.Node {
|
||||||
return p
|
return p
|
||||||
})
|
})
|
||||||
|
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
|
||||||
if len(*posts) == 0 {
|
if len(*posts) == 0 {
|
||||||
return h.P("No results found")
|
return h.P("No results found")
|
||||||
}
|
}
|
||||||
|
|
||||||
return h.Fragment(
|
return h.Fragment(
|
||||||
h.VStack(h.List(*posts, func(item Post) *h.Node {
|
h.Div(h.List(*posts, func(item Post) *h.Node {
|
||||||
return StoryCard(item)
|
return StoryCard(item)
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
@ -26,7 +29,7 @@ func StoryList() *h.Node {
|
||||||
|
|
||||||
func StoryCard(post Post) *h.Node {
|
func StoryCard(post Post) *h.Node {
|
||||||
url := fmt.Sprintf("/news/%d", post.Id)
|
url := fmt.Sprintf("/news/%d", post.Id)
|
||||||
return h.VStack(
|
return h.Div(
|
||||||
h.Class("items-center bg-indigo-200 p-4 rounded"),
|
h.Class("items-center bg-indigo-200 p-4 rounded"),
|
||||||
h.A(post.Title, h.Href(url)),
|
h.A(post.Title, h.Href(url)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,22 @@ package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"mhtml/h"
|
"mhtml/h"
|
||||||
|
"mhtml/partials"
|
||||||
|
"mhtml/partials/sheet"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RootPage(children ...*h.Node) *h.Node {
|
func RootPage(children ...*h.Node) *h.Node {
|
||||||
return h.Html(
|
return h.Html(
|
||||||
h.Head(
|
h.Head(
|
||||||
h.Script("https://cdn.tailwindcss.com"),
|
h.Script("https://cdn.tailwindcss.com"),
|
||||||
h.Script("https://unpkg.com/htmx.org@1.9.12"),
|
h.Script("https://unpkg.com/htmx.org@2.0.2"),
|
||||||
h.Script("https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"),
|
|
||||||
h.Script("/js/mhtml.js"),
|
h.Script("/js/mhtml.js"),
|
||||||
),
|
),
|
||||||
h.Body(
|
h.Body(
|
||||||
h.VStack(
|
partials.NavBar(),
|
||||||
h.Class("flex flex-col gap-2 bg-gray-100 h-full"),
|
sheet.Closed(),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex flex-col gap-2 bg-white h-full"),
|
||||||
h.Fragment(children...),
|
h.Fragment(children...),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,7 @@ func RegisterPages(f *fiber.App) {
|
||||||
f.Get("/news", func(ctx *fiber.Ctx) error {
|
f.Get("/news", func(ctx *fiber.Ctx) error {
|
||||||
return h.HtmlView(ctx, ListPage(ctx))
|
return h.HtmlView(ctx, ListPage(ctx))
|
||||||
})
|
})
|
||||||
|
f.Get("/patients", func(ctx *fiber.Ctx) error {
|
||||||
|
return h.HtmlView(ctx, PatientsIndex(ctx))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func IndexPage(c *fiber.Ctx) *h.Page {
|
func IndexPage(c *fiber.Ctx) *h.Page {
|
||||||
return h.NewPage(base.RootPage(
|
return h.NewPage(base.RootPage(h.P("this is cool")))
|
||||||
h.Fragment(
|
|
||||||
h.Div(
|
|
||||||
h.Class("inline-flex flex-col gap-4 p-4"),
|
|
||||||
h.Div(
|
|
||||||
h.Class("max-w-md flex flex-col gap-4"),
|
|
||||||
h.P("Routes"),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,38 +5,25 @@ import (
|
||||||
"mhtml/h"
|
"mhtml/h"
|
||||||
"mhtml/pages/base"
|
"mhtml/pages/base"
|
||||||
"mhtml/partials"
|
"mhtml/partials"
|
||||||
"mhtml/ui"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListPage(ctx *fiber.Ctx) *h.Page {
|
func ListPage(ctx *fiber.Ctx) *h.Page {
|
||||||
return h.NewPage(base.RootPage(
|
return h.NewPage(base.RootPage(
|
||||||
list(),
|
list(ctx),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func list() *h.Node {
|
func list(ctx *fiber.Ctx) *h.Node {
|
||||||
return h.Fragment(
|
return h.Fragment(
|
||||||
partials.SheetClosed(),
|
h.ViewOnLoad(partials.NewsSheet),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("inline-flex flex-col gap-4 p-4"),
|
h.Class("inline-flex flex-col gap-4 p-4"),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.Class("max-w-md flex flex-col gap-4 "),
|
h.Class("max-w-md flex flex-col gap-4 "),
|
||||||
openButton(),
|
partials.OpenSheetButton(h.GetQueryParam(ctx, "open") == "true"),
|
||||||
),
|
),
|
||||||
h.Div(
|
h.Div(
|
||||||
h.View(partials.SheetOpenCount, h.ReloadParams{
|
h.ViewOnLoad(partials.NewsSheetOpenCount),
|
||||||
Triggers: h.CreateTriggers("load", "sheetOpened from:body"),
|
|
||||||
}),
|
|
||||||
h.Text("you opened sheet 0 times")),
|
h.Text("you opened sheet 0 times")),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func openButton() *h.Node {
|
|
||||||
return h.VStack(
|
|
||||||
ui.PrimaryButton(ui.ButtonProps{
|
|
||||||
Text: "Open Sheet",
|
|
||||||
Target: "#sheet-partial",
|
|
||||||
Get: h.GetPartialPathWithQs(partials.Sheet, "open=true"),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
21
pages/patients.index.go
Normal file
21
pages/patients.index.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"mhtml/h"
|
||||||
|
"mhtml/pages/base"
|
||||||
|
"mhtml/partials"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PatientsIndex(ctx *fiber.Ctx) *h.Page {
|
||||||
|
return h.NewPage(base.RootPage(
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex flex-col p-4"),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex justify-between items-center"),
|
||||||
|
h.P("Manage Patients", h.Class("text-lg font-bold")),
|
||||||
|
partials.AddPatientButton()),
|
||||||
|
h.ViewWithTriggers(partials.PatientList, "load", "every 3s"),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
26
partials/button.go
Normal file
26
partials/button.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package partials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mhtml/h"
|
||||||
|
"mhtml/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OpenSheetButton(open bool, children ...*h.Node) *h.Node {
|
||||||
|
if open {
|
||||||
|
return ui.PrimaryButton(ui.ButtonProps{
|
||||||
|
Id: "open-sheet",
|
||||||
|
Text: "Close NewsSheet",
|
||||||
|
Target: "#sheet-partial",
|
||||||
|
Get: h.GetPartialPathWithQs(NewsSheet, "open=false"),
|
||||||
|
Children: children,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return ui.PrimaryButton(ui.ButtonProps{
|
||||||
|
Id: "open-sheet",
|
||||||
|
Text: "Open NewsSheet",
|
||||||
|
Target: "#sheet-partial",
|
||||||
|
Get: h.GetPartialPathWithQs(NewsSheet, "open=true"),
|
||||||
|
Children: children,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,14 +3,24 @@ package partials
|
||||||
|
|
||||||
import "mhtml/h"
|
import "mhtml/h"
|
||||||
import "github.com/gofiber/fiber/v2"
|
import "github.com/gofiber/fiber/v2"
|
||||||
|
import "mhtml/partials/sheet"
|
||||||
|
|
||||||
func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial {
|
func GetPartialFromContext(ctx *fiber.Ctx) *h.Partial {
|
||||||
path := ctx.Path()
|
path := ctx.Path()
|
||||||
if path == "SheetOpenCount" || path == "/mhtml/partials.SheetOpenCount" {
|
if path == "NewsSheet" || path == "/mhtml/partials.NewsSheet" {
|
||||||
return SheetOpenCount(ctx)
|
return NewsSheet(ctx)
|
||||||
}
|
}
|
||||||
if path == "Sheet" || path == "/mhtml/partials.Sheet" {
|
if path == "NewsSheetOpenCount" || path == "/mhtml/partials.NewsSheetOpenCount" {
|
||||||
return Sheet(ctx)
|
return NewsSheetOpenCount(ctx)
|
||||||
|
}
|
||||||
|
if path == "PatientList" || path == "/mhtml/partials.PatientList" {
|
||||||
|
return PatientList(ctx)
|
||||||
|
}
|
||||||
|
if path == "AddPatientForm" || path == "/mhtml/partials.AddPatientForm" {
|
||||||
|
return AddPatientForm(ctx)
|
||||||
|
}
|
||||||
|
if path == "Close" || path == "/mhtml/partials/sheet.Close" {
|
||||||
|
return sheet.Close(ctx)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
22
partials/nav.go
Normal file
22
partials/nav.go
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package partials
|
||||||
|
|
||||||
|
import "mhtml/h"
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NavBar() *h.Node {
|
||||||
|
|
||||||
|
links := []Link{
|
||||||
|
{"Home", "/"},
|
||||||
|
{"News", "/news"},
|
||||||
|
{"Patients", "/patients"},
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Nav(h.Class("flex gap-4 items-center p-4 text-slate-600"), h.Children(h.Map(links, func(link Link) *h.Node {
|
||||||
|
return h.A(link.Name, h.Href(link.Path), h.Class("cursor-pointer hover:text-blue-400"))
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
72
partials/news.go
Normal file
72
partials/news.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package partials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"mhtml/h"
|
||||||
|
"mhtml/news"
|
||||||
|
"mhtml/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewsSheet(ctx *fiber.Ctx) *h.Partial {
|
||||||
|
open := h.GetQueryParam(ctx, "open") == "true"
|
||||||
|
if open {
|
||||||
|
h.SessionIncr(ctx, "sheet-open-count")
|
||||||
|
}
|
||||||
|
return h.NewPartialWithHeaders(
|
||||||
|
&map[string]string{
|
||||||
|
"hx-trigger": "sheetOpened",
|
||||||
|
"hx-push-url": fmt.Sprintf("/news%s", h.Ternary(open, "?open=true", "")),
|
||||||
|
},
|
||||||
|
SheetWrapper(
|
||||||
|
h.IfElseLazy(open, SheetOpen, SheetClosed),
|
||||||
|
h.Swap(ctx, OpenSheetButton(open)),
|
||||||
|
h.Swap(ctx, NewsSheetOpenCount(ctx).Root),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewsSheetOpenCount(ctx *fiber.Ctx) *h.Partial {
|
||||||
|
rnd := h.SessionGet[int64](ctx, "sheet-open-count")
|
||||||
|
if rnd == nil {
|
||||||
|
rnd = new(int64)
|
||||||
|
}
|
||||||
|
|
||||||
|
open := h.GetQueryParam(ctx, "open") == "true"
|
||||||
|
|
||||||
|
return h.NewPartial(h.Div(
|
||||||
|
h.Id("sheet-open-count"),
|
||||||
|
h.IfElse(open,
|
||||||
|
h.Text(fmt.Sprintf("you opened sheet %d times", *rnd)),
|
||||||
|
h.Text("sheet is not open")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SheetWrapper(children ...*h.Node) *h.Node {
|
||||||
|
return h.Div(h.Id("sheet-partial"), h.Fragment(children...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func SheetClosed() *h.Node {
|
||||||
|
return h.Div()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SheetOpen() *h.Node {
|
||||||
|
return h.Fragment(h.Div(
|
||||||
|
h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`),
|
||||||
|
h.Div(
|
||||||
|
h.Class("p-4 overflow-y-auto h-full w-full flex flex-col gap-4"),
|
||||||
|
h.P("My NewsSheet",
|
||||||
|
h.Class("text-lg font-bold"),
|
||||||
|
),
|
||||||
|
h.P("This is a sheet",
|
||||||
|
h.Class("text-sm mt-2"),
|
||||||
|
),
|
||||||
|
ui.Button(ui.ButtonProps{
|
||||||
|
Text: "Close NewsSheet",
|
||||||
|
Target: "#sheet-partial",
|
||||||
|
Get: h.GetPartialPathWithQs(NewsSheet, "open=false"),
|
||||||
|
}),
|
||||||
|
news.StoryList(),
|
||||||
|
)))
|
||||||
|
}
|
||||||
65
partials/patient.go
Normal file
65
partials/patient.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package partials
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"mhtml/database"
|
||||||
|
"mhtml/h"
|
||||||
|
"mhtml/partials/sheet"
|
||||||
|
"mhtml/ui"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Patient struct {
|
||||||
|
Name string
|
||||||
|
ReasonForVisit string
|
||||||
|
AppointmentDate time.Time
|
||||||
|
LocationName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func PatientList(ctx *fiber.Ctx) *h.Partial {
|
||||||
|
patients, err := database.HList[Patient]("patients")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return h.NewPartial(h.Div(
|
||||||
|
h.Class("patient-list"),
|
||||||
|
h.P("Error loading patients"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(patients) == 0 {
|
||||||
|
return h.NewPartial(h.Div(
|
||||||
|
h.Class("patient-list"),
|
||||||
|
h.P("No patients found"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.NewPartial(h.Div(
|
||||||
|
h.Id("patient-list"),
|
||||||
|
h.List(patients, PatientRow),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPatientForm(ctx *fiber.Ctx) *h.Partial {
|
||||||
|
return h.NewPartial(sheet.Opened(h.Div(
|
||||||
|
h.Class("flex flex-col gap-4"),
|
||||||
|
h.P("Add Patient", h.Class("text-lg font-bold")),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func PatientRow(patient *Patient) *h.Node {
|
||||||
|
return h.Div(
|
||||||
|
h.Class("flex flex-col gap-2"),
|
||||||
|
h.Pf("Name: %s", patient.Name),
|
||||||
|
h.Pf("Reason for visit: %s", patient.ReasonForVisit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddPatientButton() *h.Node {
|
||||||
|
return ui.Button(ui.ButtonProps{
|
||||||
|
Id: "add-patient",
|
||||||
|
Text: "Add Patient",
|
||||||
|
Class: "bg-blue-700 text-white rounded p-2 h-12",
|
||||||
|
Target: "#active-modal",
|
||||||
|
Get: h.GetPartialPath(AddPatientForm),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
package partials
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"mhtml/h"
|
|
||||||
"mhtml/news"
|
|
||||||
"mhtml/ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SheetOpenCount(ctx *fiber.Ctx) *h.Partial {
|
|
||||||
rnd := h.SessionGet[int64](ctx, "sheet-open-count")
|
|
||||||
if rnd == nil {
|
|
||||||
rnd = new(int64)
|
|
||||||
}
|
|
||||||
return h.NewPartial(h.Div(
|
|
||||||
h.Text(fmt.Sprintf("you opened sheet %d times", *rnd)),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func SheetClosed() *h.Node {
|
|
||||||
return h.Div(h.Id("sheet-partial"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Sheet(ctx *fiber.Ctx) *h.Partial {
|
|
||||||
open := ctx.Query("open")
|
|
||||||
if open == "true" {
|
|
||||||
h.SessionIncr(ctx, "sheet-open-count")
|
|
||||||
}
|
|
||||||
return h.NewPartialWithHeaders(
|
|
||||||
&map[string]string{
|
|
||||||
"hx-trigger": "sheetOpened",
|
|
||||||
},
|
|
||||||
h.IfElse(open == "true", SheetOpen(), SheetClosed()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SheetOpen() *h.Node {
|
|
||||||
return h.Div(
|
|
||||||
h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`),
|
|
||||||
h.Div(
|
|
||||||
h.Class("p-4 overflow-y-auto h-full w-full flex flex-col gap-4"),
|
|
||||||
h.P("My Sheet",
|
|
||||||
h.Class("text-lg font-bold"),
|
|
||||||
),
|
|
||||||
h.P("This is a sheet",
|
|
||||||
h.Class("text-sm mt-2"),
|
|
||||||
),
|
|
||||||
ui.Button(ui.ButtonProps{
|
|
||||||
Text: "Close Sheet",
|
|
||||||
Target: "#sheet-partial",
|
|
||||||
Get: h.GetPartialPathWithQs(Sheet, "open=false"),
|
|
||||||
}),
|
|
||||||
news.StoryList(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
36
partials/sheet/sheet.go
Normal file
36
partials/sheet/sheet.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package sheet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"mhtml/h"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Opened(children ...*h.Node) *h.Node {
|
||||||
|
return h.Fragment(h.Div(
|
||||||
|
h.Class(`fixed top-0 right-0 h-full w-96 bg-gray-100 shadow-lg z-50`),
|
||||||
|
CloseButton(),
|
||||||
|
h.Div(
|
||||||
|
children...,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Closed() *h.Node {
|
||||||
|
return h.Div(h.Id("active-modal"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Close(ctx *fiber.Ctx) *h.Partial {
|
||||||
|
return h.NewPartial(
|
||||||
|
h.Swap(ctx, Closed()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CloseButton() *h.Node {
|
||||||
|
return h.Div(
|
||||||
|
h.Class("absolute top-0 right-0 p-3"),
|
||||||
|
h.Button(
|
||||||
|
h.Class("text-gray-500"),
|
||||||
|
h.GetPartial(Close),
|
||||||
|
h.Text("X"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,45 @@ type Page struct {
|
||||||
Import string
|
Import string
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPublicFuncsReturningHPartial(dir string) ([]string, error) {
|
type Partial struct {
|
||||||
var functions []string
|
FuncName string
|
||||||
|
Package string
|
||||||
|
Import string
|
||||||
|
}
|
||||||
|
|
||||||
|
func sliceCommonPrefix(dir1, dir2 string) string {
|
||||||
|
// Use filepath.Clean to normalize the paths
|
||||||
|
dir1 = filepath.Clean(dir1)
|
||||||
|
dir2 = filepath.Clean(dir2)
|
||||||
|
|
||||||
|
// Find the common prefix
|
||||||
|
commonPrefix := dir1
|
||||||
|
if len(dir1) > len(dir2) {
|
||||||
|
commonPrefix = dir2
|
||||||
|
}
|
||||||
|
|
||||||
|
for !strings.HasPrefix(dir1, commonPrefix) {
|
||||||
|
commonPrefix = filepath.Dir(commonPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice off the common prefix
|
||||||
|
slicedDir1 := strings.TrimPrefix(dir1, commonPrefix)
|
||||||
|
slicedDir2 := strings.TrimPrefix(dir2, commonPrefix)
|
||||||
|
|
||||||
|
// Remove leading slashes
|
||||||
|
slicedDir1 = strings.TrimPrefix(slicedDir1, string(filepath.Separator))
|
||||||
|
slicedDir2 = strings.TrimPrefix(slicedDir2, string(filepath.Separator))
|
||||||
|
|
||||||
|
// Return the longer one
|
||||||
|
if len(slicedDir1) > len(slicedDir2) {
|
||||||
|
return slicedDir1
|
||||||
|
}
|
||||||
|
return slicedDir2
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPublicFuncsReturningHPartial(dir string) ([]Partial, error) {
|
||||||
|
var partials []Partial
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
// Walk through the directory to find all Go files.
|
// Walk through the directory to find all Go files.
|
||||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
|
@ -42,7 +79,7 @@ func findPublicFuncsReturningHPartial(dir string) ([]string, error) {
|
||||||
ast.Inspect(node, func(n ast.Node) bool {
|
ast.Inspect(node, func(n ast.Node) bool {
|
||||||
// Check if the node is a function declaration.
|
// Check if the node is a function declaration.
|
||||||
if funcDecl, ok := n.(*ast.FuncDecl); ok {
|
if funcDecl, ok := n.(*ast.FuncDecl); ok {
|
||||||
// Only consider exported (public) functions.
|
// Only consider exported (public) partials.
|
||||||
if funcDecl.Name.IsExported() {
|
if funcDecl.Name.IsExported() {
|
||||||
// Check the return type.
|
// Check the return type.
|
||||||
if funcDecl.Type.Results != nil {
|
if funcDecl.Type.Results != nil {
|
||||||
|
|
@ -53,7 +90,11 @@ func findPublicFuncsReturningHPartial(dir string) ([]string, error) {
|
||||||
// Check if the package name is 'h' and type is 'Partial'.
|
// Check if the package name is 'h' and type is 'Partial'.
|
||||||
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
if ident, ok := selectorExpr.X.(*ast.Ident); ok && ident.Name == "h" {
|
||||||
if selectorExpr.Sel.Name == "Partial" {
|
if selectorExpr.Sel.Name == "Partial" {
|
||||||
functions = append(functions, funcDecl.Name.Name)
|
partials = append(partials, Partial{
|
||||||
|
Package: node.Name.Name,
|
||||||
|
Import: sliceCommonPrefix(cwd, filepath.Dir(path)),
|
||||||
|
FuncName: funcDecl.Name.Name,
|
||||||
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +114,7 @@ func findPublicFuncsReturningHPartial(dir string) ([]string, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return functions, nil
|
return partials, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
||||||
|
|
@ -114,7 +155,7 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
||||||
if selectorExpr.Sel.Name == "Page" {
|
if selectorExpr.Sel.Name == "Page" {
|
||||||
pages = append(pages, Page{
|
pages = append(pages, Page{
|
||||||
Package: node.Name.Name,
|
Package: node.Name.Name,
|
||||||
Import: fmt.Sprintf("mhtml/%s", filepath.Dir(path)),
|
Import: filepath.Dir(path),
|
||||||
Path: path,
|
Path: path,
|
||||||
FuncName: funcDecl.Name.Name,
|
FuncName: funcDecl.Name.Name,
|
||||||
})
|
})
|
||||||
|
|
@ -140,22 +181,30 @@ func findPublicFuncsReturningHPage(dir string) ([]Page, error) {
|
||||||
return pages, nil
|
return pages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildGetPartialFromContext(builder *CodeBuilder, funcs []string) {
|
func buildGetPartialFromContext(builder *CodeBuilder, partials []Partial) {
|
||||||
fName := "GetPartialFromContext"
|
fName := "GetPartialFromContext"
|
||||||
|
|
||||||
body := `
|
body := `
|
||||||
path := ctx.Path()
|
path := ctx.Path()
|
||||||
`
|
`
|
||||||
|
|
||||||
for _, f := range funcs {
|
for _, f := range partials {
|
||||||
if f == fName {
|
if f.FuncName == fName {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
caller := fmt.Sprintf("%s.%s", f.Package, f.FuncName)
|
||||||
|
path := fmt.Sprintf("/mhtml/%s.%s", f.Import, f.FuncName)
|
||||||
|
|
||||||
|
if f.Package == "partials" {
|
||||||
|
caller = f.FuncName
|
||||||
|
path = fmt.Sprintf("/mhtml/partials.%s", f.FuncName)
|
||||||
|
}
|
||||||
|
|
||||||
body += fmt.Sprintf(`
|
body += fmt.Sprintf(`
|
||||||
if path == "%s" || path == "/mhtml/partials.%s" {
|
if path == "%s" || path == "%s" {
|
||||||
return %s(ctx)
|
return %s(ctx)
|
||||||
}
|
}
|
||||||
`, f, f, f)
|
`, f.FuncName, path, caller)
|
||||||
}
|
}
|
||||||
|
|
||||||
body += "return nil"
|
body += "return nil"
|
||||||
|
|
@ -177,7 +226,7 @@ func buildGetPartialFromContext(builder *CodeBuilder, funcs []string) {
|
||||||
func writePartialsFile() {
|
func writePartialsFile() {
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
partialPath := filepath.Join(cwd, "partials")
|
partialPath := filepath.Join(cwd, "partials")
|
||||||
funcs, err := findPublicFuncsReturningHPartial(partialPath)
|
partials, err := findPublicFuncsReturningHPartial(partialPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
|
|
@ -189,7 +238,13 @@ func writePartialsFile() {
|
||||||
builder.AddImport("mhtml/h")
|
builder.AddImport("mhtml/h")
|
||||||
builder.AddImport("github.com/gofiber/fiber/v2")
|
builder.AddImport("github.com/gofiber/fiber/v2")
|
||||||
|
|
||||||
buildGetPartialFromContext(builder, funcs)
|
for _, partial := range partials {
|
||||||
|
if partial.Import != "partials" {
|
||||||
|
builder.AddImport(fmt.Sprintf(`mhtml/%s`, partial.Import))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildGetPartialFromContext(builder, partials)
|
||||||
|
|
||||||
WriteFile(filepath.Join("partials", "generated.go"), func(content *ast.File) string {
|
WriteFile(filepath.Join("partials", "generated.go"), func(content *ast.File) string {
|
||||||
return builder.String()
|
return builder.String()
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ func WriteFile(path string, cb func(content *ast.File) string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes = []byte(cb(f))
|
bytes = []byte(cb(f))
|
||||||
formatEnabled := true
|
formatEnabled := false
|
||||||
|
|
||||||
if formatEnabled {
|
if formatEnabled {
|
||||||
bytes, err = format.Source(bytes)
|
bytes, err = format.Source(bytes)
|
||||||
|
|
|
||||||
19
ui/button.go
19
ui/button.go
|
|
@ -5,11 +5,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type ButtonProps struct {
|
type ButtonProps struct {
|
||||||
|
Id string
|
||||||
Text string
|
Text string
|
||||||
Target string
|
Target string
|
||||||
|
Trigger string
|
||||||
Get string
|
Get string
|
||||||
Class string
|
Class string
|
||||||
Children *h.Node
|
Children []*h.Node
|
||||||
}
|
}
|
||||||
|
|
||||||
func PrimaryButton(props ButtonProps) *h.Node {
|
func PrimaryButton(props ButtonProps) *h.Node {
|
||||||
|
|
@ -24,22 +26,15 @@ func SecondaryButton(props ButtonProps) *h.Node {
|
||||||
|
|
||||||
func Button(props ButtonProps) *h.Node {
|
func Button(props ButtonProps) *h.Node {
|
||||||
|
|
||||||
text := h.P(props.Text)
|
text := h.Text(props.Text)
|
||||||
|
|
||||||
button := h.Button(
|
button := h.Button(
|
||||||
h.If(props.Children != nil, props.Children),
|
h.If(props.Id != "", h.Id(props.Id)),
|
||||||
|
h.If(props.Children != nil, h.Children(props.Children)),
|
||||||
|
h.If(props.Trigger != "", h.Trigger(props.Trigger)),
|
||||||
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class),
|
h.Class("flex gap-1 items-center border p-4 rounded cursor-hover", props.Class),
|
||||||
h.If(props.Get != "", h.Get(props.Get)),
|
h.If(props.Get != "", h.Get(props.Get)),
|
||||||
h.If(props.Target != "", h.Target(props.Target)),
|
h.If(props.Target != "", h.Target(props.Target)),
|
||||||
//h.BeforeRequestSetHtml(
|
|
||||||
// h.Div(
|
|
||||||
// h.Class("flex gap-1"),
|
|
||||||
// h.Text("Loading..."),
|
|
||||||
// ),
|
|
||||||
//),
|
|
||||||
//h.AfterRequestSetHtml(h.Text(props.Text)),
|
|
||||||
// Note, i really like this idea of being able to reference elements just by the instance,
|
|
||||||
//and automatically adding id
|
|
||||||
text,
|
text,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue