fix: no proper loading feedback on forms

This commit is contained in:
juancwu 2026-02-14 23:12:43 +00:00
commit d224f5a10a
27 changed files with 192 additions and 57 deletions

View file

@ -0,0 +1,22 @@
package button
import "git.juancwu.dev/juancwu/budgit/internal/ui/components/icon"
templ Submit(props ...Props) {
{{ var p Props }}
if len(props) > 0 {
{{ p = props[0] }}
}
if p.Type == "" {
{{ p.Type = TypeSubmit }}
}
{{ p.Class = p.Class + " htmx-submit-btn" }}
@Button(p) {
<span class="btn-label contents">
{ children... }
</span>
<span class="btn-spinner">
@icon.LoaderCircle(icon.Props{Class: "animate-spin"})
</span>
}
}

View file

@ -165,7 +165,7 @@ templ AddExpenseForm(props AddExpenseFormProps) {
// Shopping list items selector
@ItemSelectorSection(props.ListsWithItems, false)
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>
@ -267,7 +267,7 @@ templ EditExpenseForm(spaceID string, exp *model.ExpenseWithTagsAndMethod, metho
// Payment Method
@paymentmethod.MethodSelector(methods, exp.PaymentMethodID)
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>

View file

@ -184,7 +184,7 @@ templ CreateAccountForm(spaceID string, dialogID string) {
})
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Create
}
</div>
@ -212,7 +212,7 @@ templ EditAccountForm(spaceID string, acct *model.MoneyAccount, dialogID string)
})
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>
@ -253,7 +253,7 @@ templ TransferForm(spaceID string, accountID string, direction model.TransferDir
})
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
if direction == model.TransferDirectionDeposit {
Deposit
} else {

View file

@ -154,7 +154,7 @@ templ CreateMethodForm(spaceID string, dialogID string) {
})
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Create
}
</div>
@ -231,7 +231,7 @@ templ EditMethodForm(spaceID string, method *model.PaymentMethod, dialogID strin
})
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>

View file

@ -268,7 +268,7 @@ templ AddRecurringForm(spaceID string, tags []*model.Tag, methods []*model.Payme
// Payment Method
@paymentmethod.MethodSelector(methods, nil)
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>
@ -401,7 +401,7 @@ templ EditRecurringForm(spaceID string, re *model.RecurringExpenseWithTagsAndMet
// Payment Method
@paymentmethod.MethodSelector(methods, re.PaymentMethodID)
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>

View file

@ -35,8 +35,7 @@ templ ListCard(spaceID string, list *model.ShoppingList, items []*model.ListItem
"autocomplete": "off",
},
})
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
Size: button.SizeSm,
}) {
@icon.Plus(icon.Props{Size: 16})

View file

@ -56,6 +56,8 @@ templ Base(props ...SEOProps) {
@smoothScrollScript()
// HTMX CSRF configuration
@htmxCSRFScript()
// Form submit spinner for non-HTMX forms
@formSubmitScript()
// Google Analytics
if cfg := ctxkeys.Config(ctx); cfg != nil && cfg.GoogleMeasuringID != "" {
@googleAnalyticsScript(cfg.GoogleMeasuringID)
@ -118,6 +120,10 @@ templ htmxCSRFScript() {
<script src="/assets/js/htmx-csrf.js"></script>
}
templ formSubmitScript() {
<script src="/assets/js/form-submit.js"></script>
}
templ googleAnalyticsScript(id string) {
<script async src={ "https://www.googletagmanager.com/gtag/js?id=" + id }></script>
<script type="text/javascript">

View file

@ -79,7 +79,7 @@ templ Dashboard(spaces []*model.Space) {
Cancel
}
}
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Create
}
}

View file

@ -89,9 +89,7 @@ templ AppSettings(hasPassword bool, errorMsg string) {
}
}
}
@button.Button(button.Props{
Type: button.TypeSubmit,
}) {
@button.Submit() {
if hasPassword {
Change Password
} else {

View file

@ -261,7 +261,7 @@ templ AddBudgetForm(spaceID string, tags []*model.Tag) {
})
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>
@ -374,7 +374,7 @@ templ EditBudgetForm(spaceID string, b *model.BudgetWithSpent, tags []*model.Tag
}
</div>
<div class="flex justify-end">
@button.Button(button.Props{Type: button.TypeSubmit}) {
@button.Submit() {
Save
}
</div>

View file

@ -63,9 +63,7 @@ templ SpaceListDetailPage(space *model.Space, list *model.ShoppingList, items []
"autocomplete": "off",
},
})
@button.Button(button.Props{
Type: button.TypeSubmit,
}) {
@button.Submit() {
Add Item
}
</form>

View file

@ -27,9 +27,7 @@ templ SpaceListsPage(space *model.Space, cards []model.ListCardData) {
Name: "name",
Placeholder: "New list name...",
})
@button.Button(button.Props{
Type: button.TypeSubmit,
}) {
@button.Submit() {
Create
}
</form>

View file

@ -46,9 +46,7 @@ templ SpaceSettingsPage(space *model.Space, members []*model.SpaceMemberWithProf
"required": true,
},
})
@button.Button(button.Props{
Type: button.TypeSubmit,
}) {
@button.Submit() {
Save
}
</form>
@ -110,9 +108,7 @@ templ SpaceSettingsPage(space *model.Space, members []*model.SpaceMemberWithProf
"required": true,
},
})
@button.Button(button.Props{
Type: button.TypeSubmit,
}) {
@button.Submit() {
@icon.UserPlus(icon.Props{Class: "size-4"})
Invite
}

View file

@ -30,9 +30,7 @@ templ SpaceTagsPage(space *model.Space, tags []*model.Tag) {
"autocomplete": "off",
},
})
@button.Button(button.Props{
Type: button.TypeSubmit,
}) {
@button.Submit() {
Create
}
</form>

View file

@ -57,8 +57,7 @@ templ Auth(errorMsg string) {
}
}
}
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
FullWidth: true,
}) {
Continue with Email

View file

@ -39,10 +39,9 @@ templ MagicLinkSent(email string) {
>
@csrf.Token()
<input type="hidden" name="email" value={ email }/>
@button.Button(button.Props{
@button.Submit(button.Props{
Variant: button.VariantOutline,
FullWidth: true,
Type: button.TypeSubmit,
}) {
Resend magic link
}

View file

@ -76,8 +76,7 @@ templ AuthPassword(errorMsg string) {
</a>
</div>
}
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
FullWidth: true,
}) {
Sign In

View file

@ -66,8 +66,7 @@ templ OnboardingWelcome() {
<form action="/auth/logout" method="POST" class="text-center mt-6">
@csrf.Token()
<span class="text-sm text-muted-foreground">Not you? </span>
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
Variant: button.VariantLink,
Class: "p-0 h-auto text-sm",
}) {
@ -138,8 +137,7 @@ templ OnboardingName(errorMsg string) {
@icon.ArrowLeft()
Back
}
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
Class: "grow",
}) {
Continue
@ -150,8 +148,7 @@ templ OnboardingName(errorMsg string) {
<form action="/auth/logout" method="POST" class="text-center mt-6">
@csrf.Token()
<span class="text-sm text-muted-foreground">Not you? </span>
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
Variant: button.VariantLink,
Class: "p-0 h-auto text-sm",
}) {
@ -222,8 +219,7 @@ templ OnboardingSpace(name string, errorMsg string) {
@icon.ArrowLeft()
Back
}
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
Class: "grow",
}) {
Create Space
@ -233,8 +229,7 @@ templ OnboardingSpace(name string, errorMsg string) {
<form action="/auth/logout" method="POST" class="text-center mt-6">
@csrf.Token()
<span class="text-sm text-muted-foreground">Not you? </span>
@button.Button(button.Props{
Type: button.TypeSubmit,
@button.Submit(button.Props{
Variant: button.VariantLink,
Class: "p-0 h-auto text-sm",
}) {

View file

@ -24,6 +24,10 @@ func RenderFragment(w http.ResponseWriter, r *http.Request, c templ.Component, f
}
}
func RenderToast(w http.ResponseWriter, r *http.Request, c templ.Component) {
RenderOOB(w, r, c, "beforeend:#toast-container")
}
func RenderOOB(w http.ResponseWriter, r *http.Request, c templ.Component, target string) {
// Write OOB wrapper start
_, err := fmt.Fprintf(w, `<div hx-swap-oob="%s">`, target)