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 class="modal fade" id="new-event-modal" tabindex="-1" aria-labelledby="new-event-label" aria-hidden="true" data-events--form-target="modal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-light" id="new-event-label">Ajouter un évennement</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<%= 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 class="container text-light">
<%= form_with(model: event || Event.new) do |form| %>
<!-- form's content omitted -->
<div class="actions modal-footer">
<%= 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 ?
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);
}
}
Here's how I implemented memoization:
get modal() {
if (this._modal == undefined) {
this._modal = new bootstrap.Modal(this.modalTarget);
}
return this._modal
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With