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>
Related
I have created a Web Component for a requirement and I have received a The best way to handle this is by not injecting untrusted strings in this way. Instead, use node.innerText or node.textContent to inject the string- the browser will not parse this string at all, preventing an XSS attack. code review comment. I am still thinking about how to replace innerHTML to innerText or textContent.
Would the community have an input?
import RealBase from '../real-base';
import productCardTitleCss from '../../../css/_product-card-title.scss?inline';
import baseCss from '../../../css/_base.scss?inline';
const ESAPI = require('node-esapi');
class RealProductCardTitle extends RealBase {
constructor() {
super();
this.defaultClass = 'real-product-card-title';
}
connectedCallback() {
super.connectedCallback();
this.render();
}
static get observedAttributes() {
return ['heading', 'form'];
}
get heading() {
return this.getAttribute('heading') || '';
}
set heading(value) {
this.setAttribute('heading', value ? ESAPI.encoder().encodeForHTML(value) : value);
}
get form() {
return this.getAttribute('form') || '';
}
set form(value) {
this.setAttribute('form', value ? ESAPI.encoder().encodeForHTML(value) : value);
}
attributeChangedCallback() {
this.render();
}
render() {
const {
heading,
form,
} = this;
this.classList.add('real-block');
this.classList.add(this.defaultClass);
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
}
//Recommendation for this line below
this.shadowRoot.innerHTML = `
<style>
${productCardTitleCss}
${baseCss}
</style>
<real-heading
class="real-product-card-title-heading"
input="${heading}">
</real-heading>
<div
class="real-product-card-title-attributes real-inline-block">
${form}
</div>`;
}
}
window.customElements.define('real-product-card-title', RealProductCardTitle);
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.
In my lit-element I'm rendering something based on an outside variable. How can I know to update when that variable changes?
import { LitElement, html } from 'lit-element';
import './element01.js';
class Layout extends LitElement {
createRenderRoot(){ return this; }
static get properties() {
return {
settings: { Object }
};
}
constructor() {
super();
}
render() {
return html`
${(settings.foo === 'bar')? html`<my-element01 />` : null}
`;
}
}
customElements.define('my-layout', Layout);
The settings object is being modified outside, how can this element know to update? I'm not using any other framework.
The index.html file should always have a
<my-app></my-app>
Within the my-app element, you will be able to use all the features of litElement such as
class MyApp extends LitElement {
static get properties() {
return {
prop: {type: Object},
};
}
render() {
return html`
<my-element .prop="${this.prop}"</my-element>
<my-server .prop="${this.prop}"</my-server>
<button #onClick='${(e) => this.prop += 1}' >change the settings value</button>
`;
}
}
Here one example I tried to illustrate. settings property changed outside of litElement and effected in litElement.
demo
index.html
...
<my-layout></my-layout>
<br>
<button onClick="_buttonClicked()" >change the settings value</button>
<script>
document.querySelector('my-layout').settings={foo:"bar"};
function _buttonClicked (e) {
document.querySelector('my-layout').settings = {foo:"baz"};
}
</script>
my-layout :
import { LitElement, html } from 'lit-element';
//import './element01.js';
class Layout extends LitElement {
createRenderRoot(){ return this; }
static get properties() {
return {
settings: { Object }
};
}
constructor() {
super();
}
render() {
return html`
${this.settings.foo === 'bar'? html`<span> element01 will be rendered</span>` : null}
`;
}
}
customElements.define('my-layout', Layout)
I made some syntax corrections
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.
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.