i'm moving to Rails 7 and i feel like there are so many changes but i'm confident on understanding them and be able to upgrade a personal applications i made myself for keeping my personal records and appointments
More specific i need to communicate between controllers (@hotwire/stimulus) between a flatpickr controller and fullcalendar contorller. The idea is to jump to a date when selecting from flatpicr
I've tried so many diferent options but i'm really stuck.. any help is welcome :)
Rails 7.0.3.1
index.html.erb
<div data-controller="flatpickr" name="" data-action=""></div>
<div data-controller="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
flatpickr_controller.js
import Flatpickr from 'stimulus-flatpickr'
export default class extends Flatpickr {
connect() {
this.config = {
inline: true,
enableTime: false,
time_24hr: false,
onChange: function(selectedDates, dateStr, instance) {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
};
super.connect();
}
}
calendar_controller.js
import { Controller } from "@hotwired/stimulus";
import { Calendar } from '@fullcalendar/core';
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
import interactionPlugin from '@fullcalendar/interaction';
export default class extends Controller {
static targets = [ "popup", "window" ];
connect() {
let overlay = this.popupTarget;
this.calendar = new Calendar(this.windowTarget, {
plugins: [ resourceTimeGridPlugin, interactionPlugin ],
themeSystem: 'bootstrap5',
initialView: 'resourceTimeGridDay',
aspectRatio: 1.8,
nowIndicator: true,
selectable: true,
editable: true,
allDaySlot: false,
});
window.addEventListener('load', () => {
this.calendar.render();
});
}
refresh(e) {
if (e.detail.success) {
this.calendar.refetchEvents();
}
}
}
output
application-7082a89999639e6d01ae0ef0aaaf6707b39fab96541f1dcd1c79da24753cb0ed.js:28271 Uncaught TypeError: Cannot read properties of undefined (reading 'getControllerForElementAndIdentifier')
at Object.onChange (ap ...
I think I'm gonna get mad with this... thank you!
Well done on trying to understand all of this, it can be hard to learn something new and especially when you have 'working' code and you are kind of forced to change.
One thing that can help is to revisit the Stimulus documentation, it does have pretty much all the answers you need for these issues but maybe needs a bit of a re-read.
The other thing which can be super frustrating is JavaScript's usage of this
and how it works.
Hopefully the below breakdown helps.
this
(JavaScript)The first problem with the code above is that you are referencing this
with the assumption that it refers to your controller instance, but rather it is referring to the event's context.
onChange: function(selectedDates, dateStr, instance) {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
In the above code, this.application
and this.calendarTarget
will never work as the this
here is the context created by the onChange
handler calling context.
The quick way around this this
issue is to just use an arrow function. In the below revised code snippet (which will still not work, due to issues 2 & 3 below), the arrow function approach is used instead of a function
declaration, which pulls in the this
from the parent context, which will be the Controller's instance.
onChange: (selectedDates, dateStr, instance) => {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
The best way, however, is to read the documentation on Mozilla here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this in full, read it again and then maybe a third time. After that, find some YouTube videos and watch those. You will find JavaScript development much easier if you truly 'grok' this concept, but it is hard to understand.
The next issue is your use of this.calendarTarget
in your flatpackr controller, this controller will not have any target available due to it not being set up correctly.
In the Stimulus docs - https://stimulus.hotwired.dev/reference/targets you can read that the target must be in the controller's scope. But in the HTML below the data-controller="flatpickr"
div has no children and also has no targets in the HTML anywhere that can be accessed by this controller.
<div data-controller="flatpickr" name="" data-action="">No Children here?</div>
<div data-controller="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
There are a few ways to access something outside the controller's scope, but the easiest way would be to bypass this problem all together and use the Stimulus' preferred way to communicate with other controllers.
But, if you want to use a target you need to do two things.
export default class extends Flatpickr {
static targets = [ "calendar" ]; // this is required
<div data-controller="flatpickr" name="" data-action="">
<div data-controller="calendar" data-flatpickr-target="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
</div>
Finally, your use of getControllerForElementAndIdentifier
is documented as a work around if there is no other way to communicate with another controller.
The preferred way is using events and it is incredibly powerful, flexible and will probably solve 99.9% of your use cases. Have a read of https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent & https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent if you are not sure what browser events are first.
Then, you can dispatch an event from your flatpackr
controller for your calendar
controller to pick up on.
The simplest way to go about this is an event that jut gets dispatched on your first controller and 'bubbles' up the DOM and then your calendar controller listens to this globally.
data-action
attribute on your calendar.flatpackr:changed
and when it sees that, it will call your calendar controller's method goToDate
.<div data-controller="flatpickr"></div>
<div data-controller="calendar" data-action="flatpackr:changed@window->calendar#goToDate">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
this.dispatch()
which will trigger the dispatching of a CustomEvent
with the supplied options.import Flatpickr from 'stimulus-flatpickr'
export default class extends Flatpickr {
connect() {
this.config = {
inline: true,
enableTime: false,
time_24hr: false,
onChange: (selectedDates, dateStr, instance) => {
// note: Stimulus sets `bubbles` to true by default but good to be explicit
const someDate = '18-01-2025'; // random date
// passing data to the other controller can be via the `detail` object in the CustomEvent & Stimulus will automatically add 'flatpackr:' to the start of the event name for you (Thanks Stimulus!)
this.dispatch('changed', { detail: { date: someDate } , bubbles: true } );
},
};
// super.connect(); - not sure that you need this in most cases so commented out
}
}
goToDate
.event.detail
from the param.import { Controller } from "@hotwired/stimulus";
import { Calendar } from '@fullcalendar/core';
import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
import interactionPlugin from '@fullcalendar/interaction';
export default class extends Controller {
static targets = [ "popup", "window" ];
connect() {
// ...
// note: you may not need the window on load listener as `connect` will only be called when there is a DOM ready to attach to.
}
refresh(e) {
// ...
}
goToDate(event) {
// note: you can use destructuring above and change the signature to ` goToDate({ detail: { date } }) {` instead
const date = event.detail.date;
console.log('do something with the date now', date);
}
}
Note: I have not tested locally but should be close enough