Create Instance of controller from a controller

Hello everybody,

I just stared using stimulus and I’m having some trouble, converting my existing javascript to stimulus.

I have a class A that does things and a class B that does things.
In class A an instance of class B is created with some params.

So I wrote class A as a stimulus controller, which works just finde. But I integrated class B in that. Now I want to have class B as a separate controller and call it from that A stimulus controller. I have no idea how to do that.

I cannot use events, since there are no events that trigger my script. The controller A does its job when it’s initialized.

Hope I can get some help. Thank you

David

You can definitely use events. Events are the way to go in your case. And, it’s pretty easy to listen and dispatch events with stimulus. Let’s follow along with your case and create the classes.

class A extends Controller {
    initialize() {
       this.dispatch("custom-event-from-a-class", { 
        target: document.documentElement,
        data: { 
          // data you want to send to class B
         }
       })
    }
}

class B extends Controller {
  onClassAEvent({ data }) {
     // data dispatched from class A custom event
  }
}

Now, with the classes being set up. You want to “listen” for custom-event-from-a-class from b element. Let’s say your markup looks like this

<div data-controller="b" data-action="a:custom-event-from-a-class@window->b#onClassAEvent" />
<div data-controller="a" />

In the b element. We listen for a a:custom-event-from-a-class that targets the window via the @window descriptor. See Global Events

when a controller dispatches it’s event. Every controller that has the a:custom-event-from-a-class@window->b#onClassAEvent would react to it.

Now, A and B controller can communicate together. And vice-versa is true. If you want to interact with A controller from B, you need to dispatch an event from B, and listen to the event inside A controller.

Hope this helps

1 Like

That was really helpful to understand the dispatch. Thank you.

Still it is a bit confusing to me.
I found out that data-controller must in in the correct order, so data-controller=“a b” did not work, but data-controller=“b a” does. So far so good.

Now I don’t really get, what custom-event-from-a-class is there for… Code in custom-event-from-a-class is not executed that way. So at the moment it seems to be only helping for the dispatch (since I need a custom method, initialize doesn’t work).
If I want code from custom-event-from-a-class to be executed I have to call it (from initialize)…

But the main issue now is, that I need the return value from B->onClassAEvent in A. How can I access the return value?

Thanks again for your help.

David

Does B also need to be a controller, or just a class referenced by controller A?

I don’t think it has to be a controller.
It’s just a class, that checks if intersectionObserver is present and returns an instance if so, otherwise “false”. It gets 2 params: the callback and the observer options.

I just don’t know what’s the right way to instantiate it now, stimulus style :grimacing:

It would help if you shared the code

export default class extends Controller {

    createObserver(callback, threshold, rootMargin)  {

        this.observerCallback = callback ?? {};
        this.observerOptions = {
            root: null,
            threshold: threshold ?? 0,
            rootMargin: rootMargin ?? "0px 0px 0px 0px",
        };

        return this.checkIfObserver();
    };

    checkIfObserver() {

        if (!('IntersectionObserver' in window) ||
            !('IntersectionObserverEntry' in window) ||
            !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {

            return false;
        } else {
            return new IntersectionObserver(this.observerCallback, this.observerOptions);
        }
    };
}

that’s “B” … A is whatever needs this return value…
Don’t think, this needs to be a controller, since I don’t use it as such. I just need it for other classes to call it. I jus want it to integrate the way it’s supposed to in stimulus…

If I understand correctly, you can use javascript modules

// utils/createObserver.js
import IntersectionObserver from 'intersection-observer'

export default function createObserver(callback, threshold, rootMargin)  {
    const observerCallback = callback ?? {};
    const observerOptions = {
      root: null,
      threshold: threshold ?? 0,
      rootMargin: rootMargin ?? "0px 0px 0px 0px",
    };

    return checkIfObserver(observerCallback, observerOptions);
};

function checkIfObserver(callback, options) {
  if (!('IntersectionObserver' in window) ||
    !('IntersectionObserverEntry' in window) ||
    !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {

    return false;
  } else {
    return new IntersectionObserver(callback, options);
  }
}

Then you import into your controller

// utils/my_controller.js
import createObserver from 'utils/createObserver'

export default class extends Controller {
  connect() {
    this.observer = createObserver(... )
  }
}

Ok thank you.
If I understand correctly, there is no special “stimulus way” to integrate this. I’m ending up, keeping my original observer class and creating a regular instance in my controller:

export class Observer {

    constructor(observerCallback, observerOptions) {
        this.callback = observerCallback ?? function() {};
        this.options = observerOptions ?? {
            root: null,
            threshold: 0,
            rootMargin: "0px, 0px, 0px, 0px",
        };

        return this.checkIfObserver();
    };

    checkIfObserver() {

        if (!('IntersectionObserver' in window) ||
            !('IntersectionObserverEntry' in window) ||
            !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {

            return false;
        } else {
            return new IntersectionObserver( this.callback, this.options);
        }
    };
}

and

import {Controller} from '@hotwired/stimulus';
import {Observer} from './observer.js'

export default class extends Controller {
    initialize() {
    this. this.observerCallback = ... ;
    this.observerOptions = ... ;
        const observer = new Observer(this.observerCallback, this.observerOptions);
    ...
    }
}

Correct. I will mention a couple of additional things.

If the element assigned the controller might be removed and reinitialized in the DOM, you may want to use connect() instead of initialize().

If you are also using turbo (which acts like an SPA), you want to be careful of memory leaks caused by listeners and observers. To avoid memory leaks, add disconnect() to the stimulus controller with code that removes the listeners or observers.

1 Like

First of all thank you so much for helping me understand stimulus!

you may want to use connect() instead of initialize().

yes, I read about that in the docs. So far initialize does the job for me.

If you are also using turbo

Turbo is actually the reason I started having a look into stimulus. Turbo killed my JavaScripts ( :partying_face:) … so I wanted to start understanding what stimulus does etc.

Still not sure I really need / should use stimulus or turbo. Especially turbo seems “dangerous”… But I will try a few things and see what happens…

In the past I tried using TurboLinks (now Turbo) several times, each time I ran into enough issues which caused me to give up. It wasn’t until recently that I realized why.

Turbo Drive caches pages using HTML. Since most JavaScript libraries persist data in memory, this can cause weird behavior with turbo caching.

Stimulus easily persists data in HTML (data attributes). This is why stimulus works so well with Turbo. With stimulus you can also wrap libraries which save data in memory to store data in html.

Once I started using stimulus and understood how turbo worked, the entire Hotwire stack worked really well and no longer felt brittle. In fact, for me it feels more robust than the majority of the SPA frameworks out there.

Best of luck.

1 Like

Thanks so much for your help and sharing your experience. I’m definitely eager to learn more about stimulus and turbo. I’ll keep testing it. I’m certain I’ll ask for advice here again :smile: