Skip to content
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

Feature request - Event when Stimulus connects to an element #701

Open
lb- opened this issue Jul 8, 2023 · 5 comments
Open

Feature request - Event when Stimulus connects to an element #701

lb- opened this issue Jul 8, 2023 · 5 comments

Comments

@lb-
Copy link
Contributor

lb- commented Jul 8, 2023

Problem

I find myself writing, in various forms, controller code that allows some action to be dispatched when a controller connects.

This method could be manually called in the connect method, but sometimes it's more appropriate to have this call made optionally depending on usage. We could add a value to 'do thing on init', but writing an additional action feels more appropriate and makes more sense.

Example

As its simplest, the code often looks like this...

export class extends Controller {
  connect() {
    // Do some kind of setup 
    this.dispatch('start', { bubbles: false, cancelable: false });
  }
}
<div data-controller="my-controller" data-action="my-controller:start->my-controller#doSomething:once">
  ...STUFF
</div>

Proposal

Every controller, when connected would dispatch two events on the controlled element by default. These two events would bubble, are not cancelable and their prefix can be configured using the Stimulus schema.

  • init:my-controller - dispatched before the connect method callback is called. Less critical.
  • ready:my-controller - dispatched after the connect method has completed. If the connect method is Async /returns a promise it will only dispatch when that has resolved successfully.

Note: Originally, I had suggested these events should not bubble but it may make sense to have this bubble and allow them to be restricted by the self event filter.

This also sets up a way to do a global event listener, per controller identifier, to hide something until ready. E.g. a data attribute or class that gets removed when the ready event is dispatched.

Example new usage

We no longer need the overhead in our controller...

<div data-controller="my-controller" data-action="ready:my-controller->my-controller#doSomething:self:once">
  ...STUFF
</div>

Additional references

@marcoroth
Copy link
Member

marcoroth commented Jul 12, 2023

I was looking into this the other day and I agree that there are some good use-cases for this. One of them was when looking at #698 as you already pointed out.

But I can also see those events to be helpful for proceeding with #687 / #690. In that case I'd even argue that a controller:load (or similar) event would be useful, so you can do some stuff after a certain controller was fetched async.

@adrienpoly
Copy link
Member

Thinking about edge cases here : what would be the desired behaviour when there are multiple controller instance on a single page ?

do we fire multiple events? Or one event when all controllers are loaded?

also for the naming, I would suggested initialised/connected

@lb-
Copy link
Contributor Author

lb- commented Jul 15, 2023

@adrienpoly for the multiple controller instances, I would envisage that for each controller's connection there would be a different event fired. If you wanted you could just addEventListener(...{once: true}) to ensure your code runs whenever the first controller connects.

However, based on the above comments, here is a revised proposal.

Lifecycle events

A new feature in Stimulus that can be used to automatically trigger custom DOM events based on key application and controller lifecycle methods.

Initially, this could start very simple but be built out to potentially cover more use cases but here is a rough example documentation.

The Stimulus application schema would be updated with a new inner object, events. Additionally, there will be a new schema entry, application name, which could be used for logging but will be used initially as the event name prefix for application lifecycle events.

The default of which would be structured as follows;

name: 'application', // or Stimulus?
events: {
  // application lifecycle events
  startBefore { auto: true, name: 'starting' }, // called beforethe application instance has started, prefixed with the schema's application name (e.g. `'application:starting'`), dispatched on `window` with the application instance accessible in the event's detail, can be cancelled?
  startAfter: { auto: true, name: 'started' }, // called after the application instance has started, prefixed with the schema's application name (e.g. `'application:started'`), dispatched on `window` with the application instance accessible in the event's detail.
  stopAfter: { auto: true, name: 'stopped' }, // called after the application instance has stopped, prefixed with the schema's application name (e.g. `'application:stop'`), dispatches on `window`.
  // controller registration lifecycle events
  shouldLoad: { auto: false, name: 'load' }, // cancellable, if event.preventDefault is called, behaves similar to the `shouldLoad` static method on a controller returning false, prefixed with the controller's identifier and has access to the `controllerConstructor` in the event detail.
  loadBefore: { auto: false, name: 'registering' }, // when a controller is about to be registered, can be cancelled? dispatches on `document`, event detail will have access to the `controllerConstructor`, prefixed with the controller identifier. Note. Maybe this and the above shouldLoad event can be merged into one. An event before any registration, even before shouldLoad is called & can be cancelled.
  loadAfter: { auto: false, name: 'registered' }, // after the controller has been registered and after `afterLoad` has resolved/completed, dispatches on `document`, event detail will have access to the `controllerConstructor`, prefixed with the controller identifier.
  // controller instance lifecycle events
  connectBefore: { auto: false, name: 'connecting' }, // before the connect method on a controller instance is called, dispatches on the controlled element, prefixed with the controller's identifier.
  connectAfter: { auto: false, name: 'connected' }, // after the connect method on a controller instance has resolved/completed, dispatches on the controlled element, prefixed with the controller's identifier.
  disconnectBefore: { auto: false, name: 'disconnecting' }, // before the disconnect method on a controller instance is called, prefixed with the controller's identifier.
  disconnectAfter: { auto: false, name: 'disconnected' }, // after the disconnect method has resolved/completed, dispatches on `document`, prefixed with the controller's identifier.
  // controller instance target lifecycle events
  // maybe one day - YAGNI for now (targetConnectBefore, targetConnectAfter, detail contains the target name, dispatched on the first target element that is part of that microtask).
}

By default, only the application start/stop events will be fired, this will be mostly opt-in, but this could change in a future release. Additionally, the ability to cancel some events could be brought into later releases if needed.

This provides a way to set the auto on a per lifecycle event basis, when this is true it will mean that the event will be fired by default. This also paves the way for future event based behaviour such as registering a controller via an event dispatching and the application has its own listeners.

The event names cannot be altered any other way aside from the schema but additional opt-in/out can be set up at the Controller level.

Controller events static property

The below controller will only dispatch the connectBefore event, irrespective of the auto values in the schema. This means that loadBefore/After, disconnectBefore/After and connectAfter would not be dispatched.

class MyController extends Controller {
  static events = ['connectBefore'];
}

This API could be refined maybe with a !connectBefore type syntax in the future, or possibly with the ability to filter from another static value on the base Controller.

The below controller for example would dispatch ALL events except connectBefore.

class MyController extends Controller {
  static events = Controller.events.filter(name => name !== 'connectBefore');
}

Controller instance method return values

Another way to limit/adjust the behaviour per instance for the controllers could be a return value check for connnect, disconnect, afterLoad and maybe others. If there is a return value (e.g. not undefined) and the value is false then the default auto behaviour of that relevant after lifecyle event will be overridden.

In this controller below, if the connect method returns false, the my-controller:connected event will not dispatch.

class MyController extends Controller {
  connect() {
    // do stuff
    if (!somethingRelevant) return false;
  }
}

This, for simplicity, is a purely opt-out approach but could be further refined but the idea is that you can set things at the schema level. Then conditionally opt out as you go further into the parts of the application but again the naming stays at the schema level.

Next steps

I think this is quite a grand plan but if this is useful we should start small, maybe only set up the bare minimum connected events and application start/stop events or something like that. A PR would involve the set up of the schema and core approach in a way that it can be scaled. Even the opt in/out at the Controller events property and return values could be added at a later date.

Let me know if this makes sense and I can see if I can get an initial implementation PR up.

@lb-
Copy link
Contributor Author

lb- commented Jul 21, 2023

A potential better name for this could be signals, this way we don't confuse general events and Stimulus usage of events to signal key things have happened.

  • Actions - Listening to events to call controller methods by adding data attributes.
  • Signals - Dispatching of events before and after controller lifecycle methods are run.

It may also be simpler to avoid the whole part of the schema setting the default opt in stuff. Instead, each controller just opts in, the event names would still be declared in the application schema.

This aligns with the idea of Controllers being easily isolated/reused.

@MatTheCat
Copy link

MatTheCat commented Sep 7, 2024

Found about this issue because when doing e2e tests with lazy controllers you have to wait for them to be loaded.

I would have added such controllers’ element an HTML attribute on connect so that the test client can wait for this attribute before clicking on a button e.g.

For now it doesn’t seem like it is possible to do it in a decoupled way: you have to update every concerned controller.

From what I understand the feature requested here would allow for doing this by adding a single listener.

Is it still considered?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

4 participants