-
Notifications
You must be signed in to change notification settings - Fork 424
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
Outlets API #576
Outlets API #576
Conversation
This is brilliant! I’ve had the need for this on multiple occasions and ended up with really hacky solutions to make it work. |
This looks great! It's something that I felt the need to use before. Is there a reason we can't just get the outlet using a -<div class="pagination" data-controller="pagination">
+<div data-list-outlet="pagination" data-controller="pagination" class="hidden">
<!-- ... -->
</div>
-<div data-controller="list" data-list-pagination-outlet=".pagination">
+<div data-controller="list">
<!-- ... -->
</div> // list_controller.js
export default class extends Controller {
static outlets = [ "pagination" ]
connect () {
this.paginationOutlet.classList.remove('hidden')
}
} This way, you don't have to think about "what selector should I place here" and also avoid the possibility of having clashing selectors (multiple TBH, I think this behavior can be achieved by enabling targets to be found outside the controller container. |
Really nice idea - in some of my POC work I found that there are cases where we do want to be more explicit about some controller causing an 'output' to change in another container. |
I really like the idea of using the With that in mind, one benefit of using ad hoc CSS selectors is that multiple occurrences of the same controller in different parts of the document can include or exclude outlets in the absence of descendant scoping. For example, controllers decide which @marcoroth @adrianthedev Most of the potential use cases for Outlets I've encountered are implicitly singleton controllers (only one occurrence on the page). Have you considered situations where multiple controllers using outlets (with potentially conflicting scopes) co-exist in a document? |
Could a controller set its own scope for all its outlets, instead of per outlet? Maybe something like an attribute that's a CSS selector or |
One use case for the selector approach - We have a search form that the user can type in and it Async requests search results, we won't be using Turbo only Stimulus. This search field mostly appears in the header and we cannot easily make the results list container be in the scope of the form. The outlets approach is perfect for this scenario. However, we realised (POC code only) that the search form can also appear in some modals. This means we have to pass in an ID (or some way to select the right container) so that the search results did not appear in all results list containers. In saying that - there are always other ways to do this and maybe the selector approach could be optional? |
@adrianthedev I like the idea of having the attribute as The reason why the outlet declaration is on the "parent" controller element is because this is where you usually have the control to define which outlets you want to have on this specific controller instance. It's hard to put the declaration on the "children" controller elements because they have no idea if, where and how they are being used as outlets. Which would also lead to the problem, that you kinda have a two way relationship, where they children need to know about their parents, and the parents need to know about their children, which is not ideal. Let's say you have the // item_controller.js
export default class extends Controller {
}
// list_controller.js
export default class extends Controller {
static outlets = [ "item"]
} With the declaration on the <div id="list_1" data-controller="list" data-list-item-outlet=".list_1.item"></div>
<div id="list_2" data-controller="list" data-list-item-outlet=".list_2.item"></div>
<div class="list_1 item" data-controller="item"></div>
<div class="list_1 item" data-controller="item"></div>
<div class="list_2 item" data-controller="item"></div>
<div class="list_2 item" data-controller="item"></div> But also, since the I think the most compelling argument to put them on the parent controller element is because you could also be dealing with markup that you don't have any control over. Two situation come to mind: a) the markup might be coming from another partial/view/component or b) the markup might not be 100% related with the thing you are building the controller for (think like an |
I understand the situation where you can't manipulate the DOM and you want to select that part of the DOM. But how is that different from writing a I mean, as I see it, the value this feature brings is that it wraps the whole |
@adrianthedev I mean, yeah that's the basic idea. It's basically an abstraction on top of You could implement the idea of outlets today in your controller with something like: // list_controller.s
export default class extends Controller {
static values = {
itemClass: String
}
connect() {
this.itemOutlets.forEach(outlet => ...)
}
get itemOutletElements() {
return document.querySelectorAll(this.itemClassValue)
}
get itemOutlets() {
return Array.from(this.itemOutletElements).map(element => this.application.getControllerForElementAndIdentifier(element, "item")
}
} |
The approach using the `SelectorObserver` is much more reliable to fire the outlet connected and disconnected callbacks. It's now matching against the selector provided in the `data-[controller]-[outlet]-outlet` attribute instead of just looking for elements which have a `data-controller` attribute to appear. Previously, when a `data-controller` appeared we checked it's attribute value to see if it was relevant as an outlet for the current controller. Using the `SelectorObserver` we now solve an edge case where the user would dynamically add/remove an attribute to the outlet element which would then make that element relevant or not relevant anymore as an outlet for the current controller. We need to know that so that we can reliably fire the outlet callbacks. Previously I wouldn't have fired the callbacks. For every outlet we define we now create a separate instance of the `SelectorObserver` which also handles that we just match the relevant outlets by adding the `data-controller~=[outletName]` selector to the CSS selector we lookup.
OUTLET is a really desirable feature. We would like to have the ability to add alias to an outlet. This is because stimulus recommends a rule to convert static outlets = ['hoge-Foo']; // -x to X
someMethod() {
this['hoge-Foo'].xxx();
} I'd be happy if it could be put like this. static outlets = {'hoge--foo': 'hogeFoo'};
someMethod() {
this.hogeFooOutlet.xxx();
}
``` |
Hey @NakajimaTakuya, good remark, thank you! I added commit 305e293 which should add support for namespaced controllers. So for your static outlets = [ 'hoge--foo' ]
someMethod() {
this.hogeFooOutlet.xxx();
}
hogeFooOutletConnected(outlet, element) {
}
hogeFooOutletDisconnected(outlet, element) {
} and like this in the HTML: <div data-controller="list" data-list-hoge--foo-outlet=".hoge--foos">...</div> I know that the outlet callbacks would clash if you would add another outlet for a controller The HTML attributes don't clash, it would be <div data-controller="list" data-list-hoge-foo-outlet=".hoge-foos">...</div> But if you really need to differentiate the outlet callbacks between the a hogeFooOutletConnected(outlet, element) {
if (outlet.identifier === "hoge--foo") {
// do something in the "hoge--foo" case
}
if (outlet.identifier === "hoge-foo") {
// do something in the "hoge-foo" case
}
} or something like this for the outlets: this.hogeFooOutlets.filter(o => o.identifier === "hoge--foo") |
@marcoroth However, I also realized that my original proposal had some problems. static outlets = ['foo', {name: 'hoge-foo', alias: 'hogeFoo'}, 'piyo']; If you still feel that it is better to use only the name conversion rules to solve this problem, that is fine. |
This is just the case if you have
That would technically be possible, but I feel like that makes the wiring even more confusing if you can define aliases. Stimulus is designed around conventions and it feels weird to me to break that. Maybe I don't understand the context. Can you elaborate why you would want to define aliases? |
…rrent instance gets connected There was an edge case where outlets wouldn't fire the outlet callbacks if they appeared later in the DOM but the current instance had a "dependency" on them. To solve this the OutletObserver now "notifies" it's dependents that it conencted so they can refire the matching elements via the `refresh()` function of the ElementObserver.
@marcoroth Love this. Excellent API, great implementation. Could you have a look at |
@dhh looks like this is related to the recent merge. Will resolve 👍🏼 |
Could you do a doc PR to go along with this? |
Docs are coming via #604 |
How could we use this feature to open a |
@seb-jean - have not tested locally but it would be something like this, however, you could solve this with event dispatching also. Outlets approachThis should hopefully be pretty close to how you can do the modal controller interaction with outlets. @marcoroth - any thoughts? // controllers/modal_trigger_controller.js
class ModalTriggerController extends Controller {
static outlets = [ "modal" ]
trigger() {
this.resultOutlets.forEach(controller => {
controller.show();
});
}
}
export default ModalTriggerController; // controllers/modal_controller.js
// https://getbootstrap.com/docs/5.2/components/modal/#via-javascript as example
class ModalController extends Controller {
static targets = ['close'];
show() {
this._modal = this._modal || new bootstrap.Modal();
this._modal.show();
// rough code for example only - may not work
}
}
export default ModalController; <main>
<button type="button" data-controller="modal-trigger" data-action="modal-trigger#trigger" data-modal-trigger-modal-outlet="#my-modal">Open modal</button>
</main>
<div class="modal" tabindex="-1" data-controller="modal" id="my-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" data-modal-target="close">X</button>
</div>
<div class="modal-body"><p>Modal body text goes here.</p></div>
</div>
</div>
</div> Non-outlets approachThis uses event dispatching but also leverages a generic 'go' (aka trigger something) controller to attach the behaviour to the button. This leverages the action parameters to move the 'selector' and 'event name' back into the DOM - https://stimulus.hotwired.dev/reference/actions#action-parameters The nice approach here is that there is no digging into the controller implementation and we have an almost identical DOM API with the same power. Outlets is going to be super helpful for some cases but I do think leveraging DOM events can get you pretty far. // controllers/dispatch_controller.js
// does not have to be this 'generic' but may be a useful tool
class DispatchController extends Controller {
go({ params: { eventName, targetSelector } }) {
if (targetSelector) {
document.querySelectorAll(targetSelector).forEach(target => {
this.dispatch(eventName, { prefix: '', bubbles: false, cancelable: false, target });
});
return;
}
this.dispatch(eventName, { prefix: '', bubbles: true, cancelable: false });
}
}
export default DispatchController; // controllers/modal_controller.js
// https://getbootstrap.com/docs/5.2/components/modal/#via-javascript as example
class ModalController extends Controller {
static targets = ['close'];
show() {
this._modal = this._modal || new bootstrap.Modal();
this._modal.show();
// rough code for example only - may not work
}
}
export default ModalController; <main>
<button
type="button"
data-controller="dispatch"
data-action="dispatch#go"
data-dispatch-event-name-param="modal:show"
data-dispatch-target-selector-param="#my-modal"
>
Open modal
</button>
</main>
<div class="modal" tabindex="-1" data-controller="modal" id="my-modal" data-action="modal:show->modal#show">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal title</h5>
<button type="button" data-modal-target="close">X</button>
</div>
<div class="modal-body"><p>Modal body text goes here.</p></div>
</div>
</div>
</div> |
@marcoroth I've had an opportunity to experiment with outlets in the context of a disclosure button that controls a native The code resembled something like: <script type="module">
import { Application, Controller } from "@hotwired/stimulus"
const application = Application.start()
application.register("dialog", class extends Controller {
showModal() {
this.element.showModal()
}
})
application.register("disclosure", class extends Controller {
static outlets = ["dialog"]
dialogOutletConnected(controller, dialog) {
this.element.setAttribute("aria-controls", dialog.id)
this.element.setAttribute("aria-expanded", dialog.open)
dialog.addEventListener("close", this.#setCollapsed)
}
dialogOutletDisconnected(controller, dialog) {
this.element.removeAttribute("aria-controls")
this.element.removeAttribute("aria-expanded")
dialog.removeEventListener("close", this.#setCollapsed)
}
expand() {
for (const dialogOutlet of this.dialogOutlets) {
dialogOutlet.showModal()
this.#setExpanded()
}
}
#setCollapsed = () => this.element.setAttribute("aria-expanded", false)
#setExpanded = () => this.element.setAttribute("aria-expanded", true)
})
</script>
<dialog id="dialog" data-controller="dialog">
Hello, from a dialog element!
</dialog>
<button data-controller="disclosure"
data-disclosure-dialog-outlet="#dialog">
Toggle #dialog
</button> This feels like a huge improvement over prior art! Two things that stick out to me as opportunities for improvement are the I wonder if there's an opportunity to expand existing support for Action Descriptor syntax for global events ( Maybe something like: dialogOutletConnected(controller, dialog) {
this.element.setAttribute("aria-controls", dialog.id)
this.element.setAttribute("aria-expanded", dialog.open)
- dialog.addEventListener("close", this.#setCollapsed)
}
dialogOutletDisconnected(controller, dialog) {
this.element.removeAttribute("aria-controls")
this.element.removeAttribute("aria-expanded")
- dialog.removeEventListener("close", this.#setCollapsed)
}
+close() {
+ this.#setCollapsed()
+} Then, we could change the <button data-controller="disclosure"
- data-disclosure-dialog-outlet="#dialog">
+ data-disclosure-dialog-outlet="#dialog"
+ data-action="close@dialog->disclosure#setCollapsed">
Toggle #dialog
</button> The At the moment, the only global event symbols we support are I wonder if Assuming that's possible for Stimulus to support, does that syntax feel intuitive enough? Are there other potential collisions we need to worry about? |
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [showModal][] method. Consider the following `disclosure` controller implementation: ```js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [showModal]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [showModal][] method. Consider the following `disclosure` controller implementation: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [showModal]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
The original [idea][] for this change was outlined in a comment on [hotwired#576]. The problem --- Prior to this commit, any Outlet-powered references would need to manage event listeners from within the Stimulus Controller JavaScript. For example, consider the following HTML: ```html <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" data-action="click->disclosure#expand"> Open dialog </button> ``` Clicking the `button[type="button"]` opens the `dialog` element by calling its [HTMLDialogElement.showModal()][] method. Consider the following `element` and `disclosure` controller implementations: ```js // element_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { showModal() { this.element.showModal() } } // disclosure_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static outlets = ["element"] elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) element.addEventListener("close", this.collapse) } elementOutletDisconnected() { element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } collapse = () => { this.element.setAttribute("aria-expanded", false) this.element.focus() } expand() { for (const elementOutlet of this.elementOutlets) { elementOutlet.showModal() this.element.setAttribute("aria-expanded", elementOutlet.element.open) } } } ``` Note the mirrored calls to add and remove [close][] event listeners. Whenever the `dialog` element closes, it'll dispatch a `close` event, which the `disclosure` controller will want to respond to. Attaching and removing event listeners whenever an element connects or disconnects is one of Stimulus's core capabilities, and declaring event listeners as part of `[data-action]` is idiomatic. In spite of those facts, the `disclosure` controller is responsible for the tedium of managing its own event listeners. The proposal --- To push those declarations out of the JavaScript and back into the HTML, this commit extends the Action Descriptor syntax to support declaring actions with `@`-prefixed controller identifiers, in the same way that `window` and `document` are special-cased. With that support, the HTML changes: ```diff <dialog id="dialog" data-controller="element"> <span>This dialog is managed through a disclosure button powered by an Outlet.</span> <form method="dialog"> <button>Close</button> </form> </dialog> <button type="button" data-controller="disclosure" data-disclosure-element-outlet="#dialog" - data-action="click->disclosure#expand"> + data-action="click->disclosure#expand close@element->disclosure#collapse"> Open dialog </button> ``` And our `disclosure` controller has fewer responsibilities, and doesn't need to special-case the `collapse` function's binding: ```diff elementOutletConnected(controller, element) { this.element.setAttribute("aria-controls", element.id) this.element.setAttribute("aria-expanded", element.open) - element.addEventListener("close", this.collapse) } elementOutletDisconnected() { - element.removeEventListener("close", this.collapse) this.element.removeAttribute("aria-controls") this.element.removeAttribute("aria-expanded") } - collapse = () => { + collapse() { this.element.setAttribute("aria-expanded", false) this.element.focus() } ``` Risks --- Changing the action descriptor syntax has more long-term maintenance risks that other implementation changes. If we "spend" the syntax on this type of support, we're pretty stuck with it. Similarly, existing support for `window` and `document` as special symbols means that we'd need to make special considerations (or warnings) to support applications with `window`- and `document`-identified controllers. [hotwired#576]: hotwired#576 [idea]: hotwired#576 (comment) [HTMLDialogElement.showModal()]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal [close]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event
This pull request introduces a new Outlets API to Stimulus. Heavily influenced by HEY's Stimulus Outlet Properties.
Outlets
The concept of outlets is very similar to Stimulus Targets. While a target is a specifically marked element within the scope of the controller element, an outlet is a reference to one or many other Stimulus Controller instances on the same page.
The important difference is that outlets don't necessarily have to be within the scope of the controller element, as they can be anywhere on the page.
Outlet declaration
The Outlets API adds support for a static
outlets
array on controllers. This array declares which other controller identifiers can be used as outlets on this controller:Attributes and selectors
In order to declare a controller instance as an outlet on the "host" controller you need the add a data-attribute to the host controller element in the form of:
If you try to declare an element as an outlet which doesn't have the corresponding
data-controller
element on it, Stimulus will throw an exception:The selector can be any valid CSS selector.
Controller properties
Stimulus automatically generates five properties for every outlet identifier in the array:
has[Name]Outlet
Boolean
name
outlet[name]Outlet
Controller
Controller
instance of the firstname
outlet or throws an exception if none is present[name]Outlets
Array<Controller>
Controller
instances of allname
outlets[name]OutletElement
Element
Element
of the firstname
outlet or throws an exception if none is present[name]OutletElements
Array<Element>
Element
's of allname
outletsAccessing controllers and elements
Since you get back a
Controller
instance from the properties you are also able to access the Values, Classes, Targets and all of the other properties and functions that controller instance defines, for example:You are also able to invoke any of the functions the controller defines.
Similarly with the Outlet Element:
Outlet callbacks
Outlet callbacks are specially named functions called by Stimulus to let you respond to whenever an outlet is added or removed.
To observe outlet changes, define a function named
[name]OutletConnected()
or[name]OutletDisconnected()
.Outlets are assumed to be present
When you access an outlet property in a controller, you assert that at least one corresponding outlet is present. If the declaration is missing are no matching outlet is found Stimulus will throw an exception:
Optional outlets
If an outlet is optional, you must first check if an outlet is present using the existential property:
Resolves #35 and Resolves #552.
Feedback and any other ideas are very welcome!