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 return
} }
if ok := form.Validate(); !ok { if ok := form.Validate(); !ok {
w.WriteHeader(http.StatusUnprocessableEntity) if !IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
mustRenderProfileForm(w, r, form) mustRenderProfileForm(w, r, form)
return return
} }
@ -131,10 +133,15 @@ func HandleProfileForm(w http.ResponseWriter, r *http.Request, _ httprouter.Para
if form.Password.Val != "" { if form.Password.Val != "" {
conn.MustExec(r.Context(), "select change_password($1)", form.Password) conn.MustExec(r.Context(), "select change_password($1)", form.Password)
} }
company := mustGetCompany(r) if IsHTMxRequest(r) {
http.Redirect(w, r, companyURI(company, "/profile"), http.StatusSeeOther) 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) { 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) 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) 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{}) { func mustRenderWebTemplate(w io.Writer, r *http.Request, filename string, data interface{}) {
mustRenderTemplate(w, r, "web.gohtml", filename, data) 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 { body {
background-color: var(--numerus--background-color);
color: var(--numerus--text-color);
font-size: 1.6rem; font-size: 1.6rem;
line-height: 1.5; line-height: 1.5;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
body, dialog {
background-color: var(--numerus--background-color);
color: var(--numerus--text-color);
}
img, picture, video, canvas, svg { img, picture, video, canvas, svg {
display: block; display: block;
max-width: 100%; max-width: 100%;
@ -679,6 +682,20 @@ main > nav {
background-color: var(--numerus--color--light-gray); 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 */ /* Remix Icon */
@font-face { @font-face {

View File

@ -400,3 +400,29 @@ class Tags extends HTMLDivElement {
customElements.define('numerus-multiselect', Multiselect, {extends: 'div'}); customElements.define('numerus-multiselect', Multiselect, {extends: 'div'});
customElements.define('numerus-tags', Tags, {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"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ template "title" . }} — Numerus</title> <title>{{ template "title" . }} — Numerus</title>
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css"> <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> <script type="module" src="/static/numerus.js"></script>
</head> </head>
<body> <body>
@ -14,9 +15,9 @@
<summary> <summary>
<i class="ri-eye-close-line ri-3x"></i> <i class="ri-eye-close-line ri-3x"></i>
</summary> </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"> <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> <i class="ri-account-circle-line"></i>
{{( pgettext "Account" "menu" )}} {{( pgettext "Account" "menu" )}}
</a> </a>
@ -48,6 +49,7 @@
</ul> </ul>
</nav> </nav>
<main> <main>
{{- template "breadcrumbs" . }}
{{- template "content" . }} {{- template "content" . }}
</main> </main>
</body> </body>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
{{ .Number | printf ( pgettext "Invoice %s" "title" )}} {{ .Number | printf ( pgettext "Invoice %s" "title" )}}
{{- end }} {{- end }}
{{ define "content" }} {{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoice*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoice*/ -}}
<nav> <nav>
<p> <p>
@ -19,7 +19,10 @@
download="{{ .Number}}.pdf">{{( pgettext "Download invoice" "action" )}}</a> download="{{ .Number}}.pdf">{{( pgettext "Download invoice" "action" )}}</a>
</p> </p>
</nav> </nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.invoice*/ -}}
<link rel="stylesheet" type="text/css" href="/static/invoice.css"> <link rel="stylesheet" type="text/css" href="/static/invoice.css">
<article class="invoice"> <article class="invoice">
<header> <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 }} {{printf (pgettext "Edit Product “%s”" "title") .Name }}
{{- end }} {{- end }}
{{ define "content" }} {{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<nav> <nav>
<p> <p>
@ -11,6 +11,10 @@
<a>{{ .Name }}</a> <a>{{ .Name }}</a>
</p> </p>
</nav> </nav>
{{- end }}
{{ define "content" }}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.productForm*/ -}}
<section class="dialog-content"> <section class="dialog-content">
<h2>{{printf (pgettext "Edit Product “%s”" "title") .Name }}</h2> <h2>{{printf (pgettext "Edit Product “%s”" "title") .Name }}</h2>
<form method="POST"> <form method="POST">

View File

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

View File

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

View File

@ -2,7 +2,7 @@
{{( pgettext "User Settings" "title" )}} {{( pgettext "User Settings" "title" )}}
{{- end }} {{- end }}
{{ define "content" }} {{ define "breadcrumbs" -}}
{{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.profileForm*/ -}} {{- /*gotype: dev.tandem.ws/tandem/numerus/pkg.profileForm*/ -}}
<nav> <nav>
<p> <p>
@ -10,9 +10,13 @@
<a>{{( pgettext "User Settings" "title" )}}</a> <a>{{( pgettext "User Settings" "title" )}}</a>
</p> </p>
</nav> </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> <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 }} {{ csrfToken }}
<fieldset class="full-width"> <fieldset class="full-width">
<legend>{{( pgettext "User Access Data" "title" )}}</legend> <legend>{{( pgettext "User Access Data" "title" )}}</legend>

View File

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