Send data to another web component - javascript

I am trying out web component recently and I came across a problem, how do I send data from the first component to the second component?
I tried to use event but the second component can't receive the data, this is what I did:
(I am totally new to this, not sure if I am doing the right way)
Index.html
<body>
<first-component>
<p>First component!</p>
</first-component>
<second-component>
<p>Second component!</p>
</second-component>
<script src="./component-one.js"></script>
<script src="./component-two.js"></script>
</body>
component-one.js
const firstComponentTemplate = document.createElement('template');
firstComponentTemplate.id = 'first-component-template';
firstComponentTemplate.innerHTML = `
<div id="first-component-container">
<slot></slot>
<button>Click</button>
</div>
`;
class FirstComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(firstComponentTemplate.content.cloneNode(true));
}
btnClick() {
const btnHtml = this.shadowRoot.querySelector('button');
btnHtml.addEventListener('click', () => {
this.dispatchEvent(
new CustomEvent('a-custom-event', {
bubbles: true,
composed: true,
detail: {
value: 'Some value',
},
})
);
});
}
connectedCallback() {
this.btnClick();
}
}
customElements.define('first-component', FirstComponent);
component-two.js
const secondComponentTemplate = document.createElement('template');
secondComponentTemplate.id = 'second-component-template';
secondComponentTemplate.innerHTML = `
<div id="second-component-container">
<slot></slot>
</div>
`;
class SecondComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.appendChild(secondComponentTemplate.content.cloneNode(true));
}
connectedCallback() {
this.shadowRoot.addEventListener('a-custom-event', function (event) {
console.log(event); //Check console but nothing in console
});
}
}
customElements.define('second-component', SecondComponent);
So as you can see, I am trying to pass the data from first custom element to second custom element,
how come this is not working, which part am I doing wrong?
is this the correct way to do it?
is it bad practice to pass/receive data from one component to another sibling component?
Thanks

Related

Communcation between Web Components

I am building a project with web components and vanilla javascript.
I have a component/module called meal.module - It is the parent component of the components meal-list and meal-search.
meal-list displays multiple meals from an api.
meal-search contains an input field and seachterm as attribute.
meal.module.js
export default class MealModule extends HTMLElement {
connectedCallback() {
this.innerHTML = '<mp-meal-search searchterm=""></mp-meal-search> ' +
'<mp-meal-list></mp-meal-list> ' +
}
}
if (!customElements.get('mp-meal-module')) {
customElements.define('mp-meal-module', EssenModule);
}
meal-list.component
export default class MealListComponent extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = Template.render();
// Renders the meals from api into the template
this.getMeals();
}
(....) more code
}
if (!customElements.get('mp-meal-list')) {
customElements.define('mp-meal-list', MealListComponent);
}
meal-search.component
export default class MealSearchComponent extends HTMLElement {
static get observedAttributes() {
return ['searchterm'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'searchterm') {
this.doSearch();
}
}
set searchTerm(val) {
this.setAttribute('searchterm', val)
}
get searchTerm() {
return this.getAttribute('searchterm');
}
constructor() {
super();
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = '<input type="text" id="searchterm" value=""/>'
this.shadowRoot.addEventListener('change', (event) =>
this.searchTerm = event.target.value
)
}
doSearch() {
// send signal to MealListComponent for search
}
}
if (!customElements.get('mp-meal-search')) {
customElements.define('mp-meal-search', MealSearchComponent);
}
In the seach-component the SearchTerm is configured as Attribute. Everytime the input field gets changed, the attribute also changes.
Now I want to implement a "searchMeal" function, that always triggers when the attribute in MealSearchComponent changes.
I already tried to import the MealSearchComponent into the MealListComponent. But it does seem to break the rule of components, not having any dependencies.
JavaScript from the outside of the shadow DOM can access the shadow DOM via the element.ShadowRoot property.

Lit-elements, the idiomatic way to write a controlled component

I'm working with lit-elements via #open-wc and is currently trying to write a nested set of components where the inner component is an input field and some ancestor component has to support some arbitrary rewrite rules like 'numbers are not allowed input'.
What I'm trying to figure out is what the right way to built this is using lit-elements. In React I would use a 'controlled component' see here easily forcing all components to submit to the root component property.
The example below is what I've come up with using Lit-Elements. Is there a better way to do it?
Please note; that the challenge becomes slightly harder since I want to ignore some characters. Without the e.target.value = this.value; at level-5, the input elmement would diverge from the component state on ignored chars. I want the entire chain of components to be correctly in sync, hence the header tags to exemplify.
export class Level1 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h1>${this.value}</h1>
<level-2 value=${this.value} #input-changed=${this.onInput}></level-2>
</div>`;
}
onInput(e) {
this.value = e.detail.value.replace(/\d/g, '');
}
}
...
export class Level4 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h4>${this.value}</h4>
<level-5 value=${this.value}></level-5>
</div>`;
}
}
export class Level5 extends LitElement {
static get properties() {
return {
value: { type: String }
};
}
render() {
return html`
<div>
<h5>${this.value}</h5>
<input .value=${this.value} #input=${this.onInput}></input>
</div>`;
}
onInput(e) {
let event = new CustomEvent('input-changed', {
detail: { value: e.target.value },
bubbles: true,
composed: true
});
e.target.value = this.value;
this.dispatchEvent(event);
}
}
export class AppShell extends LitElement {
constructor() {
super();
this.value = 'initial value';
}
render() {
return html`
<level-1 value=${this.value}></level-1>
`;
}
}
Added later
An alternative approach was using the path array in the event to access the input element directly from the root component.
I think it's a worse solution because it results in a stronger coupling accross the components, i.e. by assuming the child component is an input element with a value property.
onInput(e) {
const target = e.path[0]; // origin input element
this.value = e.path[0].value.replace(/\d/g, '');
// controlling the child elements value to adhere to the colletive state
target.value = this.value;
}
Don't compose your events, handle them in the big parent with your logic there. Have the children send all needed info in the event, try not to rely on target in the parent's event handler.
To receive updates, have your components subscribe in a shared mixin, a la #mishu's suggestion, which uses some state container (here, I present some imaginary state solution)
import { subscribe } from 'some-state-solution';
export const FormMixin = superclass => class extends superclass {
static get properties() { return { value: { type: String }; } }
connectedCallback() {
super.connectedCallback();
subscribe(this);
}
}
Then any component-specific side effects you can handle in updated or the event handler (UI only - do logic in the parent or in the state container)
import { publish } from 'some-state-solution';
class Level1 extends LitElement {
// ...
onInput({ detail: { value } }) {
publish('value', value.replace(/\d/g, ''));
}
}

How can I specify the sequence of running nested web components constructors?

I created two web-components and nested one of them into the other.
Both of them have its constructor. The problem that I have is that, I have no control on running the sequence of the constructors.
Is there any way which I can set this out?
Here's my code:
child web component:
(function () {
const template = document.createElement('template');
template.innerHTML = `<div>WC1</div>`;
class WC1Component extends HTMLElement {
constructor() {
super();
console.log('WC1: constructor()');
var me = this;
me._shadowRoot = this.attachShadow({ 'mode': 'open' });
me._shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
console.log('WC1: connectedCallback');
}
test() {
console.log('test:wc1');
}
}
window.customElements.define('wc-one', WC1Component);
}());
parent web component:
(function () {
const template = document.createElement('template');
template.innerHTML = `
<wc-one id="wc1"></wc-one>
<div>WC2</div>
`;
class WC2Component extends HTMLElement {
constructor() {
super();
console.log('WC2: constructor()');
var me = this;
me._shadowRoot = this.attachShadow({ 'mode': 'open' });
me._shadowRoot.appendChild(template.content.cloneNode(true));
me._wc1 = me._shadowRoot.querySelector('#wc1');
}
connectedCallback() {
console.log('WC2: connectedCallback');
this._wc1.test(); // <-- Error: test is undefined!
}
}
window.customElements.define('wc-two', WC2Component);
}());
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test Web Component</title>
</head>
<body>
<script src="wc1.js"></script>
<script src="wc2.js"></script>
<wc-two></wc-two>
<script>
</script>
</body>
</html>
The result:
WC2: constructor()
WC2: connectedCallback
Uncaught TypeError: this._wc1.test is not a function
WC1: constructor()
WC1: connectedCallback
you can use:
setTimeout(() => {
this._wc1.test();
});
in connectedCallback
Actually you do have control on the running sequence of the constructors.
Since <wc-one> is created by <wc-two> the WC2 constructor will always be called before WC1.
The error is due to the fact that when you try invoke it, the inner component (WC1) is not yet added to the DOM.
You could listen for the DOMContentLoaded event to be sure element is OK, or the load event on window, or implement the window.onload handler. #elanz-nasiri is aslo working.
DOMContentLoaded will be fired first.
There're alternate solution but they are more difficult to implement.
window.customElements.define('wc-one', class extends HTMLElement {
constructor() {
super()
this.attachShadow({ 'mode': 'open' }).innerHTML = `<div>WC1</div>`
}
test(source) {
console.log('test:wc1', source)
}
} )
window.customElements.define('wc-two', class extends HTMLElement {
constructor() {
super()
this._shadowRoot = this.attachShadow({ 'mode': 'open' })
this._shadowRoot.innerHTML = `<wc-one id="wc1"></wc-one><div>WC2</div>`
this._wc1 = this._shadowRoot.querySelector('#wc1');
}
connectedCallback() {
setTimeout( ()=>this._wc1.test('setTimout') )
document.addEventListener( 'DOMContentLoaded', ()=>this._wc1.test('DOMContentLoaded') )
window.onload = ()=>this._wc1.test('window.onload')
window.addEventListener( 'load', ()=>this._wc1.test('load') )
}
} )
<wc-two></wc-two>
If you want to control when a constructor is called then you need to call it. Do not allow the HTML parser to do it for you. The problem with allowing the HTML parser to do it is that you do not know when the component will be upgraded.
const template = document.createElement('template');
template.innerHTML = `<div>WC1</div>`;
class WC1Component extends HTMLElement {
constructor() {
super();
console.log('WC1: constructor()');
this.attachShadow({ 'mode': 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
console.log('WC1: connectedCallback');
}
test() {
console.log('test:wc1');
}
}
window.customElements.define('wc-one', WC1Component);
//<wc-one id="wc1"></wc-one>
//<div>WC2</div>
class WC2Component extends HTMLElement {
constructor() {
super();
console.log('WC2: constructor()');
let wc1 = document.createElement('wc-one');
wc1.id = 'wc1';
this.attachShadow({ 'mode': 'open' });
this._wc1 = wc1;
this.shadowRoot.appendChild(wc1);
let div = document.createElement('div');
div.textContent = 'WC2';
this.shadowRoot.appendChild(div);
}
connectedCallback() {
console.log('WC2: connectedCallback');
this._wc1.test();
}
}
window.customElements.define('wc-two', WC2Component);
<wc-two></wc-two>
All I am doing is causing the constructor for wc-one to execute by calling document.createElement('wc-one') and then I add that into the component. This forces the object to exist before I attempt to use its test function.
Also, you do not need to save off the return value of this.attachShadow({ 'mode': 'open' }); since it is already available as this.shadowRoot.
You also do not need to save off this as me.

How to use a custom element to wrap child custom elements into a div

I'm trying to create a wrapper Custom Element that wraps its child Custom Elements into a div.
But the child elements aren't wrapped. Instead, an empty div is inserted into the wrapper element before the child elements
<script>
class ListItem extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = "<div>ListItem</div>";
}
}
class List extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `<div class="list">${this.innerHTML}</div>`;
}
}
customElements.define("list-item", ListItem);
customElements.define("my-list", List);
</script>
<my-list>
<list-item></list-item>
<list-item></list-item>
<list-item></list-item>
</my-list>
This is the result:
<my-list>
<div class="list"></div>
<list-item><div>ListItem</div></list-item>
<list-item><div>ListItem</div></list-item>
<list-item><div>ListItem</div></list-item>
</my-list>
I would have expected the following:
<my-list>
<div class="list">
<list-item><div>ListItem</div></list-item>
<list-item><div>ListItem</div></list-item>
<list-item><div>ListItem</div></list-item>
</div>
</my-list>
You can try it out here.
It's due to the parsing execution sequence. When the <my-list> tag is detected, it is created (and connected) immediately, before its children are inserted.
As a consequent ${this.innerHTML} will return an empty string in connectedCallback().
You could wait for the children to be parsed, for example with the help of setTimeout():
class List extends HTMLElement {
connectedCallback() {
setTimeout( () =>
this.innerHTML = `<div class="list">${this.innerHTML}</div>`
)
}
}
But you'd better use Shadow DOM with <slot> to insert elements of the light DOM:
class List extends HTMLElement {
connectedCallback() {
this.attachShadow( { mode: 'open' } )
.innerHTML = `<div class="list"><slot></slot></div>`
}
}
See the example below.
class ListItem extends HTMLElement {
connectedCallback() {
this.innerHTML = "<div>ListItem</div>";
}
}
class List extends HTMLElement {
connectedCallback() {
this.attachShadow( { mode: 'open' } )
.innerHTML = `<div class="list"><slot></slot></div>`
}
}
customElements.define("list-item", ListItem);
customElements.define("my-list", List);
<my-list>
<list-item></list-item>
<list-item></list-item>
<list-item></list-item>
</my-list>

error getting content from template customelement

I have a basic CustomElement but I'm having the following problem:
<template id="custom-element">
<h1>Example 1</h1>
</template>
<script>
class CustomElement extends HTMLElement {
constructor() {
super(); // always call super() first in the ctor.
let shadowRoot = this.attachShadow({mode: 'open'});
const template = document.querySelector('#custom-element');
const instance = template.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
connectedCallback() {
console.log("Connected");
}
disconnectedCallback() {
}
attributeChangedCallback(attrName, oldVal, newVal) {
}
}
window.customElements.define('custom-element', CustomElement);
</script>
I'm getting this error in console:
Uncaught TypeError: Cannot read property 'content' of null
And it is because the const templateis always null. This was working before but I don't know if anything has changed that now it doesn't works. I'm using Chrome Version 62.0.3202.94 (Official Build) (64-bit)
Any help on this please?
Try this:
<template id="custom-element">
<style>
h1 {
color: red;
font: bold 24px Tahoma;
}
</style>
<h1>Example 1</h1>
</template>
<script>
const template = (document.currentScript||document._currentScript).ownerDocument.querySelector('#custom-element').content;
class CustomElement extends HTMLElement {
constructor() {
super(); // always call super() first in the ctor.
let shadowRoot = this.attachShadow({mode: 'open'});
let instance = template.cloneNode(true);
shadowRoot.appendChild(instance);
}
connectedCallback() {
console.log("Connected");
}
disconnectedCallback() {
}
attributeChangedCallback(attrName, oldVal, newVal) {
}
}
window.customElements.define('custom-element', CustomElement);
</script>
You need to get at document.currentScript||document._currentScript before the constructor. It must be accessed in the global space of the imported HTML file.
I always use both together to work with all of the web-component polyfills. If you don't need the polyfill, by limiting the browsers you support, then you can just use document.currentScript.

Categories