Search code examples
javascriptmodal-dialogwagtailwagtail-admin

Is there a right way to open modal window in wagtail-admin?


I try to improve my wagtail-admin and now I stack because there is no way to open a modal window. Yes, of course, I could create a div with a close button, but this would be not the right way. As I've got, there is a special function (or object) which is responsible for opening such a window.

There is no reference for such a structure of js objects of wagtail-admin. May be somebody knows, how to do it? Or maybe I should forget about it and make my modal window just by vanilla javascript?


Solution

  • The short answer is that there is no documented way to use the existing Wagtail admin modals.

    However, with a bit of looking at the source code it is possible to leverage the modal workflow to implement your own modals. The approach in Wagtail is to have a server side template response supplied by render_modal_workflow.

    On the client, a function is available ModalWorkflow. That will call a URL async and render the html content inside the modal on response, it expects a response formed by the above render_modal_workflow helper.

    From these basics it is possible to add open behaviour by a button trigger, error handling, render callbacks and callbacks based on value from inside the modal.

    Below is a bare minimum example of a way to render a modal in the admin using this approach.

    Example

    1. Render some html content that has a button as a trigger

    • For the sake of example, we will render a modal on the Wagtail home (dashboard) page.
    • Using the construct_homepage_panels we can add some html to a panel part way down the page.
    wagtail_hooks.py
    from django.utils.safestring import mark_safe
    from wagtail.core import hooks
    
    class WelcomePanel:
        order = 110
    
        def render(self):
            return mark_safe("""
            <section class="panel summary nice-padding">
              <h3>Dashboard Panel Section Title</h3>
              <button data-modal-trigger="some-param">Open Modal</button>
            </section>
            """)
    
    @hooks.register('construct_homepage_panels')
    def add_another_welcome_panel(request, panels):
        panels.append(WelcomePanel())
    

    2. Ensure the modal-workflow JS script is loaded

    • By default, only pages that handle editing have the modal-workflow script loaded
    • To add it to this specific page we need to override the wagtailadmin/home.html template template.
    • We will also add a bit of jquery to find any elements that have the data-modal-trigger attribute and add an onClick listener which will call our ModalWorkflow function. This data can be passed back to the modal view, along with any other specific data.
    templates/wagtailadmin/home.html
    {% extends "wagtailadmin/home.html" %}
    {% load wagtailadmin_tags %}
    
    {% comment %}
        Javascript declaration added to bring in the modal loader, by default it is only available on edit pages
        example of usage - wagtail/search/templates/wagtailsearch/queries/chooser_field.js
    {% endcomment %}
    
    {% block extra_js %}
      {{ block.super }}
      <script src="{% versioned_static 'wagtailadmin/js/modal-workflow.js' %}"></script>
      <script type="text/javascript">
        $(function() {
          $('[data-modal-trigger]').on('click', function(element) {
            /* options passed in 'opts':
              'url' (required): initial
              'responses' (optional): dict of callbacks to be called when the modal content
                  calls modal.respond(callbackName, params)
              'onload' (optional): dict of callbacks to be called when loading a step of the workflow.
                  The 'step' field in the response identifies the callback to call, passing it the
                  modal object and response data as arguments
            */
            ModalWorkflow({
              onError: function(error) { console.log('error', error); },
              url: '/admin/modal/?trigger=' + element.target.dataset.modalTrigger
            });
          });
        });
      </script>
    {% endblock %}
    

    3. Create a view and url to handle the modal requests

    • Ensure there is an admin/... url that we can request the modal content from
    • This url must go to a view that returns a response based on render_modal_workflow
    • It is possible to initiate data on the client side along with using a normal Django template response for the server side rendered modal content
    views.py
    from django.template.response import TemplateResponse
    
    from wagtail.admin.modal_workflow import render_modal_workflow
    
    
    def modal_view(request):
    
        return render_modal_workflow(
            request,
            'base/modal.html', # html template
            None, # js template
            {'trigger': request.GET.get('trigger')}, # html template vars
            json_data={'some': 'data'} # js template data
        )
    
    
    urls.py
    from django.conf.urls import url
    from .views import modal_view
    
    urlpatterns = [
        url(r'^admin/modal/', modal_view, name='modal'),
        url(r'^admin/', include(wagtailadmin_urls)),
        # ...
    ]
    

    4. Set up your template to render the modal content

    • Modals all use the same shared header template, which gives a nice way to make it feel consistent.
    templates/base/modal.html
    {% include "wagtailadmin/shared/header.html" with title="Modal Title" icon="no-view" %}
    
    <div class="nice-padding">
        <p>Modal Triggered by {{ trigger }}</p>
    </div>