-
-
Notifications
You must be signed in to change notification settings - Fork 158
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
Start a section about writing Stimulus controllers for the back end #1519
base: main
Are you sure you want to change the base?
Conversation
0b8048f
to
5fc12e4
Compare
5fc12e4
to
29baa7f
Compare
title: "Turbo-compatible Stimulus controllers" | ||
description: "Howto write Stimulus controllers for a Turbo application (that don't suck)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
title: "Turbo-compatible Stimulus controllers" | |
description: "Howto write Stimulus controllers for a Turbo application (that don't suck)" | |
title: "Turbo-compatible Stimulus controllers" | |
description: "How to write Stimulus controllers for a Turbo application (that don't suck)" |
Not sure we should use the wording "don't suck" in the official documentation ;)
// Run once, when the current controller instance is created | ||
initialize() { | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be helpful to elaborate on this a bit. i.e. describe the difference between initialize()
and connect()
a bit more. Because if you have data-controller="foobar"
on multiple elements, initialize()
will be called multiple times as well, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it is basically:
foo = new FooController --> foo.initialize()
<foo> element found in DOM --> foo.connect()
<foo> element removed from DOM --> foo.disconnect()
<foo> element readded to DOM --> foo.connect() (on same fooInstance!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but what happens during a turbo:render
? Will initialize()
be called for every new element again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initialize()
is run right before the first connect on each newly found element. When an element is removed from DOM (but a reference is being kept) and then it is re-added, the same controller instance will be used. So in this case, there would be a connect()
call but no initialize()
call.
But as an implementer I think you should not care about this: initialize()
is basically your constructor, where you can set up things for the object instance. When Turbo serves a page from cache, the instances are probably still alive, but this is an implementation detail of the cache.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is the source code: https://github.com/hotwired/stimulus/blob/main/src/core/context.ts#L33 where you can see, that initialize()
is just invoked inside the constructor()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but that's the thing. With Turbo I cannot wrap my head around when initialize()
will be called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you even bother? You should not alter the DOM or anything in there, it's just boring setup like in any constructor. 🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still do not understand 😁
In the realm of Turbo and Stimulus, when is this constructor called?
I know in PHP for instance that a constructor of a service will only be called once per request. But if I understand this correctly, initialize()
would not behave the same way, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A service is probably not the best analogy, because there you typically only have one instance of a kind/class, whereas in Stimulus each DOM element with a data-controller="…"
property will be associated with its own instance.
Behind the scenes it kind of works like this:
- the mutation observer detects a new controller DOM element
e
- look up, if
e
is known in the weak map for this controller type (DOM element -> controller instance)- if no: create a new instance (this will invoke
initialize()
on the newly created instance; see here) and use this - if yes: take the instance from the map
- if no: create a new instance (this will invoke
- invoke
connect()
So if your element was removed from the DOM and readded (for example by a script reorganizing things, and calling element.remove()
and parent.appendChild()
), you would not need to reinitialize things - event listeners would be still intact and so on. If however the reference is lost (e.g. due to Turbo creating a snapshot with clonedElement = element.clone(true)
which is later appended to the DOM), then there is no possible connection between the DOM string <div … data-controller="…">
and the instance anymore. So if nobody is holding any reference anymore it will be garbage collected. And on the Stimulus side you would also see a initialize()
call on the next connect (because new instance).
But in practice I think you should never ever rely on how often or if this is happening. It is merely an optimization: reusing things where possible saves resources. So as a guideline I would say:
- Expect
connect()
to be called multiple times on this instance. - If you need to setup things during
connect()
that are then stored to the instance or the DOM element (= that are still valid the next time this DOM element reference is "connected"), put it ininitialize()
.
|
||
<div data-controller="add-foo"></div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<div data-controller="add-foo"></div> | |
<div data-controller="add-foo"></div> |
|
||
<div data-controller="add-foo"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<div data-controller="add-foo"> | |
<div data-controller="add-foo"> |
|
||
<div data-controller="add-foo"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<div data-controller="add-foo"> | |
<div data-controller="add-foo"> |
If you need to make changes to the DOM make sure these are idempotent. That means, applying the code multiple times does | ||
not do any harm. Why is this important? Because cache entries are made before the DOM gets removed, so any cleanup in a | ||
`disconnect()` method has no effect. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am a bit confused here. You are saying that any cleanup in a disconnect()
method has no effect, but further down in 2) you are also saying:
Cleanup CSS classes on parent elements, created sibling elements, etc.
So shouldn't it be enough to delete and created sibling elements in the disconnect()
method rather than having to rely on the turbo:before-cache
event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The order for a page transition with caching is beforeCache
, then disconnect
. So from the perspective of the snapshot the disconnect has no effect.
The disconnect
is still crucial for when the DOM/parts of it get exchanged. Memory cleanup, removing parent classes etc. This will happen without beforeCache
calls and even on the cached snapshot when it is displayed as a preview before the actual content has arrived.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically: beforeCache
must shape the DOM, so that a connect
can run on it, disconnect
must restore the DOM to its original state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to improve the text, if we can make this more clear.
No description provided.