-
-
Notifications
You must be signed in to change notification settings - Fork 221
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HTMX CSQL Fixture UI #35372
HTMX CSQL Fixture UI #35372
Conversation
Looks like a great approach, very smooth and simple. |
<form hx-post="{{ request.path_info }}" | ||
hq-hx-action="save_expression" | ||
hx-swap="outerHTML" | ||
x-data="{ isUnsaved: false }" | ||
@change="isUnsaved = true;" | ||
@submit="isUnsaved = false;"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<form hx-post="{{ request.path_info }}" | |
hq-hx-action="save_expression" | |
hx-swap="outerHTML" | |
x-data="{ isUnsaved: false }" | |
@change="isUnsaved = true;" | |
@submit="isUnsaved = false;"> | |
<form | |
hx-post="{{ request.path_info }}" | |
hq-hx-action="save_expression" | |
hx-swap="outerHTML" | |
x-data="{ isUnsaved: false }" | |
@change="isUnsaved = true;" | |
@submit="isUnsaved = false;" | |
> |
{{ form.csql }} | ||
</div> | ||
<div class="col-2"> | ||
<button type="submit" class="btn btn-outline-primary" x-bind:disabled="!isUnsaved"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<button type="submit" class="btn btn-outline-primary" x-bind:disabled="!isUnsaved"> | |
<button | |
class="btn btn-outline-primary" type="submit" | |
:disabled="!isUnsaved" | |
> |
- I like to always have
class
be the first attribute - if there is a
HTMX
oralpine
attribute (:bind
in this case), we want it on a second line - I prefer shorthand
alpine
bindings, but could be convinced otherwise?
<button class="btn btn-outline-danger" | ||
hx-post="{{ request.path_info }}" | ||
hq-hx-action="delete_expression" | ||
hx-swap="delete" | ||
hx-target="closest form"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<button class="btn btn-outline-danger" | |
hx-post="{{ request.path_info }}" | |
hq-hx-action="delete_expression" | |
hx-swap="delete" | |
hx-target="closest form"> | |
<button | |
class="btn btn-outline-danger" | |
hx-post="{{ request.path_info }}" | |
hq-hx-action="delete_expression" | |
hx-swap="delete" | |
hx-target="closest form" | |
> |
<button type="button" | ||
class="btn btn-default" | ||
hx-post="{{ request.path_info }}" | ||
hq-hx-action="new_expression" | ||
hx-target="#csql-fixture-rows" | ||
hx-swap="beforeend"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<button type="button" | |
class="btn btn-default" | |
hx-post="{{ request.path_info }}" | |
hq-hx-action="new_expression" | |
hx-target="#csql-fixture-rows" | |
hx-swap="beforeend"> | |
<button | |
class="btn btn-default" type="button" | |
hx-post="{{ request.path_info }}" | |
hq-hx-action="new_expression" | |
hx-target="#csql-fixture-rows" | |
hx-swap="beforeend" | |
> |
{% for form in csql_fixture_forms %} | ||
{{ form }} | ||
{% endfor %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this was exactly the approach i was thinking when reviewing the page with @AddisonDunn. Less front end logic being front-loaded from the backend into Alpine, and more on the python side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was super pleased to see how well this plays with Django templates! I'm still not totally sold on the second commit here though - I do think I liked it better before adding Django forms, but if I find the time I might try a compromise - like maybe I make a partial template with the <form>
tags and the buttons, but have the fields themselves rendered by the Django form (at present, I set a custom template on the form, so it contains everything, which sounds nice in principle, but means I lose error handling in the template)
3951d98
to
6151a3e
Compare
Apologies to the reviewers for neglecting this. Should be ready to go now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's too bad you had to delete all my beautiful code, but I understand.
corehq/apps/case_search/templates/case_search/csql_expression_form.html
Outdated
Show resolved
Hide resolved
{% endblock %} | ||
|
||
{% block modals %} | ||
{% include "hqwebapp/htmx/error_modal.html" %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this triggered?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HTMX framework pops it up on an error response automatically
self.domain = domain | ||
self.fields['name'].widget.attrs = { | ||
'placeholder': "name", | ||
'class': "form-control", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to enforce a character limit here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, will do, unless Django did that for me already. To be investigated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just pushed a fix. Forgot that that was already in there in the default widget attrs, but I decided to override them completely instead of using .update()
because I didn't like the defaults for the textarea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one's not a textarea though right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or you mean you decided to override both because you didn't like the update method for one of them, the one that is a textarea
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah that's right, the latter. Felt weird to keep the defaults for one and not the other, so I figured being explicit about both was best. I'm open to reconsidering
name=instance.name, | ||
csql=instance.csql | ||
) | ||
if not instance.deleted: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of this? When would an expression be saved after being soft-deleted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was causing an error during tests - I believe this happens during the soft delete, ie set the delete flag, then call save, then that triggers this handler
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh that's right, it was creating an extra log entry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see
if form.is_valid(): | ||
form.save() | ||
return HttpResponse(form.render()) | ||
raise AssertionError("The user shouldn't be able to submit an invalid form") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't seem like there are really many restrictions, right? Name or CSQL field could be empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's right - this form doesn't have much validation. I just added this because if you did throw an error, it wouldn't be displayed well in the UI. I wanted to come back and fix that, but ultimately gave up, particularly since it doesn't actually apply here.
Product Description
Technical Summary
Some changes to the CSQL fixture UI to make it use HTMX in a more idiomatic way. I did this a couple months ago and then kinda dropped it. I have a documentation ticket for the feature this sprint, so I'm taking the opportunity to wrap this up too.
https://dimagi.atlassian.net/browse/USH-4962
Feature Flag
CSQL_FIXTURE
Safety Assurance
FF used only by one project
Safety story
Automated test coverage
QA Plan
Rollback instructions
Labels & Review