It is better that way because it works without JavaScript; if HTMx is
not available, it will just use regulars forms.
The problem is that most of the submit buttons where using formaction
to send the request to a different action, and only one button was the
“real” action. Since i could not pass the formaction to
invoice-product-form template, i have changed the “default” action to
the one with “ancillary” functions.
I have to use a different action to remove for each product because i
can not pass the index to the backend without JavaScript: it only
depends on the button click, that already has a name for the action.
Thus, in a way, i have “merged” the action and the index in a single
name.
With this button, it is no longer necessary to set the quantity to zero
to remove, at least not with JavaScript. This is why i am using Alpine:
to use x-cloak and hide it from non-JavaScript users.
Although, i wonder if it would not be better to use HTMx for that?
I tried this already when i started adding filters, but i tried to use
AlpineJS for that, and could not because it would reset the context each
time i submitted the filters, due to HTMx replacing the whole content.
I realized that the only thing i need is some “flag” to show and hide
the form with CSS. I do not even need AlpineJS for that, but i used it
anyway because then i can use the x-cloak thing to hidde the toggle
button for users with JavaScript disabled.
Similarly, the body by default has that “flag” set in the markup, and is
removed when AlpineJS is initialized, thus if JavaScript is disabled the
filters form is shown nevertheless.
I had to change MethodOverrider to check whether the form is encoded as
multipart/form-data or i would not be able to get the method field from
forms with files.
For now i add the file manually, i.e., outside add_expense and
edit_expense PL/pgSQL functions, because it was faster for me, but i
will probably add an attach_to_expense function, or something like that,
to avoid having the whole ON CONFLICT logic inside Golang—this belongs
to the database.
I actually find more comfortable to select the product from the list
presented up until now, but this is mostly because i have very few
products and the list is not too long, so the idea is that with
JavaScript we will dynamically add an empty product row to the invoice
and then use the name field to search the product by name.
I have the feeling that i am doing something wrong because i ended up
with a lot of HTMx attribute for what i feel is not that much work,
but for now it will work.
I have added the `Is` field to `InputField` in order to include the `id`
attribute to the HTML element, because the HTMLAttributes are attached
to the `input`, not the `div`, and i felt like this one should also be
a custom element based on div, like all the others.
These is not yet any keyboard control to select the search results.
I am not happy with having the search of products in a different URL
than the index, specially since they use the exact same SQL query and
ProductFilter struct, but i did not know how else ask for a different
representation without resorting to the more complicated MIME types.
I was using the exact same form from edit and new pages of invoice,
which is not too bad considering it won’t change very often, but i now
want to be able to add new empty product lines with the add product
button, and i will need to have a template for that form, which would
mean a third copy.
We have reconsidered the toggle thing and instead moved the selection
into a little menu on top of the input, like the input’s label does à
la Material Design.
I just moved the checkboxes into a new details, that works as a menu,
but i had to add the type="search" to the existing input in the tags
field, or the CSS would style the checkboxes as well.
I do not do anything when the checkbox selection changes because that
already triggers a POST to the server that returns the new HTML with
the checkbox changed, and the JavaScript only has to retrieve that new
structure, exactly as it does in the initial rendering.
Since we want to add a little description to the options, i no longer
can use the same SelectOption in ToggleField, even though i could have
reused the Group element, but that felt wrong.
I realized that using a select for just two, short, options is overkill:
the select and its options use a lot more real state than the two
radios, which can have tooltips (not yet, though).
Since i am going to replace this field with a custom element that has
a toggle-like aspect, i already added the is="numerus-toggle" attribute
and use it for stying the non-JavaScript field.
With Oriol we agreed that contacts should have tags, too, and that the
“tag pool”, as it were, should be shared with the one for invoices (and
all future tags we might add).
I added the contact_tag relation and tag_contact function, just like
with invoices, and then realized that the SQL queries that Go had to
execute were becoming “complex” enough: i had to get not only the slug,
but the contact id to call tag_contact, and all inside a transaction.
Therefore, i opted to create the add_contact and edit_contact functions,
that mirror those for invoice and products, so now each “major” section
has these functions. They also simplified a bit the handling of the
VATIN and phone numbers, because it is now encapsuled inside the
PL/pgSQL function and Go does not know how to assemble the parts.
This is more or less the same as a multiselect, except that now it
adds a list of string element that you write into the search element.
It is supposed to fetch a list of tag suggestions from the server, but i
have not implemented it yet.
What i really set off on was to refactor the multiselect’s x-data
context to a separate JavaScript file.
I did not see the need at first, thinking that it would not matter
because it was used only in a template and i was not duplicating the
code in my files. However, i then realized that having the context
in the template means the visitor has to download it each and every time
it accesses a form with a multiselect, even if nothing changed, and,
worse, it would download it multiple times if there were many
multiselect controls.
It makes more sense to put all that into a file that the browser would
only download and parse once, if the proper caching is set.
Once i realized that, it was a shame that AlpineJS has no way to do
the same for the HTML structure[0], for the exact same reasons: not
wanting to download many times the same extra <template> and other
markup required to build the control for JavaScript users. And then i
remembered that this is supposed to be custom element’s main selling
point.
At first i tried to create a shadow DOW to replace the <select> with
the same <div> and <ul> that i used with Alpine, but it turns out that
<select> is not one of the allowed elements that can have a shadow root
attached[0].
Therefore, i changed the custom element to extend the <div> for the
<select> and <label> instead—the same element that had the x-init
context—, but i would have to define or include all the styles inside
the shadow DOM, and bring the lang attribute, for it to look like it
did before. Out with the shadow DOM, and modify the <div>’s contents
instead.
At this point the code was so far removed from the declarative way that
AlpineJS promotes that i did not see much value on using it, except for
its reactivity. But, given that this is such a small component, at the
end decided to write it all in plain JavaScript.
It is more code, at least looking only at the code i had to write, but
i love how i only have to add an is="numerus-multiselect" attribute to
HTML for it to work.
[0]: https://github.com/alpinejs/alpine/discussions/1205
[1]: https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow
Had to replace the tags <ul> with a div with an input, so that the
browser can focus the keywoard there. For now i do not have a
focus-within CSS rule because we do no yet have a style for focus
highlight.
I have replaced the template for-loop to fill the options with the
JavaScript equivalent for two reasons. The first is that GoLand is very
stupid and can not handle that templating code inside the JavaScript
function and complains of non-existing problemes all the time.
The second is that, taking advantage of the input, i now have filtering
of options and have to remove accents from the label and convert it to
lowercase into a separate property just for that. I could do that with
a Go function, but it is something that i also have to do for the
input’s value when it changes, therefore i am forced to use JavaScript
and, if i am already using it for one string, it makes no sense to have
duplicate functionality in Go code.
The control still has missing aria attributes, and the list of options
is not yet navigable with the keyboard.
I had in the product edit page only because it was easier to test there
while i was developing it, but it is something that should be done for
all select[multiple], of course.
I removed the whole x-cloak thing because i am not sure what would
happen if i do something wrong and Alpine can not initialize the
multiselect; probably show nothing to the user. Now it shows the
native select a fraction of a second, but if i fuck it up at least the
user can still use the app.
We will only allow to select a tax from each of the tax classes, but
the user needs to know what class each tax belongs to, and grouping
the taxes by class in the select helps with that.
It seems that we do not agree en whether the IRPF tax should be
something of the product or the contact, so we decided to make the
product have multiple taxes, just in case, and if only one is needed,
then users can just select one; no need to limit to one.
This is not yet necessary, but the empty label is because i do not want
to select a default tax for products—at least, not without a setting for
it.
Since i need to add the required attribute now to select, because
otherwise the browser would allow sending that empty value, i did not
want to do it unconditionally, just in case.
I implemented the Valuer and Scanner interfaces to InputField and
SelectField for better passing values between the database and Go. I
had a conflict with the Value name and renamed the struct member to Val.
I also had to change the attributes array to be of type
template.HTMLAttr or html/template would replace `form="newtax"`
attribute to `zgotmplz="newtax"` because it deems it “unsafe”. I do
not like having to use template.HTMLAttr when assigning values, but
i do not know what else i can do now.
Similar to the profile form, the login form now parses and validates
itself, with the InputField structs that the templates expect.
I realized that i was doing more work than necessary when parsing fields
fro the profile form because i was repeating the operation and the field
name, so now it is a function of InputField.
This time i needed extra attributes for the login form. I am not sure
that the Go source code needs to know about HTML attributes, but it was
the easiest way to pass them to the template.
Let’s start first with a non-fancy validation method with just if
conditionals instead of bringing yet another complicated library. I
hope i do not regret it.
I wanted to move all the input field to a template because all that
gobbledygook with the .input div and repeating the label in the
placeholder was starting to annoy me. Now with error messages was even
more concerning.
I did not know whether the label should be a part of the input fields
or something that the template should do. At the end i decided that
it makes more sense to be part of the input field because in the error
messages i use that same label, thus the template does not have a say
in that, and, besides, it was just easier to write the template.
The same with the error messages: i’ve seen frameworks that have a map
with the field’s id/name to the error slice, but then it would be
a bit harder to write the template.
I added AddError functions instead of just using append inside the
validator function, and have a local variable for whether it all went
OK, because i was worried that i would leave out the `ok = false`
in some conditions.
I had started writing “constructors” functions for InputField and
SelectField, but then had to add other methods to change the required
field and who knows what else, and in the end it was easier to just
construct the field inline.