1

Within a custom component defined with <template> and <slot> tags, a child element can get the parent element that is the custom component, and then use this element reference to access class defined vars and functions.

However, how can a child element know which element is the custom component?

The template itself is contained in a shadow DOM. When rendering a custom component on the page, this shadow DOM is cloned and attached into the custom component as its host.

Elements that are slotted in to the custom component however can't seem to tell the node tree it is attached to came from a shadow DOM node that was cloned, and can't tell which element in the current DOM tree is the top level custom component unless it iterates through each parentNode to check if they have a shadowRoot.

What is the way to let a child element inside a custom component access the custom component's class variables and functions?

customElements.define("my-comp", class extends HTMLElement {
  creationTimestamp;

  constructor() {
    super();
    let template = document.getElementById('my-template');
    let templateContent = template.content;

    const shadowRoot = this.attachShadow({
      mode: 'open'
    });
    shadowRoot.appendChild(templateContent.cloneNode(true));

    this.creationTimestamp = Date.now();
  }
})
<html>

<template id="my-template">
  I'm a new custom component using slots with value <slot name="slot1">default</slot>
  <button onclick="console.log(this.parentNode.host.creationTimestamp)">Print timestamp at Template level</button>
</template>

<my-comp>
  <span slot="slot1">
    slotted value
    <div>
      <button onclick="console.log(this.parentNode.parentNode.parentNode.creationTimestamp)">Print timestamp</button>
    </div>
  </span>

</my-comp>

</html>
isherwood
  • 58,414
  • 16
  • 114
  • 157
David Min
  • 1,246
  • 8
  • 27
  • The answer is simple - a child component **never** should directly access parent functionality. Please explain the scenario in which such proceedings would in your opinion be necessary or desirable. – connexo Mar 15 '22 at 19:52
  • Again, add an executable SO snippet so we can extend on it in answers. Its probably because I am dyslectic...but can process (Web Component) code like a Quantum Computer. – Danny '365CSI' Engelman Mar 15 '22 at 20:18
  • I have an inkling the answer is going to involve ``assignedNodes`` https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedNodes But I can't proces your english text properly. – Danny '365CSI' Engelman Mar 15 '22 at 20:20
  • @connexo the parent is exposing these as public, what should not be accessed is kept as private. – David Min Mar 16 '22 at 18:08
  • Scenario included in the snippet @Danny'365CSI'Engelman requested – David Min Mar 16 '22 at 18:08

2 Answers2

3

There's a couple of very simple rules for decoupling WebComponents:

  • Parent to child: one-way data-binding or HTML5 attributes for passing data and calling child.func() for performing actions.

  • Child to parent: Child fires an event. The parent(s) listen for that event. Because of event bubbling you can have an arbitrary number of parents picking up that event with no extra effort.

This pattern is used by standard web components as well (input, textbox etc). It also allows easy testing since you don't have to mock the parent.

This was the recommended pattern described by the Polymer library, which was/is a wrapper over WebComponents. Unfortunately I can't find that specific article that described this pattern.

polymer element communication diagram

In the following example, the parent listens for events from children and calls functions on itself to affect its state, or functions on the children to make them do something. None of the children know about the parent.

Diagram showing parent listening for child events and performing actions on itself or calling

nicholaswmin
  • 21,686
  • 15
  • 91
  • 167
  • Event bubbling in the end was the best solution for me, thanks! For a child button, instead of `onclick=""` I gave the button an attribute string that refers to a function inside the class. The class can then find out which element called it by using `event.path` and read the attribute. – David Min Mar 22 '22 at 19:20
  • 1
    @DavidMin That's not event bubbling. Event bubbling is the ability of an event to reach recursive parents. I wouldn't read (or concern yourself) with who the parent is within a child element. The **containing element of both the parent and the child** should listen to the event by the child and then call a function on the parent of the child that would perform the appropriate action. The child shouldn't affect the parent. The parent should only affect itself or children by calling their public methods. Don't access internals of elements within other elements. Instead expose API's. – nicholaswmin Mar 22 '22 at 20:49
  • I'm confused by what you mean by 'containing element' - this means to me the parent. I didn't mean immediate parent of a child element, but all the way up to the parent that contains the class defs. In my code snippet, `` is the 'parent', listening to event bubbled through from the ` – David Min Mar 23 '22 at 00:06
  • 1
    The child shouldn't be the one affecting the parent. The button should simply fire an event, i.e 'clicked'. The parent then listens for that event and performs the action on itself. The button only concerns itself with firing `click` events. It shouldn't be changing or meddling with the internals of another element, i.e the parent. The parent should do that to itself, in response to the `click` event of the child. Does this make sense? I've edited my question to include an image – nicholaswmin Mar 23 '22 at 06:18
  • My first comment might not have been clear, the implementation is exactly as you said just now. Parent listens for an event fired off and reads what the child is requesting to be executed, then uses internal functions to changes its internal vars. Child has no direct access, only tells the parent what it should do in response to that button being clicked. – David Min Mar 24 '22 at 17:00
  • I did `this.onclick=processEvent` on the parent, with `processEvent` getting the `MouseEvent`. The child has an attribute b that specifies the function name as a string e.g `displayStockPrices`, and `processEvent` reads `e.path[0].getAttribute(b)`, and then go finds the corresponding function and executes it. Think this is broadly in line with what you are saying? – David Min Mar 24 '22 at 17:04
  • Yep that's correct! – nicholaswmin Mar 26 '22 at 07:55
  • Polymer had an event system where you could do something like `` Basically all Polymer elements searched on initialisation the elements DOM tree and if a child was found with an `on-something` attribute, the event listener was automatically setup to match the call the parent function. You could perhaps code something similar yourself which you can include in all your elements. – nicholaswmin Mar 26 '22 at 08:16
2

Now I understand. You want to prevent:

let timeStamp = this.parentNode.parentNode.parentNode.creationTimestamp;

There are multiple StackOverflow answers tackling this:

  1. Let the child find a parent
    In control: child
    mimic standard JS .closest(selector) which does not pierce shadowRoots!
    with a custom closestElement(selector) function:
    Custom Element getRootNode.closest() function crossing multiple (parent) shadowDOM boundaries

  2. Make the parent tell something to a child
    In control: parent
    by a recursive dive into shadowRoots:
    How can I get all the HTML in a document or node containing shadowRoot elements

  3. Let the parent respond to a call from a child
    In control: both Child sends Event, Parent needs to listen
    Webcomponents communicating by custom events cannot send data

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • How would a `selector` be set? For my example, 'Print timestamp' button will need to select tag `my-comp`. So this will require the child to already know the parent's tag? i.e. Dev is hard-coding this in. – David Min Mar 17 '22 at 17:34
  • Same sort of hard-coding will apply to 3 when it is arranged by Dev what the event is going to be and implement it for all children that want to know its parent custom component – David Min Mar 17 '22 at 17:35
  • All cases require "hard coding". If I want to call you in a full room of people, you won't respond to "Henry!". If you want to call something in the DOM, you need to know how to address it. – Danny '365CSI' Engelman Mar 17 '22 at 20:12