error getting content from template customelement - javascript

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.

Related

How to set dynamic observedAttributes

My goal is to set observedAttributes dynamically, so my web component will watch only attributes following a pattern, like a colon (:attr) <my-element static="value1" :dynamic=${dynamic}/>
In this case, <my-element> should set observedAttributes only for the attribute :dynamic
The problem is that static get observedAttributes() runs before there's even a this, explained in https://andyogo.github.io/custom-element-reactions-diagram/
So this won't work
static get observedAttributes() {
return this.getAttributeNames().filter((item) => item.startsWith(':'));
}
and of course neither does
constructor() {
super();
this._observedAttributes = this.getAttributeNames().filter((item) => item.startsWith(':'));
}
static get observedAttributes() {
return this._observedAttributes;
}
Thanks!
<!DOCTYPE html>
<body>
<my-element static="value1" :dynamic="value2" ></my-element>
<script>
class MyElement extends HTMLElement {
constructor() {
super();
this._observedAttributes= this.getAttributeNames().filter((item) => item.startsWith(':'));
console.log('observedAttributes',this._observedAttributes);
}
static get observedAttributes() {
return this._observedAttributes;
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(name, oldValue, newValue); //doesn't log anything
}
}
customElements.define("my-element", MyElement);
setTimeout(() => {
console.log('setting dynamic attribute. This should trigger attributeChangedCallback. But no.');
document.querySelector('my-element').setAttribute(':dynamic', 'value3');
}, 2000);
</script>
</body>
</html>
Following #Danny '365CSI' Engelman's suggestion, I looked at the MutationObserver API and came up with a simple solution that I think offers more than observedAttributes
<!DOCTYPE html>
<body>
<my-element static="value1" :dynamic="value2"></my-element>
<script>
class MyElement extends HTMLElement {
constructor() {
super();
}
mutationObserverCallback(mutationList, observer) {
for (const mutation of mutationList) {
if (mutation.type === 'attributes'
&& mutation.attributeName.startsWith(':')
&& mutation.oldValue !== mutation.target.getAttribute(mutation.attributeName)) {
console.log(`The dynamic ${mutation.attributeName} attribute was modified.`);
}
}
}
connectedCallback() {
this.mutationObserver = new MutationObserver(this.mutationObserverCallback);
this.mutationObserver.observe(this, { attributes: true, attributeOldValue : true });
}
disconnectedCallback() {
this.mutationObserver.disconnect();
}
}
customElements.define("my-element", MyElement);
setTimeout(() => {
console.log('setting dynamic attribute for :dynamic and static attributes. This should trigger mutationObserverCallback for :dynamic only.');
// ▼ will trigger mutationObserverCallback
document.querySelector('my-element').setAttribute(':dynamic', 'value3');
// ▼ will not trigger mutationObserverCallback
document.querySelector('my-element').setAttribute('static', 'value4');
}, 200);
</script>
</body>
</html>

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.

Send data to another web component

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

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.

Polymer 2.0 strange behaviour when removing an item from an observed array

While working on an application with Polymer 2.0, I've encountered a problem that is illustrated by this simplified code: https://jsfiddle.net/w912gf8g/33/
<base href="https://polygit.org/polymer+polymer+v2.0.0-rc.2/webcomponentsjs+webcomponents+:master/components/">
<script src="webcomponentsjs/webcomponents-lite.js"></script>
<link rel="import" href="polymer/polymer-element.html">
<link rel="import" href="polymer/lib/elements/dom-repeat.html">
<root-element></root-element>
<dom-module id="root-element">
<template>
<dom-repeat items="[[items]]">
<template>
<inner-element inner=[[item]]></inner-element>
</template>
</dom-repeat>
<button on-click="_removeItem">Remove the first item</button>
</template>
</dom-module>
<dom-module id="inner-element">
<template>
<h3>Element: {{inner.prop}}</h3>
</template>
</dom-module>
<script>
document.addEventListener('WebComponentsReady', () => {
// Extend Polymer.Element base class
class RootElement extends Polymer.Element {
static get is() {
return 'root-element';
}
constructor() {
super();
}
static get properties() {
return {
items: {
type: Array,
value: () => [{ prop: 1 }, { prop: 2 }, { prop: 3 }],
notify: true
}
}
}
_removeItem(){
this.splice('items', 0, 1);
}
}
// Register custom element definition using standard platform API
customElements.define(RootElement.is, RootElement);
class InnerElement extends Polymer.Element {
static get is() {
return 'inner-element';
}
constructor() {
super();
}
static get properties() {
return {
inner: {
type: Object,
value: () => {},
notify: true
}
}
}
static get observers() {
return [
'_changed(inner.prop)'
]
}
_changed(newVal){
if(this.lastValue != null && this.lastValue !== newVal){
alert("How on earth does this happen?");
}
this.lastValue = newVal;
}
}
customElements.define(InnerElement.is, InnerElement);
});
</script>
I would expect that after the button is clicked, which invokes removal of the first element, the code that invokes the alert message should never be run, because this indicates that the properties of the array entries have been changed and this makes little sense as the operation was intended to simply remove an entry from the array.
Question
Why does this happen? How can this be avoided? Am I using observers or splice method not the way it was designed to be used?
Side note
I can guess the reason why this happens is because Polymer is optimized to actually always remove the last DOM element and shifting the data "upwards" before that.
But I cannot understand how can this not cause major side effects in other applications.
You have to add mutable-data as property on your dom-repeat:
<template is="dom-repeat" items="[[data]]" mutable-data>
Please check this page Data system concepts

Categories