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

disconnect() not called if outlet is accessed in connect() #763

Open
sebastianludwig opened this issue Apr 21, 2024 · 3 comments
Open

disconnect() not called if outlet is accessed in connect() #763

sebastianludwig opened this issue Apr 21, 2024 · 3 comments

Comments

@sebastianludwig
Copy link

I noticed that accessing an outlet in connect() causes the reference count of that controller to be 2 instead of 1 after start has finished. That means all actions which under normal circumstances cause the controller to be disconnected (like removing the data-controller attribute) don't cause the disconnect anymore.

The following code snippet illustrates the problem

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script type="module">
    import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
    window.Stimulus = Application.start()

    Stimulus.register("child", class extends Controller {
      connect() {
        console.log("Child connected")
      }
      disconnect() {
        console.log("Child disconnected")
        alert("works as expected")
      }
    })

    Stimulus.register("parent", class extends Controller {
      static outlets = [ "child" ]
  
      connect() {
        console.log("Parent connected")
        console.log(this.childOutlet) // <- works without this line 💥
      }
      disconnect() {
        console.log("Parent disconnected")
      }
      
      listChildren() {
        console.log(this.childOutlet)
      }
    });

    document.getElementById("remove").addEventListener("click", function() {
      console.log("Removing data-controller attribute");
      document.getElementById("child").setAttribute("data-controller", "");
    });
  </script>
</head>
<body>
  <div id="parent" data-controller="parent" data-parent-child-outlet="#child">
    <button data-action="click->parent#listChildren">List children</button>
  </div>
  <div id="child" data-controller="child"></div>
  <button id="remove">Remove data-controller=child attribute</button>
</body>
</html>

Setting a breakpoint in ScopeObserver.elementMatchedValue() and reloading the page you can see that it is hit twice for child. Clicking the remove button does not trigger disconnect() to be called. Everything works as expected with the marked line commented out.

@ildarkayumov
Copy link

Hi.

Looks like the problem occurs because of Outlet#get. So when you load outlet inside of parent controller there is no associated controller for child outlet element and stimulus tries to connect controller with element before show warning message The provided outlet element is missing an outlet controller...

function getControllerAndEnsureConnectedScope(controller: Controller, element: Element, outletName: string) {
  let outletController = getOutletController(controller, element, outletName)
  if (outletController) return outletController

  controller.application.router.proposeToConnectScopeForElementAndIdentifier(element, outletName)
  ...
}

https://github.com/hotwired/stimulus/blob/main/src/core/outlet_properties.ts#L21

@sebastianludwig
Copy link
Author

sebastianludwig commented May 4, 2024

I'm not sure that's the right track. At least I'm not seeing the warning in the console 🤔

Edit: It indeed seems to be related. In a similar situation I do get the warning. But I haven't investigated yet what the difference to the example in the issue is and why I'm not seeing the warning there.

@ildarkayumov
Copy link

ildarkayumov commented May 6, 2024

please check this

function getControllerAndEnsureConnectedScope(controller: Controller, element: Element, outletName: string) {
  let outletController = getOutletController(controller, element, outletName)
  if (outletController) return outletController

  controller.application.router.proposeToConnectScopeForElementAndIdentifier(element, outletName)

  outletController = getOutletController(controller, element, outletName)
  if (outletController) return outletController
}

function propertiesForOutletDefinition(name: string) {
  const camelizedName = namespaceCamelize(name)

  return {
    [`${camelizedName}Outlet`]: {
      get(this: Controller) {
        const outletElement = this.outlets.find(name)
        const selector = this.outlets.getSelectorForOutletName(name)

        if (outletElement) {
          const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name)

          if (outletController) return outletController

          throw new Error(
            `The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`
          )
        }
        ...
}
  1. inside singular outlet controller getter getControllerAndEnsureConnectedScope is called and if it returns null then throws and error.
  2. inside getControllerAndEnsureConnectedScope router method router.proposeToConnectScopeForElementAndIdentifier -> elementMatchedValue is called. So this is first time
  3. plus 1 more time elementMatchedValue is called during Stimulus loading

by my opinion calling outlet inside parent_controller#connect does not seem like correct for me because controller loading is not finished yet
I guess the easiest "hacky" fix is to wrap childOutlet with setTimeout.

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

2 participants