Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: polymorphic outlets #628

Open
sfnelson opened this issue Dec 19, 2022 · 4 comments
Open

Feature request: polymorphic outlets #628

sfnelson opened this issue Dec 19, 2022 · 4 comments

Comments

@sfnelson
Copy link

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 have data-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:

class TriggerController extends Controller {
  static outlets = ["controlled"]
  toggle() {
    this.controlledOutlet.toggle();
  }
}

Targets:

// menu, or modal, or accordion, etc
class MenuController extends Controller {
  toggle() {
    ...
  }
}

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.

@seanpdoyle
Copy link
Contributor

Thank you for opening this issue!

We might use the source for a sliding menu, a modal+scrim, a menu, etc – anywhere that aria-controls might be appropriate.

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.

@sfnelson
Copy link
Author

@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.

@ianterrell
Copy link

ianterrell commented Jan 4, 2023

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.

@ianterrell
Copy link

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 document, but it can easily be made to search within a single element for scoped use cases.

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

No branches or pull requests

3 participants