entry page
This commit is contained in:
parent
ae983473b3
commit
787ccb4fc1
11 changed files with 313 additions and 120 deletions
|
|
@ -293,6 +293,17 @@ func formatRoute(path string) string {
|
|||
path = strings.ReplaceAll(path, "_", "/")
|
||||
path = strings.ReplaceAll(path, ".", "/")
|
||||
path = strings.ReplaceAll(path, "\\", "/")
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, ":") {
|
||||
parts[i] = fmt.Sprintf("{%s}", part[1:])
|
||||
}
|
||||
}
|
||||
|
||||
path = strings.Join(parts, "/")
|
||||
|
||||
if path == "" {
|
||||
return "/"
|
||||
}
|
||||
|
|
|
|||
25
examples/chat/chat/service.go
Normal file
25
examples/chat/chat/service.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package chat
|
||||
|
||||
import (
|
||||
"chat/internal/db"
|
||||
"context"
|
||||
"github.com/maddalax/htmgo/framework/service"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
queries *db.Queries
|
||||
}
|
||||
|
||||
func NewService(locator *service.Locator) *Service {
|
||||
return &Service{
|
||||
queries: service.Get[db.Queries](locator),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetRoom(id string) (*db.ChatRoom, error) {
|
||||
room, err := s.queries.GetChatRoom(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &room, nil
|
||||
}
|
||||
41
examples/chat/components/button.go
Normal file
41
examples/chat/components/button.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package components
|
||||
|
||||
import "github.com/maddalax/htmgo/framework/h"
|
||||
|
||||
type ButtonProps struct {
|
||||
Id string
|
||||
Text string
|
||||
Target string
|
||||
Type string
|
||||
Trigger string
|
||||
Get string
|
||||
Class string
|
||||
Children []h.Ren
|
||||
}
|
||||
|
||||
func PrimaryButton(props ButtonProps) h.Ren {
|
||||
props.Class = h.MergeClasses(props.Class, "border-slate-800 bg-slate-900 hover:bg-slate-800 text-white")
|
||||
return Button(props)
|
||||
}
|
||||
|
||||
func SecondaryButton(props ButtonProps) h.Ren {
|
||||
props.Class = h.MergeClasses(props.Class, "border-gray-700 bg-gray-700 text-white")
|
||||
return Button(props)
|
||||
}
|
||||
|
||||
func Button(props ButtonProps) h.Ren {
|
||||
|
||||
text := h.Text(props.Text)
|
||||
|
||||
button := h.Button(
|
||||
h.If(props.Id != "", h.Id(props.Id)),
|
||||
h.If(props.Children != nil, h.Children(props.Children...)),
|
||||
h.Class("flex gap-1 items-center justify-center border p-4 rounded cursor-hover", props.Class),
|
||||
h.If(props.Get != "", h.Get(props.Get)),
|
||||
h.If(props.Target != "", h.HxTarget(props.Target)),
|
||||
h.IfElse(props.Type != "", h.Type(props.Type), h.Type("button")),
|
||||
text,
|
||||
)
|
||||
|
||||
return button
|
||||
}
|
||||
11
examples/chat/components/error.go
Normal file
11
examples/chat/components/error.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package components
|
||||
|
||||
import "github.com/maddalax/htmgo/framework/h"
|
||||
|
||||
func FormError(error string) *h.Element {
|
||||
return h.Div(
|
||||
h.Id("form-error"),
|
||||
h.Text(error),
|
||||
h.If(error != "", h.Class("p-4 bg-rose-400 text-white rounded")),
|
||||
)
|
||||
}
|
||||
54
examples/chat/components/input.go
Normal file
54
examples/chat/components/input.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package components
|
||||
|
||||
import (
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"github.com/maddalax/htmgo/framework/hx"
|
||||
)
|
||||
|
||||
type InputProps struct {
|
||||
Id string
|
||||
Label string
|
||||
Name string
|
||||
Type string
|
||||
DefaultValue string
|
||||
Placeholder string
|
||||
Required bool
|
||||
ValidationPath string
|
||||
Error string
|
||||
Children []h.Ren
|
||||
}
|
||||
|
||||
func Input(props InputProps) *h.Element {
|
||||
validation := h.If(props.ValidationPath != "", h.Children(
|
||||
h.Post(props.ValidationPath, hx.BlurEvent),
|
||||
h.Attribute("hx-swap", "innerHTML transition:true"),
|
||||
h.Attribute("hx-target", "next div"),
|
||||
))
|
||||
|
||||
if props.Type == "" {
|
||||
props.Type = "text"
|
||||
}
|
||||
|
||||
input := h.Input(
|
||||
props.Type,
|
||||
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
|
||||
h.If(props.Name != "", h.Name(props.Name)),
|
||||
h.If(props.Children != nil, h.Children(props.Children...)),
|
||||
h.If(props.Required, h.Required()),
|
||||
h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)),
|
||||
validation,
|
||||
)
|
||||
|
||||
wrapped := h.Div(
|
||||
h.If(props.Id != "", h.Id(props.Id)),
|
||||
h.Class("flex flex-col gap-1"),
|
||||
h.If(props.Label != "", h.Label(h.Text(props.Label))),
|
||||
input,
|
||||
h.Div(
|
||||
h.Id(props.Id+"-error"),
|
||||
h.Class("text-red-500"),
|
||||
),
|
||||
)
|
||||
|
||||
return wrapped
|
||||
}
|
||||
91
examples/chat/pages/chat.$id.go
Normal file
91
examples/chat/pages/chat.$id.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"github.com/maddalax/htmgo/framework/hx"
|
||||
"github.com/maddalax/htmgo/framework/js"
|
||||
)
|
||||
|
||||
func ChatRoom(ctx *h.RequestContext) *h.Page {
|
||||
return h.NewPage(
|
||||
RootPage(
|
||||
h.Div(
|
||||
h.JoinExtensions(
|
||||
h.TriggerChildren(),
|
||||
h.HxExtension("ws"),
|
||||
),
|
||||
h.Attribute("ws-connect", "/chat"),
|
||||
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
|
||||
Form(ctx),
|
||||
h.Div(
|
||||
h.Div(
|
||||
h.Id("messages"),
|
||||
h.Class("flex flex-col gap-2 w-full"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func MessageInput() *h.Element {
|
||||
return h.Input("text",
|
||||
h.Id("message-input"),
|
||||
h.Required(),
|
||||
h.Class("p-4 rounded-md border border-slate-200"),
|
||||
h.Name("message"),
|
||||
h.Placeholder("Message"),
|
||||
h.HxBeforeWsSend(
|
||||
js.SetValue(""),
|
||||
),
|
||||
h.OnEvent(hx.KeyDownEvent, js.SubmitFormOnEnter()),
|
||||
)
|
||||
}
|
||||
|
||||
func Form(ctx *h.RequestContext) *h.Element {
|
||||
return h.Div(
|
||||
h.Class("flex flex-col items-center justify-center p-4 gap-6"),
|
||||
h.H2F("Form submission with ws example", h.Class("text-2xl font-bold")),
|
||||
h.Form(
|
||||
h.Attribute("ws-send", ""),
|
||||
h.Class("flex flex-col gap-2"),
|
||||
h.LabelFor("name", "Your Message"),
|
||||
MessageInput(),
|
||||
SubmitButton(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func SubmitButton() *h.Element {
|
||||
buttonClasses := "rounded items-center px-3 py-2 bg-slate-800 text-white w-full text-center"
|
||||
return h.Div(
|
||||
h.HxBeforeRequest(
|
||||
js.RemoveClassOnChildren(".loading", "hidden"),
|
||||
js.SetClassOnChildren(".submit", "hidden"),
|
||||
),
|
||||
h.HxAfterRequest(
|
||||
js.SetClassOnChildren(".loading", "hidden"),
|
||||
js.RemoveClassOnChildren(".submit", "hidden"),
|
||||
),
|
||||
h.Class("flex gap-2 justify-center"),
|
||||
h.Button(
|
||||
h.Class("loading hidden relative text-center", buttonClasses),
|
||||
Spinner(),
|
||||
h.Disabled(),
|
||||
h.Text("Submitting..."),
|
||||
),
|
||||
h.Button(
|
||||
h.Type("submit"),
|
||||
h.Class("submit", buttonClasses),
|
||||
h.Text("Submit"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Spinner(children ...h.Ren) *h.Element {
|
||||
return h.Div(
|
||||
h.Children(children...),
|
||||
h.Class("absolute left-1 spinner spinner-border animate-spin inline-block w-6 h-6 border-4 rounded-full border-slate-200 border-t-transparent"),
|
||||
h.Attribute("role", "status"),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,91 +1,63 @@
|
|||
package pages
|
||||
|
||||
import (
|
||||
"chat/components"
|
||||
"chat/partials"
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"github.com/maddalax/htmgo/framework/hx"
|
||||
"github.com/maddalax/htmgo/framework/js"
|
||||
)
|
||||
|
||||
func IndexPage(ctx *h.RequestContext) *h.Page {
|
||||
func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
|
||||
return h.NewPage(
|
||||
RootPage(
|
||||
h.Div(
|
||||
h.JoinExtensions(
|
||||
h.TriggerChildren(),
|
||||
h.HxExtension("ws"),
|
||||
),
|
||||
h.Attribute("ws-connect", "/chat"),
|
||||
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
|
||||
Form(ctx),
|
||||
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
|
||||
h.Div(
|
||||
h.Div(
|
||||
h.Id("messages"),
|
||||
h.Class("flex flex-col gap-2 w-full"),
|
||||
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
|
||||
h.H2F("htmgo chat", h.Class("text-3xl font-bold text-center mb-6")),
|
||||
h.Form(
|
||||
h.Attribute("hx-swap", "none"),
|
||||
h.PostPartial(partials.CreateOrJoinRoom),
|
||||
h.Class("flex flex-col gap-3"),
|
||||
|
||||
components.Input(components.InputProps{
|
||||
Id: "username",
|
||||
Name: "username",
|
||||
Label: "Username",
|
||||
Required: true,
|
||||
}),
|
||||
|
||||
h.Div(
|
||||
h.Class("mt-6 flex flex-col gap-3"),
|
||||
|
||||
components.Input(components.InputProps{
|
||||
Name: "new-chat-room",
|
||||
Label: "Create a New Chat Room",
|
||||
Placeholder: "Chat Room Name",
|
||||
}),
|
||||
|
||||
h.Div(
|
||||
h.Class("flex items-center justify-center gap-4"),
|
||||
h.Div(h.Class("border-t border-gray-300 flex-grow")),
|
||||
h.P(h.Text("OR"), h.Class("text-gray-500")),
|
||||
h.Div(h.Class("border-t border-gray-300 flex-grow")),
|
||||
),
|
||||
|
||||
components.Input(components.InputProps{
|
||||
Id: "join-chat-room",
|
||||
Name: "join-chat-room",
|
||||
Label: "Join a Chat Room",
|
||||
Placeholder: "Chat Room Id",
|
||||
}),
|
||||
),
|
||||
|
||||
components.FormError(""),
|
||||
components.PrimaryButton(components.ButtonProps{
|
||||
Type: "submit",
|
||||
Text: "Submit",
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func MessageInput() *h.Element {
|
||||
return h.Input("text",
|
||||
h.Id("message-input"),
|
||||
h.Required(),
|
||||
h.Class("p-4 rounded-md border border-slate-200"),
|
||||
h.Name("message"),
|
||||
h.Placeholder("Message"),
|
||||
h.HxBeforeWsSend(
|
||||
js.SetValue(""),
|
||||
),
|
||||
h.OnEvent(hx.KeyDownEvent, js.SubmitFormOnEnter()),
|
||||
)
|
||||
}
|
||||
|
||||
func Form(ctx *h.RequestContext) *h.Element {
|
||||
return h.Div(
|
||||
h.Class("flex flex-col items-center justify-center p-4 gap-6"),
|
||||
h.H2F("Form submission with ws example", h.Class("text-2xl font-bold")),
|
||||
h.Form(
|
||||
h.Attribute("ws-send", ""),
|
||||
h.Class("flex flex-col gap-2"),
|
||||
h.LabelFor("name", "Your Message"),
|
||||
MessageInput(),
|
||||
SubmitButton(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func SubmitButton() *h.Element {
|
||||
buttonClasses := "rounded items-center px-3 py-2 bg-slate-800 text-white w-full text-center"
|
||||
return h.Div(
|
||||
h.HxBeforeRequest(
|
||||
js.RemoveClassOnChildren(".loading", "hidden"),
|
||||
js.SetClassOnChildren(".submit", "hidden"),
|
||||
),
|
||||
h.HxAfterRequest(
|
||||
js.SetClassOnChildren(".loading", "hidden"),
|
||||
js.RemoveClassOnChildren(".submit", "hidden"),
|
||||
),
|
||||
h.Class("flex gap-2 justify-center"),
|
||||
h.Button(
|
||||
h.Class("loading hidden relative text-center", buttonClasses),
|
||||
Spinner(),
|
||||
h.Disabled(),
|
||||
h.Text("Submitting..."),
|
||||
),
|
||||
h.Button(
|
||||
h.Type("submit"),
|
||||
h.Class("submit", buttonClasses),
|
||||
h.Text("Submit"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Spinner(children ...h.Ren) *h.Element {
|
||||
return h.Div(
|
||||
h.Children(children...),
|
||||
h.Class("absolute left-1 spinner spinner-border animate-spin inline-block w-6 h-6 border-4 rounded-full border-slate-200 border-t-transparent"),
|
||||
h.Attribute("role", "status"),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,30 @@
|
|||
package partials
|
||||
|
||||
import (
|
||||
"chat/chat"
|
||||
"chat/components"
|
||||
"github.com/maddalax/htmgo/framework/h"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CounterPartial(ctx *h.RequestContext) *h.Partial {
|
||||
count, err := strconv.ParseInt(ctx.FormValue("count"), 10, 64)
|
||||
func CreateOrJoinRoom(ctx *h.RequestContext) *h.Partial {
|
||||
locator := ctx.ServiceLocator()
|
||||
service := chat.NewService(locator)
|
||||
|
||||
if err != nil {
|
||||
count = 0
|
||||
chatRoomId := ctx.FormValue("join-chat-room")
|
||||
|
||||
if chatRoomId != "" {
|
||||
room, _ := service.GetRoom(chatRoomId)
|
||||
if room == nil {
|
||||
return h.SwapPartial(ctx, components.FormError("Room not found"))
|
||||
} else {
|
||||
return h.RedirectPartial("/chat/" + chatRoomId)
|
||||
}
|
||||
}
|
||||
|
||||
count++
|
||||
chatRoomName := ctx.FormValue("chat-room-name")
|
||||
if chatRoomName != "" {
|
||||
// create room
|
||||
}
|
||||
|
||||
return h.SwapManyPartial(
|
||||
ctx,
|
||||
CounterForm(int(count)),
|
||||
h.ElementIf(count > 10, SubmitButton("New record!")),
|
||||
)
|
||||
}
|
||||
|
||||
func CounterForm(count int) *h.Element {
|
||||
return h.Form(
|
||||
h.Class("flex flex-col gap-3 items-center"),
|
||||
h.Id("counter-form"),
|
||||
h.PostPartial(CounterPartial),
|
||||
h.Input("text",
|
||||
h.Class("hidden"),
|
||||
h.Value(count),
|
||||
h.Name("count"),
|
||||
),
|
||||
h.P(
|
||||
h.AttributePairs(
|
||||
"id", "counter",
|
||||
"class", "text-xl",
|
||||
"name", "count",
|
||||
"text", "count",
|
||||
),
|
||||
h.TextF("Count: %d", count),
|
||||
),
|
||||
SubmitButton("Increment"),
|
||||
)
|
||||
}
|
||||
|
||||
func SubmitButton(text string) *h.Element {
|
||||
return h.Button(
|
||||
h.Class("bg-rose-400 hover:bg-rose-500 text-white font-bold py-2 px-4 rounded"),
|
||||
h.Id("swap-text"),
|
||||
h.Type("submit"),
|
||||
h.Text(text),
|
||||
)
|
||||
return h.SwapPartial(ctx, components.FormError("Create a new room or join an existing one"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ type InputProps struct {
|
|||
Name string
|
||||
Type string
|
||||
DefaultValue string
|
||||
Placeholder string
|
||||
Required bool
|
||||
ValidationPath string
|
||||
Children []h.Ren
|
||||
}
|
||||
|
|
@ -22,19 +24,24 @@ func Input(props InputProps) h.Ren {
|
|||
h.Attribute("hx-target", "next div"),
|
||||
))
|
||||
|
||||
if props.Type == "" {
|
||||
props.Type = "text"
|
||||
}
|
||||
|
||||
input := h.Input(
|
||||
props.Type,
|
||||
h.Class("border p-2 rounded"),
|
||||
h.If(props.Id != "", h.Id(props.Id)),
|
||||
h.If(props.Name != "", h.Name(props.Name)),
|
||||
h.If(props.Children != nil, h.Children(props.Children...)),
|
||||
h.If(props.Required, h.Required()),
|
||||
h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)),
|
||||
validation,
|
||||
)
|
||||
|
||||
wrapped := h.Div(
|
||||
h.Class("flex flex-col gap-1"),
|
||||
h.If(props.Label != "", h.Label(props.Label)),
|
||||
h.If(props.Label != "", h.Label(h.Text(props.Label))),
|
||||
input,
|
||||
h.Div(h.Class("text-red-500")),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package h
|
||||
|
||||
import (
|
||||
"github.com/maddalax/htmgo/framework/hx"
|
||||
"html"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
|
@ -56,6 +57,10 @@ func SwapPartial(ctx *RequestContext, swap *Element) *Partial {
|
|||
SwapMany(ctx, swap))
|
||||
}
|
||||
|
||||
func RedirectPartial(url string) *Partial {
|
||||
return NewPartialWithHeaders(NewHeaders(hx.RedirectHeader, url), Fragment())
|
||||
}
|
||||
|
||||
func SwapManyPartial(ctx *RequestContext, swaps ...*Element) *Partial {
|
||||
return NewPartial(
|
||||
SwapMany(ctx, swaps...),
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ func SwapMany(ctx *RequestContext, elements ...*Element) *Element {
|
|||
for _, element := range elements {
|
||||
element.AppendChild(outOfBandSwap(""))
|
||||
}
|
||||
return Template(Map(elements, func(arg *Element) Ren {
|
||||
return Fragment(Map(elements, func(arg *Element) Ren {
|
||||
return arg
|
||||
})...)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue