Modals

When reaching for a modal, first consider if you can communicate this message in another way.

Overview

Modals are disruptive, confusing, poorly accessible, blocking the user’s interaction, hard to escape, used as a junk drawer, frustrating on small screens, and add to cognitive load. Consider non-modal dialogs on empy screen space, go inline, expand elements or use a new page.

If you must use a modal, make sure it is easy to close, single purpose, short, and accessible.

There are lots of alternatives to modals. Most of the time, if you need to confirm an action, material design suggests instead offering an option to undo. This still gives a user an option to reverse an action, but it does not interrupt their flow and increase their cognitive load. Read more on modalzmodalzmodalz.com

Using Modals

When it's appropriate to use a modal, we have several options in HQ to initialize a modal.

Standard Usage

The standard way to use modals is with HTML—an example is shown below. You can read Bootstrap 5's modal docs for all available background options, sizing, javascript usage, and more.

HTML
<!-- Button trigger modal -->
<button
  class="btn btn-primary"
  type="button"
  data-bs-toggle="modal"
  data-bs-target="#bs-example-modal"
>
  Launch demo modal
</button>

<!-- Modal -->
<div
  id="bs-example-modal" class="modal fade"
  tabindex="-1" aria-labelledby="example-modal-label" aria-hidden="true"
>
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h4 id="example-modal-label" class="modal-title">
          Modal Title
        </h4>
        <button
          class="btn-close" type="button" aria-label="Close"
          data-bs-dismiss="modal"
        ></button>
      </div>
      <div class="modal-body">
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi interdum nibh mattis felis
          consequat euismod id sed lorem. Etiam cursus magna id turpis tincidunt, eu volutpat tellus
          placerat. In porttitor mauris non felis ullamcorper elementum. Duis at neque accumsan massa
          sodales vulputate et eget elit.
        </p>
      </div>
      <div class="modal-footer">
        <button
          class="btn btn-outline-primary" type="button"
          data-bs-dismiss="modal"
        >
          Close
        </button>
        <button class="btn btn-primary" type="button">
          Save Options
        </button>
      </div>
    </div>
  </div>
</div>

Modal Knockout Binding

It is also possible to control modals with Knockout using the modal knockout binding. This binding accepts an observable as an argument. If the value of the observable is true, then the modal is shown. If the value is false, then the modal is hidden. This method eliminates the use of the data-bs-toggle="modal" and data-bs-target="#modalId" attributes on the modal trigger element, as well as the data-bs-dismiss="modal" attributes.

The most simplified version of this is to have a boolean observable bound to modal that's toggled true or false with knockout code. However, the example below is a slightly more complex approach that's more realistic to how we use it in HQ (like with UserRole editing).

HTML
<div id="js-ko-demo-modal">
  <div class="row">
    <!-- ko foreach: items -->
    <div class="col-6">
      <div class="card ">
        <div class="card-body">
          <h5 data-bind="text: name"></h5>
          <p data-bind="text: description"></p>
          <button
            class="btn btn-outline-primary"
            type="button"
            data-bind="click: $root.setItemBeingEdited"
          >
            <i class="fa fa-edit"></i> Edit <!-- ko text: name --><!-- /ko -->
          </button>
        </div>
      </div>
    </div>
    <!-- /ko -->
  </div>

  <div
    class="modal fade"
    tabindex="-1" aria-labelledby="example-ko-modal-label" aria-hidden="true"
    data-bind="modal: itemBeingEdited"
  >
    <div class="modal-dialog">
      <div
        class="modal-content"
        data-bind="with: itemBeingEdited"
      >
        <div class="modal-header">
          <h4
            id="example-ko-modal-label"
            class="modal-title"
            data-bind="text: modalTitle"
          ></h4>
          <button
            class="btn-close"
            type="button" aria-label="Close"
            data-bind="click: $root.unsetItemBeingEdited"
          ></button>
        </div>
        <form data-bind="submit: $root.submitItemChanges">
          <div class="modal-body">
            <div class="mb-3">
              <label for="id-example-item-name" class="form-label">Name</label>
              <input
                id="id-example-item-name"
                class="form-control"
                type="text"
                data-bind="value: name"
              />
            </div>
            <div class="mb-3">
              <label for="id-example-item-description" class="form-label">Description</label>
              <textarea
                id="id-example-item-description"
                class="form-control"
                rows="3"
                data-bind="value: description"
              ></textarea>
            </div>
          </div>
          <div class="modal-footer">
            <button
              class="btn btn-outline-primary"
              type="button"
              data-bind="click: $root.unsetItemBeingEdited"
            >
              Cancel
            </button>
            <button class="btn btn-primary" type="submit">
              Save Changes
            </button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
JS
import $ from 'jquery';
import ko from 'knockout';

let Item = function (id, name, description) {
    let self = {};
    self.id = ko.observable(id);
    self.name = ko.observable(name);
    self.description = ko.observable(description);
    return self;
};

$("#js-ko-demo-modal").koApplyBindings(function () {
    let self = {};
    self.items = ko.observableArray([
        Item(1, "First", "This is the first test item"),
        Item(2, "Second", "This is the second test item"),
    ]);
    self.itemBeingEdited = ko.observable(undefined);

    self.submitItemChanges = function () {
        for (let i = 0; i < self.items().length; i++) {
            if (self.items()[i].id() === self.itemBeingEdited().id()) {
                self.items()[i].name(self.itemBeingEdited().name());
                self.items()[i].description(self.itemBeingEdited().description());
            }
        }
        self.unsetItemBeingEdited();
    };
    self.unsetItemBeingEdited = function () {
        self.itemBeingEdited(undefined);
    };
    self.setItemBeingEdited = function (item) {
        let tempItem = Item(item.id(), item.name(), item.description());
        tempItem.modalTitle = "Editing Item '" + item.name() + "'";
        self.itemBeingEdited(tempItem);
    };
    return self;
});

OpenModal Knockout Binding

This method quickly opens a modal using the openModal knockout binding, which takes a string value of the id of the knockout template supplying the HTML for this modal.

Note that the HTML for the modal in the knockout template differentiates from the standard use because it doesn't contain the <div class="modal fade"> wrapper element. Additionally, the modal inherits the knockout model context that was applied to the element that applied openModal binding.
HTML
<div id="js-ko-demo-open-modal">
  <button
    class="btn btn-primary"
    type="button"
    data-bind="openModal: 'js-ko-template-open-modal-demo'"
  >
    Open Modal
  </button>
</div>

<script id="js-ko-template-open-modal-demo" type="text/html">
  <div class="modal-dialog" aria-labelledby="example-open-modal-label" aria-hidden="true">
    <div class="modal-content">
      <div class="modal-header">
        <h4
          id="example-open-modal-label"
          class="modal-title"
          data-bind="text: modalTitle"
        ></h4>
        <button
          class="btn-close"
          type="button" aria-label="Close"
          data-bs-dismiss="modal"
        ></button>
      </div>
      <div class="modal-body">
        <p data-bind="text: modalText"></p>
      </div>
      <div class="modal-footer">
        <button
          class="btn btn-outline-primary"
          type="button"
          data-bs-dismiss="modal"
        >
          Close
        </button>
        <button type="button" class="btn btn-primary">
          Okaaaaay
        </button>
      </div>
    </div>
  </div>
</script>
JS
import ko from 'knockout';

$("#js-ko-demo-open-modal").koApplyBindings(function () {
    return {
        modalTitle: ko.observable("OpenModal Modal Example"),
        modalText: ko.observable("The modal obtains its knockout context from the context of the triggering element"),
    };
});

OpenRemoteModal Knockout Binding

Another less frequently-used way of triggering a modal is to use the openRemoteModal Knockout Binding. This binding takes a URL to a view in HQ that renders the modal-dialog content of the modal, similar to the knockout template above.

HTML
<div id="js-ko-demo-open-remote-modal">
  <button type="button"
          class="btn btn-primary"
          data-bind="openRemoteModal: remoteUrl">
    Open Modal
  </button>
</div>
JS
import $ from 'jquery';
import ko from 'knockout';
import initialPageData from 'hqwebapp/js/initial_page_data';

$(function () {
    $("#js-ko-demo-open-remote-modal").koApplyBindings(function () {
        return {
            remoteUrl: ko.observable(initialPageData.reverse("styleguide_data_remote_modal") + "?testParam=Okaaaaay"),
        };
    });
});

In the example above, the URL passed to openRemoteModal was registered on the page using the following template tag:

{% registerurl "styleguide_data_remote_modal" %}

The view for styleguide_data_remote_modal rendered the HTML below. Note that the secret_message template variable was populated byt the GET parameter testParam specified at the end of the remoteUrl value as seen in the code above.

HTML
<div class="modal-dialog" aria-labelledby="example-open-remote-modal" aria-hidden="true">
  <div class="modal-content">
    <div class="modal-header">
      <h4 id="example-open-remote-modal" class="modal-title">
        Remote Modal Example
      </h4>
      <button
        class="btn-close"
        type="button" aria-label="Close"
        data-bs-dismiss="modal"
      ></button>
    </div>
    <div class="modal-body">
      <p>
        This modal's context is defined by the context of the view that rendered the modal.
        See <code>styleguide.views.bootstrap5_data.remote_modal_demo</code> for the view that
        rendered this modal.
      </p>
    </div>
    <div class="modal-footer">
      <button
        class="btn btn-outline-primary" type="button"
        data-bs-dismiss="modal"
      >
        Close
      </button>
      <button class="btn btn-primary" type="button">
        {{ secret_message }}
      </button>
    </div>
  </div>
</div>