-
Notifications
You must be signed in to change notification settings - Fork 420
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Communicating between controllers #35
Comments
From a Controller instance, you can call import { Controller } from "stimulus"
export default class ListFilterController extends Controller {
// ...
reloadList() {
this.listController.reload()
}
get listController() {
return this.application.getControllerForElementAndIdentifier(this.element, "list")
}
} |
Thanks! I think this is exactly what I was looking for. I suppose I should pass the element List component is on as first argument for So when a List is next sibling to a ListFilter in the DOM, I should call |
I wouldn’t suggest relying on the relative orders of HTML elements. Prefer targets to access particular elements. Can you move the |
Yeah, I can do that. This look like a better way to go, thanks. If it wouldn't be possible the only way I can think of is to create a parent Controller with targets on both However, I am not sure whether |
FWIW T3 (another JS framework for the HTML you already have ;) has a simple messaging system for communicating between modules (or controllers), keeping them loosely coupled. I wonder if this is something that Stimulus might consider? The T3 equivalent would look something like: Box.Application.addModule('list-filter', function (context) {
return {
onchange: function () {
context.broadcast('listfilterchanged', { filterParam1: value1, … })
}
}
})
Box.Application.addModule('list', function (context) {
function reload (params) {
// reload list with AJAX
}
return {
onmessage: {
listfilterchanged: reload
}
}
}) That way other controllers could respond to the |
We don’t have anything built in for this right now. The easiest thing to do is probably to emit a DOM event from the inner controller and observe it with an action from the outer controller. What I would like to see eventually is a |
Closing this issue for now, but feel free to continue discussing it here. |
Could someone please explain exactly what the parameters to If I have two controllers, 'hello' and 'out' and I want to get to the 'out' controller from inside the 'hello' controller. It seems that I need to pass both parameters, otherwise I get A bit of background: I too am trying to talk from one controller to another. My first controller wants to output some information in an abstract way, so wants to talk to another controller's |
First parameter is element the controller is connected to and the second parameter is name of the controller you want to get instance of. You need first parameter because controller of the same name can be connected to more elements on the page. So the function needs to know where to look for the controller instance you want to get. And you need second parameter to specify controller's name because there can be more than one controller connected to one element. So, one way to do this is to nest components and then handle this in the parent controller as @georgeclaghorn suggested. Second clean way I can think of without relying on access to particular elements would be to add a listener to document for custom event in one component and fire this event in the second one. |
How should one do that? |
Here's an example: #200 (comment) |
I started to work on something for stimulus, check it out, https://github.com/kinnrot/telepub tell me what you think |
I've been using a pattern for several months that allows me to access the controller instances attached to DOM elements without having to call Just put this into your controller's
This means that if you attach a comments controller to a DOM element, all you need to do to access the internal scope of your controller is I wrote about it at length in this post: https://leastbad.com/stimulus-power-move I offer a slightly longer version that automatically camelCases the controller identifier, too. |
@leastbad thanks for that, really helpful and feels good when using it. We are using namespaced controllers so we have something like export function camelize(str: string) {
return str.split(/[-_]/).map(w=> w.replace(/./, m=> m.toUpperCase())).join("").replace(/^\w/, c => c.toLowerCase());
} |
Great catch, @forelabs. I'm always excited to see bullet-proof code broken. 😀 I'm going to borrow your code and update my post. |
If you do this, and I don't recommend that you do, be very careful with your controller names. If they conflict with any of the element's own property names (there are hundreds!) things will go 💥. For example: // value_controller.js
export default class extends Controller {
connect() {
this.element[this.identifier] = this
}
} <input type="text" data-controller="value"> |
To be on the safe side you could put |
Hey @javan, Your advice is solid. People should definitely careful to test whatever objects they attach to for potential namespace collisions. If you check out the original blog post on the technique, you'll see that I'm careful to explain to developers that they can use any valid string. All of that said, I am a bit bummed that you seem to be throwing shade on a useful technique without offering a superior approach. Again, You can use DOM events for cross-controller communication, but that's not really a substitute for accessing the internal state of a controller. |
A different take on this problem of inter-controller communication. Some sample code: // dashboard_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
connect() {
this.name = "Simon"
$(document).on('dashboard.state', function(event, callback) {
callback(this)
}.bind(this))
}
} And in the controller that wants to grab information from the first controller: // sidebar_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
getDashboardState() {
$(document).trigger('dashboard.state', function(dashboardState) {
// Your code to handle the state
})
}
} Please excuse my use of jQuery, but wanted to share the concept of it (source). |
Hey Simon, that's actually pretty interesting. Do you have a solution in mind for accessing multiple instances of a controller? One thing that I like about assigning the controller instance to an accessor on the DOM element (call it the sharp knives approach) it that it allows multiple instances of a controller in the DOM... and heck, multiple controllers on an element, potentially communicating through the one thing they all have in common. jQuery isn't itself a bad thing, and it's a reality for many projects. I actually like that Stimulus provides a viable onramp for jQuery devs that suddenly find themselves in a world where they have a legacy codebase that isn't responding well to webpack and Turbolinks. The biggest strike against jQuery in this regard is that it has its own proprietary events implementation. I did what I could to make the problem easier by releasing jquery-events-to-dom-events. |
The nice approach suggested by @leastbad should be part of the the StimulusJS documentation! (possibly with all the mentioned notes regarding multi-word names, namespace, collision with existing properties. I personally use It took me some days to get to this good solution. (first trying all the hacky and questionable solutions including getControllerForElementAndIdentifier) |
Oh c'mon I don't know the reason why author still didn't mention https://github.com/stimulus-use/stimulus-use/blob/master/docs/application-controller.md but it works like a charm! :) (passing events from child to parent) |
Based on some of the previous suggestions, I went with the approach of using custom events to let child controllers register with the parent controller. I followed the pattern of static declarations (targets, classes, and values) and added declarations for parent and children. Here's what the controllers might look like: class ItemsController extends ApplicationController {
static children = ['item']
}
class ItemController extends ApplicationController {
static parent = 'items'
} Like targets, this adds properties. In this example, the items controller would have Here's a proof of concept with the full example. I'm also working on a fork, if there's any interest in a pull request. |
Try to listen to dispatched events: my_controller.js
In other controller
make sure |
Here's mine approach: trigger_controller.js
and then in your HTML:
and:
You can even pass some additional data:
|
What do you think about the I think the dependency between controllers is something Stimulus doesn't like, but it may be a good thing if the dependency is appropriate. I'm currently using I'd be happy if there was an |
Do you have a URL handy for the Hey If you could propose a syntax for an outlet attribute, what would it look like? <div data-controller="foo" data-foo-beast-value="666">
<div data-controller="bar" data-bar-outlet="beast->foo"></div>
</div> Just thinking out loud. Would you want to restrict outlets to exposed value properties, or could you target any internal variable... which would force the above example to become |
Here it is. The syntax adopted by hey is as follows <div data-controller="parent" data-parent-child-outlet="box">
<div data-controler="child" id="box">
</div>
</div> app.register('parent', class extends Controller {
static outlets = ['child'];
xxx() {
this.childOutlet.yyy(); // yyy is child controller's method
}
}) I am generally happy with this implementation. But Stimulus, in some ways, looks like a mixin of behaviour with HTML. |
Just to offer an additional solution. I don't mean to necro this thread too much, but as people are still commenting on it. I know others here have said, and as similar discussions on the Stimulus forums have often said, the recommended way for communication between controllers is via event based communication. But obviously that doesn't work if the controllers aren't parent/child, or you don't want to fire events on the Window itself. Sometimes you just want to call something on another controller like For controllers that aren't necessarily co-located or parent/child in the DOM, I've often achieved communication by using something lightweight like https://github.com/developit/mitt. Mitt gives you an event bus that you can use to pass events around without the controllers having to be specifically located anywhere in the DOM, or using the Window as a vehicle. It's a pretty flexible approach, and it allows communication between any number of controllers that may or may not be on a page, and the event payload can pass any Javascript Object/String/Array/JSON data you want. It also allows your Stimulus controllers to communicate with other non-Stimulusy things and frameworks that you might have kicking around. If you want to call a method on another controller in the DOM, send an event it's listening for. EventBus.emit('foo:do-thing') connect() {
EventBus.on('foo:do-thing', () => this.doThing())
} I've used it for a lot of various things, like achieving a Vue/React-like input binding to mirror the value of an input to various places in the page: // src/utilities/event_bus.js
import mitt from 'mitt';
export const EventBus = mitt(); import {EventBus} from "../../utilities/event_bus";
import {Controller} from "stimulus";
export class InputBroadcasterController extends Controller {
static values = {
name: String,
};
initialize() {
this.emit = this.emit.bind(this);
}
connect() {
requestAnimationFrame(() => {
this.emit();
this.element.addEventListener("input", this.emit);
});
}
disconnect() {
this.element.removeEventListener("input", this.emit);
}
emit() {
EventBus.emit(`input:${this.nameValue}:change`, {value: this.element.value, dispatcher: this.element});
}
}
export class ValueListenerController extends Controller {
static values = {
name: String,
};
initialize() {
this.read = this.read.bind(this);
}
connect() {
EventBus.on(`input:${this.nameValue}:change`, this.read);
}
disconnect() {
EventBus.off(`input:${this.nameValue}:change`, this.read);
}
read(payload = null) {
if (payload === null) {
throw new Error("No payload received");
}
let {dispatcher, value} = payload;
if (dispatcher !== this.element) {
this.innerHTML = value;
}
}
} <label>What is your name?</label>
<input data-controller='input-broadcaster' data-input-broadcaster-name-value='name' />
...
<p>
Hi <span data-controller="value-listener" data-value-listener-name-value="name">Name</span>,
We're so glad that you're here.
</p
...
<h5> Check your details </h5>
<p>
<b>Name:</b>
<span data-controller="value-listener" data-value-listener-name-value="name">Name</span>
<a href='#name'> <i class="fa fa-edit"></i>Change </a>
</p Personally I think that if anything, having a sort of inbuilt way in Stimulus to fire named events and payloads to other instantiated controllers that aren't necessarily co-located is a better approach than the slightly arcane There could just be a simple extension to the 🤷 my £0.02 |
Sam was the one who said he wanted to see a
What's interesting about your example is that it actually works in the opposite direction - parent reaching into child - than I had anticipated. I find that, usually, I design my controllers so that this kind of thing bubbles up, not down. I'm trying to think of a practical case where a parent needs to call a method on a child. It also strikes me as odd that it would be tied to an
Thanks for the link to One of the great things about the community building up a body of best practices is that we can bring in libraries like Mitt when we need them. I believe that we should be extremely conservative about adding any additional complexity or magic to the I could just be haunted by the enduring ramifications of jQuery's proprietary event system. |
There are often structures in which the parent knows the existence of the child, but the child does not necessarily know the existence of the parent. (In the latter case, communication through events is very effective.) When the parent knows the existence of the child, a relationship of use may be necessary. So, in this case, we need a composition where the menubar uses the menu. Postscript:
This is probably because In modal dialogs, the triggering element and the element that serves as the dialog are often located far apart. In such a case, it is tempting to put a modal controller only in the dialog, so that the controller managing the area with the trigger element can use outlet to open and close the modal. |
Check here how to communicate parent to child, child to parent, mixed controllers and distinct controllers osadasami/code#1 |
@osadasami For example, suppose we have HTML that assumes the following modal. Maybe we will write only one controller to control the modal. This example is very simple and the elements of the button and modal are very close together, but the reality is that this is not always the ideal case. In this case, we would want to put it on the #container in the above example. https://codepen.io/nazomikan/pen/podzPBR If we wanted to do this with events alone, we would have a global event in the modal-launcher controller, and the modal controller would try to open the modal on the trigger. |
@NakajimaTakuya What do you think about this simple solution? https://codepen.io/osadasami/pen/wvPwLYg |
@osadasami It occurred to me that it might be interesting to allow the id to be specified in the at-mark syntax of |
@NakajimaTakuya I would prefer that solution too. Something similar _hyperscript does https://codepen.io/osadasami/pen/YzEKmZG |
I prefer the browser events approach. But sometimes you can't rely on I use stimulus-use so I wrote a little "hook" named Here is how I use it in import { useEventActionMap } from './hooks';
import { Controller } from '@hotwired/stimulus';
import { useDispatch } from 'stimulus-use';
export default class extends Controller {
connect() {
// I want to emit events from this controller
useDispatch(this, {
eventPrefix: true, // Prefix emitted events with controller identifier
});
// Listen for global events and map to actions
useEventActionMap(this, {
eventPrefix: true, // Prefix listened events with controller identifier
actions: {
open: this.open,
close: this.close,
toggle: this.toggle,
},
});
}
open() {
// Will be called for every "menu:open" event and dispatch "menu:opened" event
// ...
this.dispatch('opened');
}
close() { /* logic */ }
toggle() { /* logic */ }
} The import { useEventActionMap } from './hooks';
import { Controller } from '@hotwired/stimulus';
import { useDispatch } from 'stimulus-use';
export default class extends Controller {
connect() {
// I want to emit events from this controller (to open the menu)
useDispatch(this, {
eventPrefix: 'menu'
});
// Listen for global events (emitted by menu) and map to actions
useEventActionMap(this, {
eventPrefix: 'menu',
actions: {
opened: this.onMenuOpened,
closed: this.onMenuClosed,
},
});
onMenuOpened( /* Menu state changed */ ) {}
onMenuClosed( /* Menu state changed */ ) {}
open(e) {
e?.preventDefault();
this.dispatch('open'); // useEventActionMap of the menu controller will map this event to the controller action
}
close(e) { /* logic */ }
toggle(e) { /* logic */ }
} I'm curious about this approach so any comment is much appreciated! NOTE: of course the eventPrefix could be passed as a value to handle multiple menu instances |
@gremo It looks pretty interesting. Mind to share your useEventActionMap source? |
If the |
@gremo have you had a chance to see messages above? |
I'm sorry @pySilver I switched to a more "standard way" of doing the same thing, that is using |
@gremo thanks for your reply! tbh, I'm also using standard way & some event flavoring at the moment. I was just curious your approach. |
Oh I see. The approach was in fact quite simple, just a matter of looping on actions, setup listeners for calling the corresponding controller method 😄 |
For those coming to this via Google, Stimulus now has built-in support of cross controller communication via events. |
It would also be worth add that it has Outlets mechanism https://stimulus.hotwired.dev/reference/outlets |
I know this isn't exactly an issue, but I think it is a good question. I think of your controllers as controllers for components on a page.
How would you suggest to communicate between more controllers?
I have two components
List
(data rendered in a list) andListFilter
(bunch of selects and text inputs). List is loaded using AJAX. It should be reloaded every timeListFilter
changes. I would like to use separate controllers forList
andListFilter
to keep files small and organized.So, how to trigger a
List
reload whenListFilter
changes?The text was updated successfully, but these errors were encountered: