Hi,
I’m trying to use HTMX to render a form inside a modal. I have the initial form rendering, but for form validation im not sure how i get the view to pass the erros of fields back to the view within the modal?
def add_wallet(request):
if request.method == 'POST':
form = WalletForm(request.POST)
if form.is_valid():
wallet = form.save(commit=False) # Don't save yet
wallet.user = request.user # Assign logged-in user
wallet.save() # Now save with user assigned
return redirect('portal') # Redirect to portal
else:
return render(request, 'portal/partials/wallet_form.html', {'form': form})
else:
form = WalletForm()
return render(request, 'portal/partials/wallet_form.html', {'form': form})
When the form is invalid on submit it is rendering the portal/partials/wallet_form.htm rather than reloading the modal with the content and error messages.
I hope this makes sense?
I think we’d need to see the original template that contains the hx-post attribute to understand what’s happening in that template along with the response.
It would also be helpful to see the wallet_form.html file to understand what the html is that is being rendered to understand how the response is going to be processed by htmx.
Hi, Ken.
Here is my form
<div class="modal-content">
<h3 class="mt-5 text-center">Add Wallet Address</h3>
<p class="text-center text-light-emphasis">Wallet Addresses are used for <span class="text-primary">alerts</span> and <span class="text-primary">notifications</span></p>
<div class="modal-body">
<form method="POST" action="{% url 'add_wallet' %}" class="p-4">
{% csrf_token %}
<!-- Wallet Name -->
<div class="mb-3">
<label class="form-label" for="{{ form.name.id_for_label }}">Wallet Name</label>
<input type="text" name="name" class="form-control {% if form.name.errors %}is-invalid{% endif %}"
value="{{ form.name.value|default_if_none:'' }}" placeholder="Enter a wallet name">
{% for error in form.name.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<div class="row">
<!-- Blockchain -->
<div class="col-md-4 mb-3 mt-3">
<label class="form-label" for="{{ form.blockchain.id_for_label }}">Blockchain</label>
<select name="blockchain" class="form-select {% if form.blockchain.errors %}is-invalid{% endif %}">
<option value="">Select Chain</option>
{% for blockchain in form.blockchain.field.queryset %}
<option value="{{ blockchain.id }}" {% if blockchain.id == form.blockchain.value %}selected{% endif %}>{{ blockchain.name }}</option>
{% endfor %}
</select>
{% for error in form.blockchain.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<!-- Wallet Address -->
<div class="col-md-8 mb-3 mt-3">
<label class="form-label" for="{{ form.address.id_for_label }}">Contract Address</label>
<input type="text" name="address" class="form-control {% if form.address.errors %}is-invalid{% endif %}"
value="{{ form.address.value|default_if_none:'' }}" placeholder="0x00000000000000000007">
{% for error in form.address.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
</div>
<!-- Notes -->
<div class="mb-3 mt-3">
<label class="form-label" for="{{ form.notes.id_for_label }}">Notes</label>
<textarea name="notes" class="form-control {% if form.notes.errors %}is-invalid{% endif %}" rows="3">{{ form.notes.value|default_if_none:'' }}</textarea>
{% for error in form.notes.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<!-- Tags -->
<div class="mb-3 mt-3">
<label class="form-label" for="{{ form.tags.id_for_label }}">Tags</label>
<input type="text" name="tags" class="form-control {% if form.tags.errors %}is-invalid{% endif %}"
value="{{ form.tags.value|default_if_none:'' }}" placeholder="Apply any tags to help organize wallets">
{% for error in form.tags.errors %}
<div class="invalid-feedback">{{ error }}</div>
{% endfor %}
</div>
<!-- Switch for Adding Users -->
<div class="mb-3 switch-container">
<label class="form-label">Adding Users by Team Members</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="adding_users_by_team" {% if form.adding_users_by_team.value %}checked{% endif %}>
</div>
</div>
<!-- Notifications -->
<div class="mb-3 d-flex justify-content-between align-items-center">
<div>
<label class="form-label">Notifications</label>
<p><small>Allow Notifications for wallet</small></p>
</div>
<div class="d-flex gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="alerts" {% if form.alerts.value %}checked{% endif %}>
<label class="form-check-label">Alerts</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="news" {% if form.news.value %}checked{% endif %}>
<label class="form-check-label">News</label>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="text-center mb-5">
<button type="button" class="btn btn-secondary btn-sm float-start" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success text-white btn-sm float-start mx-2">Submit</button>
{% if form.instance.id %}
<!-- Delete Button (appears only for editing) -->
<button type="button" class="btn btn-danger btn-sm float-end" hx-delete="{% url 'delete_wallet' form.instance.id %}"
hx-target="#dialog" hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
Delete Wallet
</button>
{% endif %}
</div>
</form>
</div>
</div>
This is the button that triggers the modal content
<button type="button" class="btn btn-success btn-sm text-white" hx-get="{% url 'add_wallet' %}" hx-target="#dialog">
Add Wallet Address
</button>
I use hx-get and hx-target to render the form inside my #dialog modal.
Ok, you’re using HTMX to get the form, but you’re not using it to submit the data being posted. You’re using a regular form submit button.
This means that your browser would be expecting a full page refresh - your post handling would need to return a full page, not the page fragment.
If you use HTMX to submit the form data, then handling errors is handled like any other HTMX request - you’ll get back the proper div. What changes in this case is that you need to handle the successful response differently. Fortunately, HTMX has a built-in mechanism for that - the HX-Redirect header that you can supply in the response.
(Or, if you’re building this as an entire HTMX-style SPA-ish application, then the successful response would not be a redirect - it would be a rendering of the content of the success url.)
Ah that makes sense. Thanks, Ken.
This is the first time i’m using htmx so not that familar with how this works. Ill look at the hx-redirect to see where i get.
Thanks
Tom.