stimulus: Outlets not available in connect/disconnect

When I try to test Outlets using the example from the docs, I get the following error:

The provided outlet element is missing the outlet controller "result" for "search" 
<div class="result" data-controller="result">

I’ve prepared a JSFiddle to demonstrate. Here’s the full code

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Outlets tehst</title>
  <script type="module">
    import { Application, Controller } from 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js'

    const app = Application.start()
    app.debug = true;
    
    app.register('search', class extends Controller {
      static outlets = ['result']

      connect() {
        console.log(this.resultOutlets)
        this.element.insertAdjacentHTML('beforeend', `. Found ${this.resultOutlets.length} outlets`);
      }
    });

    app.register('result', class extends Controller {});
  </script>
</head>

<body>
  <div
    data-controller="search"
    data-search-result-outlet=".result"
  >Search</div>
  
  <div>
    <div class="result" data-controller="result">Result</div>
    <div class="result" data-controller="result">Result</div>
  </div>
</body>
</html>

I’ve done a little debugging and found that Router.getContextForElementAndIdentifier finds a module with no contexts (empty Array), which quickly leads to the error above.

I’ve tried versions 3.2.0 and 3.2.1. Neither works.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 6
  • Comments: 22 (10 by maintainers)

Most upvoted comments

@dhh could you please create a new release which includes the fix for this issue? Thanks in advance.

I will check out what might be wrong here, thanks for opening this issue @drjayvee!

Thanks for pointing out the workaround.

I’d argue that this is a bug in the implementation. The values/targets/classes APIs don’t require waits, so this is hugely surprising.

The error message also seems totally buggy since its message complains about the controller missing, when the very message points out that the data-controller attribute is present and correct.

At the very least, the timing issue should be documented.

This seems to be a timing issue. I tried delaying it a bit and got it right. It may be a good idea to modify the sample code in the documentation. https://codepen.io/nazomikan/pen/zYayvNJ?editors=1010

@promisedlandt I’m open to update the error message if you have an idea on how to improve it.

I think the error message would be slightly improved by:

“Unable to find a controller when searching for the “result” outlet of the “search” controller. Ensure CSS selector “#result” is correct and target element has a data-controller attribute that includes “result”, e.g. “data-controller=result”.”

(Not sure how easy including the CSS selector woud be, it’s not particularily important)

Speaking to my original point: if you’re planning a release soon-ish without #648 I would suggest adding another senctence to the error message with an explicit reference to this issue , i.e. “If the target is correctly declared as the “result” controller, it might not be connected yet. See https://github.com/hotwired/stimulus/issues/618 for a workaround.”

Same thing is happening to us in the connect we have also encountered similar issue in disconnect when using Turbo Drive. Outlet can be disconnected before the controller which causes same error as above.

has[identifier]Outlet also returns true even though outlet isn’t there which makes it harder to check for the situation. [identifier]Outlets.length can be used as a workaround even though it shows warning with the same text as an error in the javascript console (at least in safari).

setTimeout workaround unfortunately didn’t work in our case when lazily loading several controllers via importmaps from individual javascript files. It can be hard to set delay properly if we don’t know when the outlet controller will be loaded.

Hey @rgarner, #648 fixed the underlying issue. If you want to give it a shot feel free to pull in the dev-build to double check if it also solves your use-case.

yarn add @hotwired/stimulus@https://github.com/hotwired/dev-builds/archive/@hotwired/stimulus/7bf453c.tar.gz

By wrapping codes in a setTimeout avoid this warning for me:

  messageOutletConnected(outlet, element) {
    // Run in next tick to wait message outlet controller ready
    setTimeout(() => {
      let lastAnswer = this.messageOutlets[this.messageOutlets.length - 1]
      this.isWaiting = lastAnswer.statusValue == "waiting"
      this.handleStatus()
    }, 0)
  }

III seems like the most robust

@drjayvee I hope you meant to say ii here 🙈 But I would agree with you in general.

In any case, I started to explore ii in #648, which looks promising so far.

@promisedlandt I’m open to update the error message if you have an idea on how to improve it.

Luckily, the root cause is fixed and already implemented with #648, but I haven’t had the time to finish it up yet. I’m looking to finish that up soon!

The problem is the order how Stimulus connects controllers. So this issue is dependent how the elements are found in the DOM and in which order the MutationObserver processes them.

So if you try to access the outlets in the connect() action of the “host controller” and the dependent outlets haven’t connected yet you will get the The provided outlet element is missing the outlet controller "result" for "search" error.

This won’t work:

<div data-controller="search" data-search-result-outlet=".result">Search</div>
<div class="result" data-controller="result">Result</div>
<div class="result" data-controller="result">Result</div>

However, this works as intended.

<div class="result" data-controller="result">Result</div>
<div class="result" data-controller="result">Result</div>
<div data-controller="search" data-search-result-outlet=".result">Search</div>

But in both scenarios you can access the outlets after the dependent outlet controllers have connected. Also the outlet callbacks work as expected.

There are few options how we can solve that you can access outlets in any case, independent of DOM order appearance:

    1. We try to scan all element instances on the page which have a data-controller attribute attached, build a “dependency tree”, sort the elements to be connected accordingly and then connect them in that order. This works if we disallow circular outlet dependencies.
    1. When the outlets are accessed and we can’t find them (this is where the error message appears now) we can try to find the element with the provided selector and see if it would have a matching data-controller attribute. If it does, we could connect that controller on-the-fly and enforce it that way. However, there is the downside that it might try to connect that controller again at a later point, because it was scheduled to be connected. But maybe we can also enhance the “connect logic” in that step to make sure there isn’t a connected controller already and just disregard the second connect attempt.
    1. We don’t allow outlets to be accessed in the connect(), but this feels like a weird limitation.
    1. We make the accessor for this.[name]Outlet and this.[name]Outlets async and make them return a promise which resolves as soon as the dependent controllers are connected. But this would probably lead to a breaking change.

I’ll investigate which option is the best solution here. Personally my vote would go for option ii. But happy to hear what others think.