neowenshun
neowenshun

Reputation: 960

Modal Formbuilder for wagtail?

Hello im new to wagtail and its been really awesome so far. However im facing an issue trying to create a modal version of the formbuilder. My intentions is to create an action button within the base.html of which the user can click at any point in time and enter a modal pop up form to leave a feed back . Is there a way of accomplishing this?

Upvotes: 1

Views: 699

Answers (1)

LB Ben Johnston
LB Ben Johnston

Reputation: 5176

This is very doable, you will need to work out how you want your modals to look and work by finding a suitable modal library.

Once you have that, you will need to determine how your admin interface will provide the setting of which FormPage will be used on the base template to render the modal. Wagtail's site settings is a good option.

From here, you then need to get the linked form and use it's get_form() method (all pages that extend AbstractForm or AbstractEmailForm have this). If you want to understand how the form is processed you can see the code here.

The simplest way to handle form submissions is to POST to the original form, this way there does not need to be any additional handling elsewhere. You also get the 'success' page as per normal without having to render that inside the modal.

Below is a basic code example that should get you started.

Example Code Walk-through

1. install wagtail settings

# settings.py

INSTALLED_APPS += [
    'wagtail.contrib.settings',
]

2. Set up a settings model

from django.db import models
from wagtail.contrib.settings.models import BaseSetting, register_setting
from wagtail.admin.edit_handlers PageChooserPanel

# ... other models & imports

@register_setting
class MyAppSettings(BaseSetting):
    # relationship to a single form page (one per site)
    modal_form_page = models.ForeignKey(
        'wagtailcore.Page',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+',
        verbose_name='Modal Form'
    )

    panels = [
        # note the kwarg - this will only allow form pages to be selected (replace base with your app)
        PageChooserPanel('modal_form_page', page_type='base.FormPage')
    ]

3. Install a basic modal library

  • This will be up to you, if you use bootsrap for example it comes with a modal library
  • For this example I have used https://micromodal.now.sh/#installation
  • There are lots of ways to do this, this is the simplest and does not require any async background calls to the server

3a. Add the css in your static folder (e.g. my-app/static/css/micromodal.css) & then import it in your central header or layout template.

<head>
  <!-- ALL OTHER ITEMS -->
  <!-- Modal CSS -->
  <link href="{% static 'css/micromodal.css' %}" rel="stylesheet" type="text/css">
</head>

3b. Add the JS & init call in your base template, best to do this as the last item before the closing body tag

<!-- Modal JS -->
<script src="https://unpkg.com/micromodal/dist/micromodal.min.js"></script>
<script>
    document.addEventListener("DOMContentLoaded", function() {
        MicroModal.init();
    });
</script>

4. Set up a template tag

  • There are a few ways to do this, but basically we want our base.html template to have a convenient way to place the modal & modal trigger. Django's template tags are a great way to do this.
  • Docs - https://docs.djangoproject.com/en/3.0/howto/custom-template-tags/#inclusion-tags
  • Using the instructions in the Wagtail site settings page we can import our settings model and access the related form page, from here we can generate the form object.
  • The template code below is the bare minimum, you will want to do more styling probably, it assumes the trigger will just be rendered along with the modal content.
  • The template contains some basic logic to allow for tracking of the page the form was submitted on source-page-id so we can redirect the user to their source page and request.session reading to show a success message.
# my-app/templatetags/modal_tags.py
from django import template
from models import MyAppSettings

register = template.Library()

# reminder - you will need to restart Django when adding a template tag


@register.inclusion_tag('tags/form_modal.html', takes_context=True)
def form_modal(context):
    request = context['request']  # important - you must have the request in context
    settings = MyAppSettings.for_request(request)
    form_page = settings.modal_form_page

    if not form_page:
        return context

    form_page = form_page.specific

    # this will provide the parts needed to render the form
    # this does NOT handle the submission of the form - that still goes to the form page
    # this does NOT handle anything to do with rendering the 'thank you' message

    context['form_page'] = form_page
    context['form'] = form_page.get_form(page=form_page, user=request.user)

    return context
{% comment %} e.g. my-app/templates/tags/form_modal.html {% endcomment %}
{% load wagtailcore_tags %}

{% if request.session.form_page_success %}
  Thanks for submitting the form!
{% endif %}

{% if form %}

<button data-micromodal-trigger="modal-1">Open {{ form_page.title }} Modal</button>

<div class="modal micromodal-slide" id="modal-1" aria-hidden="true">
  <div class="modal__overlay" tabindex="-1" data-micromodal-close>
    <div class="modal__container" role="dialog" aria-modal="true" aria-labelledby="modal-1-title">
      <header class="modal__header">
        <h2 class="modal__title" id="modal-1-title">
          {{ form_page.title }}
        </h2>
        <button class="modal__close" aria-label="Close modal" data-micromodal-close></button>
      </header>
      <form action="{% pageurl form_page %}" method="POST" role="form">
        <main class="modal__content" id="modal-1-content">
          {% csrf_token %}
          {{ form.as_p }}
          {% if page.pk != form_page.pk %}
          {% comment %} only provide the source page if not on the actual form page {% endcomment %}
          <input name="source-page-id" type="hidden" value="{{ page.pk }}">
          {% endif %}
        </main>
        <footer class="modal__footer">
          <input class="modal__btn modal__btn-primary" type="submit">
          <button class="modal__btn" data-micromodal-close aria-label="Close this dialog window">Close</button>
        </footer>
      </form>
    </div>
  </div>
</div>

{% endif %}

5. Use the template tag where you want the modal trigger

  • Now you can add the modal (along with its trigger)
  • Important: for the template tag above to have access to the request via context, you may need to add this (depending on your setup).
    <!-- Footer -->
    <footer>
        {% form_modal %}
        {% include "includes/footer.html" %}
    </footer>

6. Redirecting back to the source page

# models.py

class FormPage(AbstractEmailForm):
    # .. fields etc

    def render_landing_page(self, request, form_submission=None, *args, **kwargs):
        source_page_id = request.POST.get('source-page-id')
        source_page = Page.objects.get(pk=source_page_id)

        if source_page:
            request.session['form_page_success'] = True
            return redirect(source_page.url, permanent=False)

        # if no source_page is set, render default landing page
        return super().render_landing_page(request, form_submission, *args, **kwargs)

Upvotes: 3

Related Questions