1

I'm implementing an Orchestrator pattern for my web components, like this:

<body>
  <my-controller>
    <div>
      <my-list>
        <span>
          <my-item></my-item>
        </span>
      </my-list>
    </div>
  </my-controller>
</body>

All custom elements I created utilize Shadow DOM using const root = super.attachShadow({mode: "open"}); root.appendChild(...);.

From my inner web components I want to reach my my-controller component in connectedCallback():

public connectedCallback(): void
    {
        if (this.isConnected)
        {
            for (let node = this.parentElement; node; node = node.parentElement)
                if (node instanceof ContainerBase)
                {
                    this._service = (<ContainerBase>node).GetService(this);
                    break;
                }

            if (this._service) this.Reset();
            else throw new ReferenceError(`${this.nodeName.toLowerCase()}: Couldn't find host element while connecting to document.`);
        }
    }


The strange thing is: I can only reach the immediate parent web control.


So, if connectedCallback() is called on <my-list> I can reach <my-controller>, but if connectedCallback() is called on <my-item> I only reach <span>. I can't even reach <my-list> when I'm starting my search with <my-item>.

Even when I walk the DOM tree after connectedCallback() is called, I cannot reach beyond <span> when I start at <my-item>.

Is this by intention?

Why can I reach an outer web component from the first nested one while I cannot reach the first nested web component from the second nested one?

How can I go up the DOM tree completely, from any nested level?

AxD
  • 2,714
  • 3
  • 31
  • 53
  • Maybe it's because you are using shadow dom? Else it works. – Supersharp Mar 18 '19 at 21:11
  • AFAIK, web components should always use Shadow DOM. The awkward thing is: `node.shadowRoot` is `null` when `node.parentElement` is `null` for the inner web component - and for the *inner* web component **only**. The two outermost web components behave as expected. – AxD Mar 19 '19 at 10:02

3 Answers3

5

When you define a Custom Element content whith a Shadow DOM, you create a distinct DOM tree. The Shadow DOM is a DocumentFragment with no root element.

As a consequence, you cannot reach its (intuitive) ancestor simply by walking the DOM up by the parentElement property.

To reach the host element of a Shadow DOM, instead use getRootNode() combined with host.

From <my-item>'s connectedCallback() method:

connectedCallback() {
   var parent = this.getRootNode().host
   console.log( parent.localNode ) // my-list
}

If you want to get an ancestor, you could try this recursive function.

Supersharp
  • 29,002
  • 9
  • 92
  • 134
  • I'd like to share here what I learned after studying on your excellent comment in regard to the `getRootNode()` return value: https://github.com/whatwg/dom/issues/739#issuecomment-474647270 . The type of the return value may appear obscure when reading the documentation, but that comment sheds a good share of light on it. – AxD Mar 21 '19 at 01:25
1

It is generally considered bad practice for an inner/child element to be able to access data from an outer/parent element.

It is safer and less coupled to use custom events from the inner components that are captured by the outer components.

The inner component would dispatch an event letting the outer element know that it needs something, then the outer component can call a function or set a parameter on the inner component.

You can do something like this:

Child Element

connectedCallback() {
  this.dispatch(new CustomEvent('request-service'));
}

set service(val) {
  this._service = val;
}

get service() {
  return this._service;
}

Service element:

constructor() {
  super();
  this.addEventListener('request-service',
    evt => {
      evt.target.service = this.GetService(evt.target);
    }
  );
}
Intervalia
  • 10,248
  • 2
  • 30
  • 60
  • 1
    I disagree. The event driven approach is disadvantageous: If you've got more than one instances on a page, they are all listening to the same event. You'd need to make sure to catch those events in the bubbling phase only. I don't see a disadvantage in a decent parent-child relation. That's what the DOM tree is about. The question remains: Why do web controls nested deep in a hierarchy behave different from the outer two web controls? – AxD Mar 19 '19 at 09:59
  • Disagree all you want. But the moment you decide to have parents and children knowing more about each other than they should you create a spaghetti mess and components that can never be used anywhere else. Your code can easily figure out the source of the events. If it can't then you should refactor it. Once you allow a child to dig into its parent you allow for code to be placed in the wrong component and getting out of that mess is very difficult to do. – Intervalia Mar 19 '19 at 14:34
  • No, it depends on the design. A
  • element, for instance, cannot live without an
      element as its parent. There is a tight relationship and a requirement for such. In a parent-child-relationship, if there is no parent providing a service, the child doesn't make sense. Using events, on the other side, you can't even guarantee there is a parent available servicing its children. If a tight parent-child-relationship requirement is what you want, then there is no bad practice in following that route.
  • – AxD Mar 19 '19 at 17:58
  • Actually the `
  • ` element can live outside of the parent. But, if my code had a __DOM dependency relationship__ just to get data into a child element I would rewrite it. The idea of making the script of one element depend on the script of a particular parent element just sends shivers down my spine. I am just mentioning a specific and well understood practice. If you want to write two elements that tightly coupled that is your choice. But I think it is a bad idea.
  • – Intervalia Mar 19 '19 at 19:49
  • There is only one "Bad" Practice and that is No Practice. Inherently means there are no Good Practices. – Danny '365CSI' Engelman Mar 21 '19 at 15:10