Show the profile form in a dialog using HTMx

Had to split the actual page content and the breadcrumbs because they
do not belong in a dialog.  However, i had to change all templates to
do that.
This commit is contained in:
jordi fita mas 2023-03-20 13:09:52 +01:00
parent 82eb8a2733
commit 9e757cb9f4
22 changed files with 150 additions and 28 deletions

View File

@ -121,7 +121,9 @@ func HandleProfileForm(w http.ResponseWriter, r *http.Request, _ httprouter.Para
return
}
if ok := form.Validate(); !ok {
w.WriteHeader(http.StatusUnprocessableEntity)
if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderProfileForm(w, r, form)
return
}
@ -131,10 +133,15 @@ func HandleProfileForm(w http.ResponseWriter, r *http.Request, _ httprouter.Para
if form.Password.Val != "" {
conn.MustExec(r.Context(), "select change_password($1)", form.Password)
}
company := mustGetCompany(r)
http.Redirect(w, r, companyURI(company, "/profile"), http.StatusSeeOther)
if IsHTMxRequest(r) {
w.Header().Set("HX-Trigger", "closeModal")
w.WriteHeader(http.StatusNoContent)
} else {
company := mustGetCompany(r)
http.Redirect(w, r, companyURI(company, "/profile"), http.StatusSeeOther)
}
}
func mustRenderProfileForm(w http.ResponseWriter, r *http.Request, form *profileForm) {
mustRenderAppTemplate(w, r, "profile.gohtml", form)
mustRenderModalTemplate(w, r, "profile.gohtml", form)
}

View File

@ -87,3 +87,7 @@ func MethodOverrider(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}
func IsHTMxRequest(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}

View File

@ -99,6 +99,14 @@ func mustRenderAppTemplate(w io.Writer, r *http.Request, filename string, data i
mustRenderTemplate(w, r, "app.gohtml", filename, data)
}
func mustRenderModalTemplate(w io.Writer, r *http.Request, filename string, data interface{}) {
layout := "app.gohtml"
if IsHTMxRequest(r) {
layout = "modal.gohtml"
}
mustRenderTemplate(w, r, layout, filename, data)
}
func mustRenderWebTemplate(w io.Writer, r *http.Request, filename string, data interface{}) {
mustRenderTemplate(w, r, "web.gohtml", filename, data)
}

1
web/static/htmx@1.8.6.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -170,13 +170,16 @@ html {
}
body {
background-color: var(--numerus--background-color);
color: var(--numerus--text-color);
font-size: 1.6rem;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
body, dialog {
background-color: var(--numerus--background-color);
color: var(--numerus--text-color);
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
@ -679,6 +682,20 @@ main > nav {
background-color: var(--numerus--color--light-gray);
}
/* Modal */
dialog {
margin: auto;
}
.modal .close-dialog {
min-width: initial;
border: 0;
position: absolute;
top: .5rem;
right: .5rem;
cursor: pointer;
}
/* Remix Icon */
@font-face {

View File

@ -400,3 +400,29 @@ class Tags extends HTMLDivElement {
customElements.define('numerus-multiselect', Multiselect, {extends: 'div'});
customElements.define('numerus-tags', Tags, {extends: 'div'});
htmx.onLoad((target) => {
if (target.tagName === 'DIALOG') {
const details = document.querySelectorAll('details[open]');
for (const detail of details) {
detail.removeAttribute('open');
}
target.showModal();
const button = target.querySelector('.close-dialog');
if (button) {
button.addEventListener('click', () => {
htmx.trigger(target, 'closeModal');
});
}
}
})
htmx.on('closeModal', () => {
const openDialog = document.querySelector('dialog[open]');
if (!openDialog) {
return;
}
openDialog.close();
openDialog.remove();
});

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ template "title" . }} — Numerus</title>
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
<script src="/static/htmx@1.8.6.min.js"></script>
<script type="module" src="/static/numerus.js"></script>
</head>
<body>
@ -14,9 +15,9 @@
<summary>
<i class="ri-eye-close-line ri-3x"></i>
</summary>
<ul role="menu" class="action-menu">
<ul role="menu" class="action-menu" data-hx-push-url="false" data-hx-swap="beforeend">
<li role="presentation">
<a role="menuitem" href="{{ companyURI "/profile" }}">
<a role="menuitem" href="{{ companyURI "/profile" }}" data-hx-boost="true">
<i class="ri-account-circle-line"></i>
{{( pgettext "Account" "menu" )}}
</a>
@ -48,6 +49,7 @@
</ul>
</nav>
<main>
{{- template "breadcrumbs" . }}
{{- template "content" . }}
</main>
</body>

View File

@ -2,7 +2,7 @@
{{printf (pgettext "Edit Contact “%s”" "title") .BusinessName.Val }}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.contactForm*/ -}}
<nav>
<p>
@ -11,6 +11,10 @@
<a>{{ .BusinessName.Val }}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.contactForm*/ -}}
<section class="dialog-content">
<h2>{{printf (pgettext "Edit Contact “%s”" "title") .BusinessName.Val }}</h2>
<form method="POST">

View File

@ -2,7 +2,8 @@
{{( pgettext "Contacts" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.ContactsIndexPage*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
@ -13,7 +14,9 @@
href="{{ companyURI "/contacts/new" }}">{{( pgettext "New contact" "action" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.ContactsIndexPage*/ -}}
<table>
<thead>

View File

@ -2,7 +2,7 @@
{{( pgettext "New Contact" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.contactForm*/ -}}
<nav>
<p>
@ -11,6 +11,10 @@
<a>{{( pgettext "New Contact" "title" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.contactForm*/ -}}
<section class="dialog-content">
<h2>{{(pgettext "New Contact" "title")}}</h2>
<form method="POST" action="{{ companyURI "/contacts" }}">

View File

@ -2,5 +2,8 @@
{{( pgettext "Dashboard" "title" )}}
{{- end }}
{{ define "breadcrumbs" -}}
{{- end }}
{{ define "content" }}
{{- end }}

View File

@ -2,7 +2,7 @@
{{ printf ( pgettext "Edit Invoice “%s”" "title" ) .Number }}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}}
<nav>
<p>
@ -11,6 +11,10 @@
<a>{{ .Number }}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.editInvoicePage*/ -}}
<section class="dialog-content">
<h2>{{ printf (pgettext "Edit Invoice “%s”" "title") .Number }}</h2>
<form method="POST" action="{{ companyURI "/invoices/" }}{{ .Slug }}">

View File

@ -2,7 +2,8 @@
{{( pgettext "Invoices" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
@ -20,7 +21,9 @@
</p>
</form>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.InvoicesIndexPage*/ -}}
<table class="no-padding">
<thead>

View File

@ -2,7 +2,7 @@
{{( pgettext "New Invoice" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoicePage*/ -}}
<nav>
<p>
@ -11,6 +11,10 @@
<a>{{( pgettext "New Invoice" "title" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoicePage*/ -}}
<section class="dialog-content">
<h2>{{(pgettext "New Invoice" "title")}}</h2>
<form method="POST" action="{{ companyURI "/invoices" }}">

View File

@ -2,7 +2,7 @@
{{( pgettext "Add Products to Invoice" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoiceProductsPage*/ -}}
<nav>
<p>
@ -15,6 +15,10 @@
{{ end }}
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.newInvoiceProductsPage*/ -}}
<section class="dialog-content">
<h2>{{(pgettext "Add Products to Invoice" "title")}}</h2>
<form method="POST" action="{{ .Action }}">

View File

@ -2,7 +2,7 @@
{{ .Number | printf ( pgettext "Invoice %s" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoice*/ -}}
<nav>
<p>
@ -19,7 +19,10 @@
download="{{ .Number}}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoice*/ -}}
<link rel="stylesheet" type="text/css" href="/static/invoice.css">
<article class="invoice">
<header>

View File

@ -0,0 +1,4 @@
<dialog class="modal" data-hx-push-url="false" data-hx-swap="outerHTML">
<button class="close-dialog" type="button" title="{{( pgettext "Close dialog" "action" )}}"><i class="ri-close-line ri-2x"></i></button>
{{- template "content" . }}
</dialog>

View File

@ -2,7 +2,7 @@
{{printf (pgettext "Edit Product “%s”" "title") .Name }}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<nav>
<p>
@ -11,6 +11,10 @@
<a>{{ .Name }}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<section class="dialog-content">
<h2>{{printf (pgettext "Edit Product “%s”" "title") .Name }}</h2>
<form method="POST">

View File

@ -2,7 +2,8 @@
{{( pgettext "Products" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productsIndexPage*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
@ -13,7 +14,9 @@
href="{{ companyURI "/products/new" }}">{{( pgettext "New product" "action" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productsIndexPage*/ -}}
<table>
<thead>

View File

@ -2,7 +2,7 @@
{{( pgettext "New Product" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<nav>
<p>
@ -11,6 +11,10 @@
<a>{{( pgettext "New Product" "title" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<section class="dialog-content">
<h2>{{(pgettext "New Product" "title")}}</h2>
<form method="POST" action="{{ companyURI "/products" }}">

View File

@ -2,7 +2,7 @@
{{( pgettext "User Settings" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.profileForm*/ -}}
<nav>
<p>
@ -10,9 +10,13 @@
<a>{{( pgettext "User Settings" "title" )}}</a>
</p>
</nav>
<section class="dialog-content">
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.profileForm*/ -}}
<section class="dialog-content" id="profile-dialog-content" data-hx-target="this">
<h2>{{(pgettext "User Settings" "title")}}</h2>
<form method="POST">
<form method="POST" action="{{ companyURI "/profile" }}" data-hx-boost="true" data-hx-select="#profile-dialog-content">
{{ csrfToken }}
<fieldset class="full-width">
<legend>{{( pgettext "User Access Data" "title" )}}</legend>

View File

@ -2,16 +2,20 @@
{{( pgettext "Tax Details" "title" )}}
{{- end }}
{{ define "content" }}
{{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.TaxDetailsPage*/ -}}
<nav>
<p>
<a href="{{ companyURI "/" }}">{{( pgettext "Home" "title" )}}</a> /
<a>{{( pgettext "Tax Details" "title" )}}</a>
</p>
</nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.TaxDetailsPage*/ -}}
<section class="dialog-content">
<h2>{{(pgettext "Tax Details" "title")}}</h2>
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.TaxDetailsPage*/ -}}
{{ with .DetailsForm }}
<form id="details" method="POST">
{{ csrfToken }}
@ -32,10 +36,10 @@
{{ template "select-field" .Currency }}
</fieldset>
<fieldset>
<legend>{{( pgettext "Invoicing" "title" )}}</legend>
{{ template "input-field" .InvoiceNumberFormat }}
{{ template "input-field" .LegalDisclaimer }}
</fieldset>
@ -129,7 +133,8 @@
<form method="POST" action="{{ companyURI "/payment-method"}}/{{ .Id }}">
{{ csrfToken }}
{{ deleteMethod }}
<button class="icon" aria-label="{{( gettext "Delete payment method" )}}" type="submit"><i
<button class="icon" aria-label="{{( gettext "Delete payment method" )}}"
type="submit"><i
class="ri-delete-back-2-line"></i></button>
</form>
</td>
@ -154,7 +159,8 @@
<tr>
<td colspan="2"></td>
<td colspan="2">
<button form="new-payment-method" type="submit">{{( pgettext "Add new payment method" "action" )}}</button>
<button form="new-payment-method"
type="submit">{{( pgettext "Add new payment method" "action" )}}</button>
</td>
</tr>
</tfoot>