37

Someone has asked the exact same question in April, without any answer. But since he provided too little information; the question was abandoned.

I have the same problem. Within a main_page.html I have this line:

<a href="/contact/edit/{{ item.id }}" title="Edit">edit</a>

Once you click there, the edit template shall appear inside a twitter bootstrap modal.

url.py

(r'^contact/edit/(?P<contact_id>\d+)/$', contact_view),

view.py

def contact_view(request, contact_id=None):
    profile = request.user.get_profile()
    if contact_id is None:
        contact = Contact(company=profile.company)
        template_title = _(u'Add Contact')
    else:
        contact = get_object_or_404(profile.company.contact_set.all(), pk=contact_id)
        template_title = _(u'Edit Contact')
    if request.POST:
        if request.POST.get('cancel', None):
            return HttpResponseRedirect('/')
        form = ContactsForm(profile.company, request.POST, instance=contact)
        if form.is_valid():
            contact = form.save()
            return HttpResponseRedirect('/')
    else:
        form = ContactsForm(instance=contact, company=profile.company)
    variables = RequestContext(request, {'form':form, 'template_title': template_title})
    return render_to_response("contact.html", variables)

This is usually how the contact.html would look like:

        <form class="well" method="post" action=".">
            {% csrf_token %}
            {{form.as_p}}
            <input class="btn btn-primary" type="submit" value="Save" />
            <input name="cancel" class="btn" type="submit" value="Cancel"/>
        </form>

I could put that inside a <div class="modal-body">. But then how do I open the modal from view?

Community
  • 1
  • 1
Houman
  • 64,245
  • 87
  • 278
  • 460

2 Answers2

51

Unless you need to use the contact form outside of the modal, this should work for you. If you do need to use it elsewhere, maintain two versions (one modal, one not). Also, a tip - give django-crispy-forms a lookover - it helps you render forms with twitter-bootstrap classes.

contact.html:

<div class="modal hide" id="contactModal">
<form class="well" method="post" action="/contact/edit/{{ item.id }}">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal">×</button>
    <h3>Editing Contact</h3>
  </div>
  <div class="modal-body">
       {% csrf_token %}
       {{form.as_p}}
  </div>
  <div class="modal-footer">
       <input class="btn btn-primary" type="submit" value="Save" />
       <input name="cancel" class="btn" type="submit" value="Cancel"/>
  </div>
</form>
</div>

main_page.html

<html>
...

<a data-toggle="modal" href="#contactModal">Edit Contact</a>

{% include "contact.html" %}

...
</html>

Edit:

Ok, so now that I know that you have potentially multiple forms, it's probably a bad idea to render each form hidden within the html. You probably want to go to an ajax-y version, and load each form on demand. I'm assuming here that on form submit, the whole page will reload. If you want to asynchronously submit the form, there are answers elsewhere.

We'll start by re-defining the contact.html fragment. It should render within a modal, and contain all the markup necessary to play nice with the modal. The contact view that you have originally is fine - except that if the form is invalid, you'll end up rendering the contact.html and nothing else.

<form class="well contact-form" method="post" action="/contact/edit/{{ item.id }}">
  <div class="modal-header">
    <button type="button" class="close" data-dismiss="modal">×</button>
    <h3>Editing Contact</h3>
  </div>
  <div class="modal-body">
       {% csrf_token %}
       {{form.as_p}} <!-- {{form|crispy}} if you use django-crispy-forms -->
  </div>
  <div class="modal-footer">
       <input class="btn btn-primary" type="submit" value="Save" />
       <input name="cancel" class="btn" type="submit" value="Cancel"/>
  </div>
</form>

And now, your main_page.html:

<html>
.. snip ..

<a class="contact" href="#" data-form="/contact/edit/{{ item.id }}" title="Edit">edit</a>
<a class="contact" href="#" data-form="/contact/edit/{{ item.id }}" title="Edit">edit</a>
<a class="contact" href="#" data-form="/contact/edit/{{ item.id }}" title="Edit">edit</a>

<div class="modal hide" id="contactModal">
</div>

<script>
    $(".contact").click(function(ev) { // for each edit contact url
        ev.preventDefault(); // prevent navigation
        var url = $(this).data("form"); // get the contact form url
        $("#contactModal").load(url, function() { // load the url into the modal
            $(this).modal('show'); // display the modal on url load
        });
        return false; // prevent the click propagation
    });

    $('.contact-form').live('submit', function() {
        $.ajax({ 
            type: $(this).attr('method'), 
            url: this.action, 
            data: $(this).serialize(),
            context: this,
            success: function(data, status) {
                $('#contactModal').html(data);
            }
        });
        return false;
    });
</script>

.. snip ..
</html>

This is all untested, but it should give you a good place to start/iterate from.

Josh Smeaton
  • 47,939
  • 24
  • 129
  • 164
  • Thanks Josh for this excellent solution. the modal is still empty. I cant see the form actually rendered. I think its because it is opened directly from main_page.html, rather than from view. Hence no instance of form has been created. :( Its like chicken and egg. I will also look into the crispy forms as you suggested... – Houman Jul 01 '12 at 00:13
  • @Kave you need to make sure you're passing the form to your main_html template from your main/index view. Otherwise, look into using `jQuery.load` to load the html fragment using your existing contact_view. My answer was assuming that you'd be only using `contact_view` for processing the form (and redirecting back to your main/index), and using your main view for rendering. Do you want to process the contact form using ajax, or do a full reload of the entire page? I'm leaving a lot of this up to you obviously, but your options are render from main, or use ajax to render and process a partial. – Josh Smeaton Jul 01 '12 at 04:25
  • Thanks Josh. Lets try to clarify this further. The `main_page.html` is the index page and shows all saved Contacts, which can be deleted or edited. If the user clicks on edit of second item he will be redirected to `/contact/edit/2`, which in turn is captured in url.py and delegated to our `contact_view`. In there it would search for the requested contact id and instantiate it into a form object which is rendered in the `contact.html`. Therefore within `main_page.html` the form cannot even exist since we don't even have the contact object yet until redirecting to the view first. – Houman Jul 01 '12 at 10:34
  • the jQuery load looks very promising. I am still having trouble due a endless loop, but I keep you updated. :) – Houman Jul 01 '12 at 16:04
  • Thanks Josh. Amazing code. It actually works :D Thank you so much. Only problem I am now facing is that inside the modal window the `action="/contact/edit/{{ item.id }}"` doesn't know anything about item.id. I am now googling to find a way to pass in the id... – Houman Jul 02 '12 at 09:20
  • 1
    @Kave, your view function is already accepting a `contact_id`, just pass that into your `RequestContext` as a variable, and use *that* instead of `{{ item.id }}`. Easy. Glad to help. – Josh Smeaton Jul 02 '12 at 10:07
  • Cheers Josh, oh ofcourse. I didn't see the wood for the trees. :D Before seeing your answer here, I found a different solution: `action="/contact/edit/{{ form.initial.id }}/"` (See also trailing slash, for some reason the modal window requires that or url.py goes crazy, even though in browser you don't need that). Not sure if this is a good way to do it. but passing contact_id seems cleaner. – Houman Jul 02 '12 at 10:34
  • btw Josh, I think this Cispy-Form question is something for you. ;) http://stackoverflow.com/questions/11297348/how-to-spread-form-fields-on-two-column-layout-in-django-crispy-forms – Houman Jul 02 '12 at 16:50
  • 1
    @Kave, django expects a trailing slash everywhere. You might have a django setting `ADD_TRAILING_SLASH` (I think it's called) that's causing a redirect to happen - and I'm not sure how jQuery.load handles redirection (probably not well). Avoid `form.initial` if ever possible, it's a work-around only. Glad it all worked for you. – Josh Smeaton Jul 03 '12 at 05:01
  • Josh, I had to take a step back from this solution to get more experience in django before giving it another shot. This solution has a major problem, that is if the form has any validation issues, the form isn't re-shown to see the errors. Is there a way to check first if the sent form was valid before dismissing the modal (once clicked on save) ? It seems there is a need for javascript magic isn't it? ;) – Houman Aug 22 '12 at 07:40
  • @Kave, I did mention that constraint in my answer :P you need to submit the form asynchronously (use jquery), and render the modal again with the errors, or with success. I'd suggest looking for another question/answer here on SO as it has been answered many times. – Josh Smeaton Aug 22 '12 at 22:56
  • @Kave, I've included a javascript snippet that will submit the form and render the results (in the main_page.html). I leave it to you to work out how to do the django view. You must render the form when it hasn't been POSTed, or if it's POSTed with errors. You should render the form with "Success" or something similar if it passed validation. Do not do a redirect for the asynch POST. – Josh Smeaton Aug 22 '12 at 23:05
  • If you go the ajax to submit the form as given in the example, you have consider how you would handle a redirect when the form posts successfully. One way to do that is to test the response back from the ajax and do a location.reload(true) to reload the underlying page. – Pier1 Sys Dec 08 '12 at 02:58
  • I am not sure this is a good solution. One of the reasons why modal forms are used is to prevent a whole page reload when the form is submitted. – Shailen Aug 21 '13 at 14:39
  • @shailenTJ the question and this answer were mainly about rendering the form correctly. I did suggest that the form should be submitted asynchronously, but that was outside the scope of this answer. – Josh Smeaton Aug 21 '13 at 21:35
1

I was having the same problem when I encounter this post. I tried this solution but it wasn't really working for me (but it gave me direction). I came up with a solution that does work for me but, it feels hacky and I would like to receive some pointers on how to do it better.

The problem is the same: Show a form inside twitter boostrap modal and django form (edit/add model for me using django generic form views)

So what I did:

main.html

<script type="text/javascript">
    function genericLoadDialog(form_selector, dialog_selector, matchString){
        $.ajax({
            url: $(form_selector).attr('action'),
            type: 'POST',
            data:  $(form_selector).serialize(),
            success: function(data, textStatus, jqXHR){
                if(data.match(matchString)){
                // We got errors in form
                    $(dialog_selector).html(data).modal('show');
                            return false;
                }
                        $(dialog_selector).modal('hide');
            },
        })
    }
</script>

<body>
<div class="modal hide" id="{{ editor_dialog_id }}"></div>
<a data-toggle="modal" href="#{{ editor_dialog_id }}" onclick="$('#{{ editor_dialog_id }}').load('/create/form');">Title</a>
<body>

editor.html

<form id="{{ editor_form_id }}" action="{{ submit_url }}" method="POST">
    <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal">×</button>
        <h3>Modal header</h3>
    </div>
    <div class="modal-body" id="editor-dialog-body">
        {% if not form.is_valid %}
            <div class='hide'>invalid_form</div>
        {% endif %}
        {% csrf_token %}
        {{ form.as_p }}
    </div>
    <div class="modal-footer">
       <a href="#" class="btn" data-dismiss="modal">Cancel</a>
       <a href="#" data-toggle="modal" class="btn btn-primary" onclick="genericLoadDialog('#{{ editor_form_id }}', '#{{ editor_dialog_id }}', null, 'invalid_form');">Save</a>
    </div>
</form>

So the flow is this:

  • click on the tag loads the form from the create form url into the modal div
  • click the save button in modal (submit) which triggers the genericLoadDialog function. This loads makes a POST request to the create form url with data collected from the form
  • If the form is invalid it reloads the modal html and shows the error fields, else closes the modal and django should save/redirect (I use get_success_url method in django form view but not working for me for some reason. It does save the object)

I'm uncomfortable with the way I check if the form is valid or not in genericLoadDialog, if someone has a better idea would be nice.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
  • 1
    thank you for this solution. I will now start looking into it. As the solution above works only as long as you dont get any validation errors. – Houman Aug 21 '12 at 22:09
  • 2
    After some further evaluation, your example doesn't seem to be working. Your javascript takes three parameters and pass in four. Generally its not clear. – Houman Aug 22 '12 at 07:22