0

The question is, How do I call a method from a child component?

Ex : Consider I have a login form component located in the Parent component. So I need to show that form when I click the login button. The function to show the login form will be written in the Parent component and I need to t that function when I click the Button located in a child component.

This is the Parent component

import Nav from './componets/navigation-bar.js'
import Comp from './componets/footer.js'
import UserComp from './componets/user-comp.js'

import Base from './componets/Base.js'

const style = `
    .container {
        display: flex;
        flex-direction: row;
        justify-content: center;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }

    .container > user-comp {
        padding: 1em;
    }

`
const content = `
<navigation-bar></navigation-bar>
    <div class="container">
        <user-comp mirror="true">
            <img slot="image" src="https://www.zricks.com/img/UpdatesBlog/44b94c9d-ab13-401d-9e5b-86a00f9da6496%20Must%20Follow%20Tips%20to%20Market%20a%20Luxury%20Home.jpg" alt="Image"></img>
            <h1 slot="title">Rent or Lease your own property</h1>
        </user-comp>
        <user-comp mirror="true">
            <img slot="image" src="https://s3.amazonaws.com/clients.granalacantadvertiser.images/wp-content/uploads/2017/06/14072232/2236775_2_O.jpg" alt="Image"></img>
            <h1 slot="title">Looking for a place</h1>
        </user-comp>
    </div>
<footer-c></footer-c>
`

export default class UI extends Base {
    constructor() {
        super()

        this.render(style, content)
        this.attachShadow({ mode: 'open' })
        this.shadowRoot.appendChild(this.template.content.cloneNode(true))
    }
    clicked = () => {
        console.log('clicked')
    }
}
window.customElements.define('ui-c', UI)

document.querySelector('#root').innerHTML = '<ui-c></ui-c>'

This is the child component

import Base from './Base.js'

const style = `
    header {
        position: absolute;
        top:0;
        left:0;
        right:0;
        background-color: #111111;
        color: #eeeeee;
        z-index:1;
    }
    
    .logo {
        margin-left: 2em;
    }

    nav {
        display: flex;
        flex-direction: row;
        justify-content: space-between;
    }

    #login-button {
        height: 2.5em;
        width: 10em;
        margin: auto 2em;
        text-transform: uppercase;
        color: #eeeeee;
        background-color: #239710;
        border: none;
        box-shadow: 1px 1px 5px 1px rgba(23,97,10,0.64);
        outline: none;
        cursor: pointer;
        transition: 0.4s;
    }
    
    #login-button:hover {
        background-color: #34a832;
    }

`
const content = `
    <header>
        <nav>
            <h3 class="logo">Homey</h3>
            <button id="login-button"> login </button>
        </nav
    </header>
`

export default class Nav extends Base {
    constructor() {
        super()
        this.render(style, content)
        this.attachShadow({ mode: 'open' })
        this.shadowRoot.appendChild(this.template.content.cloneNode(true))
    }

    connectedCallback() {
        this.shadowRoot
            .querySelector('#login-button')
            .addEventListener('click', clicked())
    }
}
window.customElements.define('navigation-bar', Nav)

This is the Base class was written by me (In case to understand)

export default class Base extends HTMLElement {
    template = document.createElement('template')

    style(style) {
        if (style === null) return ' '
        return '<style>' + style + '</style>'
    }
    render(style, content) {
        if (content === null) content = ''
        this.template.innerHTML = this.style(style) + content
    }
}
artkoshelev
  • 872
  • 7
  • 22

2 Answers2

1

You can pass data from your child component to parent component using an event.

Within your child component you can create a custom event and fire it up when you want to call the method on parent component.

// create and dispatch the event
var event = new CustomEvent("cat", {
  detail: {
    hazcheeseburger: true
  }
});
obj.dispatchEvent(event);

Then in your parent component you can listen for that event. Once the event is triggered, the event listener will catch and proceed accordingly. It will look something like this.

obj.addEventListener("cat", function(e) { process(e.detail) });

Example is taken from MDN web docs.

Suvin Nimnaka Sukka
  • 341
  • 1
  • 7
  • 21
0

Events are a great solution to prevent tight coupling between components. But require some work.

Sometimes you just know you need that DIV element 3 levels up/down the DOM

UP the DOM

The standard element.closest(selector) "walks" up the DOM to find the selector you are after.

https://developer.mozilla.org/en-US/docs/Web/API/Element/closest

But .closest() does not escape shadowDOM

For that you have to write a recursive closestNode(selector) function that crosses all shadowDOMs with .getRootNode() till it finds the selector

customElements.define("my-element", class extends HTMLElement {

  closestNode(
    selector, // selector like in .closest()
    start = this, // extra functionality to skip a parent
    closest = (el, found = el && el.closest(selector)) =>
    !el || el === document || el === window 
      ? null // standard .closest() returns null for non-found selectors also
      : found || closest(el.getRootNode().host) // recursion!! break out to parent DOM
  ) {
    return closest(start); // look from start
  }

  connectedCallback() {
    this.attachShadow({
           mode: 'closed'// just to show it works with closed mode
         }).append(document.getElementById(this.nodeName).content.cloneNode(true));

    this.onclick = (evt) => {
      evt.stopPropagation();
      let container = this.closestNode('div');
      let color = evt.target.childNodes[0].nodeValue;
      container.style.background = color;
    }
  }
})
<template id=MY-ELEMENT>
  <style>
    button {
      font: 16px Arial, sans;
      margin:.5em;
    }
    button:hover{
      background:lightgreen;
    }
  </style>
  <button><slot></slot></button>
</template>
<div>
  <my-element>red
    <my-element>green
      <my-element>orange
        <my-element>blue
          <my-element>yellow
            <my-element>hotpink
            </my-element>
          </my-element>
        </my-element>
      </my-element>
    </my-element>
  </my-element>
</div>

DOWN the DOM

Something you want to prevent, but sometimes comes in handy

  const shadowDive = (
          el, 
          selector, 
          match = (m, r) => console.warn('match', m, r)
  ) => {
    let root = el.shadowRoot || el;
    root.querySelector(selector) && match(root.querySelector(selector), root);
    [...root.children].map(el => shadowDive(el, selector, match));
  }
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49