Home > Blockchain >  Memoize a Bootstrap5 modal inside a Stimulus controller
Memoize a Bootstrap5 modal inside a Stimulus controller

Time:02-23

I've a form within a modal defined in a partial:

<!-- app/views/events/new_event_modal.html.erb -->
<div data-controller="events--form">  
  <div  id="new-event-modal" tabindex="-1" aria-labelledby="new-event-label" aria-hidden="true" data-events--form-target="modal">
    <div >
      <div >
        <div >
          <h5  id="new-event-label">Ajouter un évennement</h5>
          <button type="button"  data-bs-dismiss="modal" aria-label="Fermer"></button>
        </div>
        <div >
          <%= render partial: 'events/form',
                     locals: { event:     event,
                               calendars: calendars,
                               users:     users } %>
        </div>
      </div>
    </div>
  </div>
</div>


<!-- app/views/events/_form.html.erb -->
<%= turbo_frame_tag dom_id(event) do %>
  <div >
    <%= form_with(model: event || Event.new) do |form| %>
      
      <!-- form's content omitted -->

      <div >
        <%= form.submit class: 'btn btn-primary' %>
      </div>
    <% end %>
  </div>
<% end %>

As it is a calendar application, I pre-fill the form with the date selected by users and then show the modal. This is done with some " " icons and a Stimulus controller:

# app/views/simple_calendar/_mounth_calendar.html.erb
# each day has this icon
<%= image_tag 'add.svg',
  size: '20x20',
  alt:  ' ',
  data: {
    action: 'click->events--form#configure_and_show_modal',
    date:   day.strftime('%d/%m/%Y')
  }
%>
// app/javascript/packs/controllers/events/form_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "modal" ]

  connect() {
    this.element.addEventListener('turbo:submit-end', (event) => {
      if (event.detail.success) {
        this.hide_modal();
      }
    });
  }

  configure_and_show_modal(event) {
    const date = event.target.dataset.date;

    this.configure_flatpickr(date);
    this.show_modal();
  }

  configure_flatpickr(date) {
    flatpickr("[data-behavior='flatpickr']", {
      altInput:      true,
      altFormat:     'd F Y',
      altInputClass: 'form-control input text-dark',
      dateFormat:    'd/m/Y H:i',
      defaultDate:   date,
      mode:          'range',
    })
  }

  show_modal() {
    this.modal.show();
  }

  hide_modal() {
    this.modal.hide();
  }

  get modal() {
    return new bootstrap.Modal(this.modalTarget);
  }
}

Issue is that get modal() instantiates a new object each time, thus calling hide_modal() invokes .hide() on a newly created object and does not hide the displayed modal.

I know I need some kind of memoization, but I've no idea how to implement it on a Stimulus controller.

Using ruby we'd do something like:

@modal ||= get_modal

Could anyone guide me in this direction ?

CodePudding user response:

A simple way to achieve this goal would be to store the modal instance on the class instance.

This way you can still access it from your getter but not have to recreate it each time.

The _modalInstance is just a convention, the underscore indicating that it should be private ish. However this could cause issues if the controller gets disconnected somehow and has to reconnect with the modal already shown.


import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "modal" ]

  get modal() {
    if (this._modalInstance) return this._modalInstance;
    const modal = new bootstrap.Modal(this.modalTarget);
    this._modalInstance = modal;
    return this._modalInstance;
  }
}

A nicer way to achieve this though would be to use the Bootstrap JavaScript API.

bootstrap.Modal.getOrCreateInstance(myModalEl)

https://getbootstrap.com/docs/5.1/components/modal/#getorcreateinstance


import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "modal" ]

  get modal() {
    return bootstrap.Modal.getOrCreateInstance(this.modalTarget);
  }
}

CodePudding user response:

Here's how I implemented memoization:

get modal() {
  if (this._modal == undefined) {
    this._modal = new bootstrap.Modal(this.modalTarget);
  }
  return this._modal
}
  • Related