Form Validation inside of modal with htmx

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.)

1 Like

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.