-
Notifications
You must be signed in to change notification settings - Fork 426
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: polymorphic outlets #628
Comments
Thank you for opening this issue!
You might find #627 to be interesting. I'd appreciate feedback as far as developer ergonomics goes. I'm also curious how it compares with the Outlet mental model, and how the two might coexist. |
@seanpdoyle that looks like exactly what we're trying to do! We'll give it a try, unfortunately unlikely to be before the new year. |
I found this issue when looking to do something similar. In my case, rather than a one-to-one polymorphic relationship I wanted a heterogenous collection of controller outlets. This use case felt similar enough that I thought I would add a note here. I didn't see how references would exactly work for what I wanted, and so I worked up a solution with a bridging controller, which I think would also work in cases with a one-to-one polymorphic relationship. To concretize the use case, we're building a survey or form building system and a given survey page may have questions of different types (multiple choice, text entry, etc). Each type of question has custom logic, but I sometimes want to be able to perform actions or queries on all questions as a collection. For example, before submission I want to check if any were left blank; the specific logic for what "left blank" means depends on both the question type and how that instance is configured. For my particular use case I worked a solution with a bridging controller. Controller that wants to check on the set of others: export default class extends Controller {
static outlets = [ "bridge" ]
checkIfLeftBlank(event) {
event.preventDefault()
if (this.bridgeOutlets.some(outlet => outlet.leftBlank())) {
// ...
}
}
} Bridging controller: export default class extends Controller {
static values = {
controller: String,
}
controllerValueChanged() {
if (this.controllerValue.length === 0) { return }
if (this.bridgedController) { return }
this.bridgedController = this.application.getControllerForElementAndIdentifier(this.element, this.controllerValue)
}
leftBlank() {
return this.bridgedController.leftBlank()
}
} Specific controller: export default class extends Controller {
static targets = [ "input" ]
connect() {
this.element.dataset.bridgeControllerValue = "specific-controller-type"
}
leftBlank() {
return // custom logic per type
}
// ...
} With HTML something like this: <div class="question" data-controller="bridge specific-controller-type">
<input data-specific-controller-type-target="input" type="text" />
</div>
<div data-controller="other-controller" data-global-bridge-outlet=".question">
<a href="#" data-action="other-controller#checkIfLeftBlank">Check if blank</a>
</div> There are some flaws with this approach, especially potentially using value change callbacks to coordinate initialization between controllers. But I hope it helps if someone else needs to do something similar. |
I've shifted to another approach for heterogenous collections of controllers, which I'll note here in case it's helpful for anyone: export async function dataIdentifiedControllers(application, identifier) {
const elements = Array.from(document.querySelectorAll(`[data-${identifier}]`))
const promises = elements.map(e => {
return new Promise(resolve => {
const findController = () => {
const controller = application.getControllerForElementAndIdentifier(e, e.dataset[identifier])
controller ? resolve(controller) : setTimeout(findController)
}
findController()
})
})
return await Promise.all(promises)
} With this we can write controllers like: <div
data-controller="mix-and-match--stimulus-controller--identifiers"
data-sortof-outlet-identifier="mix-and-match--stimulus-controller--identifiers"
> and then access a collection of them from within a Stimulus controller like const heterogenousCollection =
await dataIdentifiedControllers(this.application, 'sortof-outlet-identifier') The helper method above uses |
Outlets require that the target controller name corresponds to the outlet name, e.g. if I write
data-source-target-outlet
then my target element must havedata-controller="target"
.This makes sense as a technical requirement, e.g. if there are multiple controllers registered on the target then the source must be able to discriminate between them to choose the controller to receive messages.
The technical constraint appears to us to be preventing object oriented polymorphism, i.e. we must statically know the name of the controller in order to send it messages, and it gets baked into the code for the source controller.
An example of where polymorphism would be useful is a generic "trigger" controller, ala
aria-controls
that triggers a change in state for a modal/menu controller.Source:
Targets:
We might use the source for a sliding menu, a modal+scrim, a menu, etc – anywhere that
aria-controls
might be appropriate.We have previously solved this problem using events, but there's quite a bit of boilerplate to this approach and were hoping that outlets could simplify this approach.
The text was updated successfully, but these errors were encountered: