I am stuck on trying to set an attribute for a custom web component called ws-dialog I designed. It always adds an attribute undefined on the code:
<ws-dialog class="global" undefined="add-page-content"></ws-dialog>
What I want to do here is to declare an attribute like template instead of undefined. Here is the code that I used for adding an onclick event to all dialog target elements:
function addDialogEvents(target) {
target.addEventListener("click", () => {
let dialog = document.createElement(Dialog.getName());
let body = document.querySelector("body");
dialog.template = "add-page-content";
body.append(dialog);
});
}
And here is the code for my custom component:
"use strict";
class Dialog extends HTMLElement {
connectedCallback() {
//Styles for this custom component are declared in a separate CSS module
}
updateContent(id) {
let content = document.querySelector(`template#${id}`).content;
if (content)
this.append(content);
}
attributeChangedCallback(name, oldValue, newValue) {
this.TEMPLATE = "template";
switch (name) {
case this.TEMPLATE:
this.updateContent(newValue);
break;
}
}
static get observedAttributes() {
return [this.TEMPLATE];
}
static getName() {
return "ws-dialog";
}
get template() {
return this.getAttribute(this.TEMPLATE);
}
set template(template) {
this.setAttribute(this.TEMPLATE, template);
}
}
customElements.define(Dialog.getName(), Dialog);
Here is a playground to help you see when what method is fired.
Note when and how many times observedAttributes is called.
That means your this.TEMPLATE is undefined, and thus becomes a String undefined in the Array
<script>
const log = (...args) => {
let div=document.body.appendChild(document.createElement('DIV'));
div.style = `background:${args.shift()};color:white;font:13px Arial`;
div.append(args.join(" "));
}
customElements.define('my-element', class extends HTMLElement {
log() {
log(this.getAttribute("color"), this.outerHTML.split(">")[0],'>', ...arguments);
}
static get observedAttributes() {
log('red', `my-element observedAttributes`);// NO 'this' / Element here!
return ["color"];
}
constructor() { super().log("constructor") }
connectedCallback() {
this.log("connectedCallback" , this.innerHTML || "No innerHTML" );//FireFox difference!
setTimeout(() => this.log(`delayed connectedCallback ${this.innerHTML}`), 0);
}
attributeChangedCallback(name, oldValue, newValue) { // 4th W3C parameter = Namespace (not implemented in Browsers)
this.log("attributeChangedCallback", name, oldValue || "null", newValue);
}
disconnectedCallback(){ this.log("disconnectedCallback") }
})
document.body.onload = () => {
log('magenta', 'onload event');
A.setAttribute("color", "darkolivegreen");
B.innerHTML = "<my-element id=C color=hotpink>Charlie replaced Bravo</my-element>";
B.remove();
}
</script>
<my-element id=A color=green>Alfa</my-element>
<my-element id=B color=blue>Bravo</my-element>
Notes:
Helpful diagram at: https://andyogo.github.io/custom-element-reactions-diagram/
The execution order is slightly different in FireFox only, my advice is to not Develop, only Test in Firefox
In FireFox you can access the inner Elements from the connectedCallback. In all other Browsers you need that setTimeout. Different tech interpretations of the same W3C standard. And in this case Apple engineers were correct source. Chromium is based on the same engine.
Note how (delayed) code from the C connectedCallback (or any method) runs AFTER the disconnectedCallback.
You need to write code that does not error when DOM elements no longer exist.
JSFiddle playground: https://jsfiddle.net/WebComponents/67pduja9/
Related
I have a Custom Element that should have many HTML children. I had this problem when initializing it in class' constructor (The result must not have children). I understand why and know how to fix it. But exactly how I should design my class around it now? Please consider this code:
class MyElement extends HTMLElement {
constructor() {
super();
}
// Due to the problem, these codes that should be in constructor are moved here
connectedCallback() {
// Should have check for first time connection as well but ommited here for brevity
this.innerHTML = `<a></a><div></div>`;
this.a = this.querySelector("a");
this.div = this.querySelector("div");
}
set myText(v) {
this.a.textContent = v;
}
set url(v) {
this.a.href = v;
}
}
customElements.define("my-el", MyElement);
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el); // connectedCallback is not called yet since it's not technically connected to the document.
el.myText = "abc"; // Now this wouldn't work because connectedCallback isn't called
el.url = "https://www.example.com/";
Since MyElement would be used in a list, it's set up beforehand and inserted into a DocumentFragment. How do you handle this?
Currently I am keeping a list of pre-connected properties and set them when it's actually connected but I can't imagine this to be a good solution. I also thought of another solution: have an init method (well I just realized nothing prevents you from invoking connectedCallback yourself) that must be manually called before doing anything but I myself haven't seen any component that needs to do that and it's similar to the upgrade weakness mentioned in the above article:
The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.
Custom elements are tricky to work with.
The shadowDOM
if the shadowDOM features and restrictions suits your needs, you should go for it, it's straightforward :
customElements.define('my-test', class extends HTMLElement{
constructor(){
super();
this.shadow = this.attachShadow({mode: 'open'});
const div = document.createElement('div');
div.innerText = "Youhou";
this.shadow.appendChild(div);
}
});
const myTest = document.createElement('my-test');
console.log(myTest.shadow.querySelector('div')); //Outputs your div.
More about it there
Without shadowDOM
Sometimes, the shadowDOM is too restrictive. It provides a really great isolation, but if your components are designed to be used in an application and not be distributed to everyone to be used in any project, it can really be a nightmare to manage.
Keep in mind that the solution I provide below is just an idea of how to solve this problem, you may want to manage much more than that, especialy if you work with attributeChangedCallback, if you need to support component reloading or many other use cases not covered by this answer.
If, like me, you don't want the ShadowDOM features, and there is many reasons not to want it (cascading CSS, using a library like fontawesome without having to redeclare the link in every component, global i18n mechanism, being able to use a custom component as any other DOM tag, and so on), there is some clue :
Create a base class that will handle it in the same way for all components, let's call it BaseWebComponent.
class BaseWebComponent extends HTMLElement{
//Will store the ready promise, since we want to always return
//the same
#ready = null;
constructor(){
super();
}
//Must be overwritten in child class to create the dom, read/write attributes, etc.
async init(){
throw new Error('Must be implemented !');
}
//Will call the init method and await for it to resolve before resolving itself.
//Always return the same promise, so several part of the code can
//call it safely
async ready(){
//We don't want to call init more that one time
//and we want every call to ready() to return the same promise.
if(this.#ready) return this.#ready
this.#ready = new Promise(resolve => resolve(this.init()));
return this.#ready;
}
connectedCallback(){
//Will init the component automatically when attached to the DOM
//Note that you can also call ready to init your component before
//if you need to, every subsequent call will just resolve immediately.
this.ready();
}
}
Then I create a new component :
class MyComponent extends BaseWebComponent{
async init(){
this.setAttribute('something', '54');
const div = document.createElement('div');
div.innerText = 'Initialized !';
this.appendChild(div);
}
}
customElements.define('my-component', MyComponent);
/* somewhere in a javascript file/tag */
customElements.whenDefined('my-component').then(async () => {
const component = document.createElement('my-component');
//Optional : if you need it to be ready before doing something, let's go
await component.ready();
console.log("attribute value : ", component.getAttribute('something'));
//otherwise, just append it
document.body.appendChild(component);
});
I do not know any approach, without shdowDOM, to init a component in a spec compliant way that do not imply to automaticaly call a method.
You should be able to call this.ready() in the constructor instead of connectedCallback, since it's async, document.createElement should create your component before your init function starts to populate it. But it can be error prone, and you must await that promise to resolve anyway to execute code that needs your component to be initialized.
You need (a) DOM to assign content to it
customElements.define("my-el", class extends HTMLElement {
constructor() {
super().attachShadow({mode:"open"}).innerHTML=`<a></a>`;
this.a = this.shadowRoot.querySelector("a");
}
set myText(v) {
this.a.textContent = v;
}
});
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el);
el.myText = "abc";
document.body.append(frag);
Without shadowDOM you could store the content and process it in the connectedCallback
customElements.define("my-el", class extends HTMLElement {
constructor() {
super().atext = "";
}
connectedCallback() {
console.log("connected");
this.innerHTML = `<a>${this.atext}</a>`;
this.onclick = () => this.myText = "XYZ";
}
set myText(v) {
if (this.isConnected) {
console.warn("writing",v);
this.querySelector("a").textContent = v;
} else {
console.warn("storing value!", v);
this.atext = v;
}
}
});
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el);
el.myText = "abc";
document.body.append(frag);
Since there are many great answers, I am moving my approach into a separate answer here. I tried to use "hanging DOM" like this:
class MyElement extends HTMLElement {
constructor() {
super();
const tmp = this.tmp = document.createElement("div"); // Note in a few cases, div wouldn't work
this.tmp.innerHTML = `<a></a><div></div>`;
this.a = tmp.querySelector("a");
this.div = tmp.querySelector("div");
}
connectedCallback() {
// Should have check for first time connection as well but ommited here for brevity
// Beside attaching tmp as direct descendant, we can also move all its children
this.append(this.tmp);
}
set myText(v) {
this.a.textContent = v;
}
set url(v) {
this.a.href = v;
}
}
customElements.define("my-el", MyElement);
const frag = new DocumentFragment();
const el = document.createElement("my-el");
frag.append(el); // connectedCallback is not called yet since it's not technically connected to the document.
el.myText = "abc"; // Now this wouldn't work because connectedCallback isn't called
el.url = "https://www.example.com/";
document.body.append(frag);
It "works" although it "upsets" my code a lot, for example, instead of this.querySelector which is more natural, it becomes tmp.querySelector. Same in methods, if you do a querySelector, you have to make sure tmp is pointing to the correct Element that the children are in. I have to admit this is probably the best solution so far.
I'm not exactly sure about what makes your component so problematic, so I'm just adding what I would do:
class MyElement extends HTMLElement {
#a = document.createElement('a');
#div = document.createElement('div');
constructor() {
super().attachShadow({mode:'open'}).append(this.#a, this.#div);
console.log(this.shadowRoot.innerHTML);
}
set myText(v) { this.#a.textContent = v; }
set url(v) { this.#a.href = v; }
}
customElements.define("my-el", MyElement);
const frag = new DocumentFragment();
const el = document.createElement("my-el");
el.myText = 'foo'; el.url= 'https://www.example.com/';
frag.append(el);
document.body.append(el);
can anyone provide a solution to the problem that I'm currently encountering? I created a custom element where this custom element must have been detected on the dom, but I need to have the data contained in this custom element loaded, so my program code is like this.
import './menu-item.js';
class MenuList extends HTMLElement {
// forEach cannot be used if I use the ConnectedCallback () method
connectedCallback() {
this.render()
}
// my data can be from this method setter
set menus(menus) {
this._menus = menus;
this.render();
}
render() {
this._menus.forEach(menu => {
const menuItemElement = document.createElement('menu-item');
menuItemElement.menu = menu;
this.appendChild(menuItemElement);
});
}
}
customElements.define('menu-list', MenuList);
and this is the data I sent in the main.js file
import '../component/menu/menu-list.js';
import polo from '../data/polo/polo.js';
const menuListElement = document.querySelector('menu-list');
menuListElement.menus = polo;
please give me the solution.
The connectedCallback runs before the menus=polo statement.
So there is no this._menus declared.
If all the menus setter does is call render, then why not merge them:
set menus(menus) {
this.append(...menus.map(menu => {
const menuItemElement = document.createElement('menu-item');
menuItemElement.menu = menu;
return menuItemElement;
}));
}
Hi All I am a beginner in javaScript and currently exploring JS Web-component and I got stuck due to some use cases
1 ) I want to pass a JS Object into my component like
<my-component data=obj ></my-component>
And require to use inside my component code Like
connectedCallback () {
console.log(this.data) // it should print {"name":"xyz" , "role" : "dev"}
}
2 ) I also need to pass some functions or maybe call back functions like.
function myFunction(e){
console.log(e)
}
<my-component click=myFunction ></my-component>
please try to add code snippet also in ans that will help me to learn more JS.
Thanks
You should pass large object by Javascript.
Via a custom element method:
let comp = document.querySelector( 'my-component' )
comp.myMethod( obj )
Or setting a property:
comp.data = obj
It is best to pass in complex data using a property and not an attribute.
myEl.data = {a:1,b:'two'};
The standard on events work fine on a custom element:
function myFunction(e){
alert(JSON.stringify(e.target.data));
e.target.data = {a:1,b:"two"};
}
class MyComponent extends HTMLElement {
constructor() {
super();
this._data = 0;
this.attachShadow({mode:'open'}).innerHTML="Click Me";
}
static get observedAttributes() {
return ['data'];
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
}
}
get data() {
return this._data;
}
set data(newVal) {
this._data = newVal;
}
}
customElements.define('my-component', MyComponent);
<my-component onclick="myFunction(event)"></my-component>
If your component dispatches a custom event then it is best to access it through code:
function specialEventHandler(evt) {
// do something
}
myEl.addEventListener('special-event;', specialEventHandler);
I did a Udemy course with Andreas Galster and the tutor passed in a JSON object via attribute.
As you can see it needs encodeURIComponent and decodeURIComponent as well to
attributeChangedCallback (name, oldValue, newValue) {
if (newValue && name === 'profile-data') {
this.profileData = JSON.parse(decodeURIComponent(newValue));
this.removeAttribute('profile-data');
}
this.render();
}
Pass in:
<profile-card profile-data=${encodeURIComponent(JSON.stringify(profile))}>
</profile-card>
The code worked fine for me.
Ad 1) You need to use JSON.stringify(obj)
Ad 2) As far as I know All attributes need to be defined as strings. You can pass the function that is global and inside component try to eval(fn)
When creating an HTML custom element with a JSON string embedded by the user (though the type of string is not relevant here) ...
<my-elem>
{ "some":"content" }
</my-elem>
I would like to JSON.parse it like this ...
class MyElement extends HTMLElement {
constructor() {
super();
this.root = this.attachShadow({ mode:'open' });
this.root.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
JSON.parse(this.innerHTML);
}
}
customElements.define('my-elem', MyElement);
const template = document.createElement('template');
template.innerHTML = `irrelevant`;
... and get a perfect result with Firefox v.63.
But running this with Chrome v.71 I get
Uncaught SyntaxError: Unexpected end of JSON input
due to this.innerHTML returning an empty string.
I also tried other DOM methods to access the textual content, but all of them failed also.
Now I'm rather clueless, how to get this to work with Chrome.
Btw: Using <slot> is of no help, since I do not want to render the textual content ... only access it for parsing.
Resolved:
Put the template definition before the class definition.
Ensure having the script defining the custom element inserted behind all custom element instances in the HTML document.
Why not make it more flexible and support both a src attribute and a data property?
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['src'];
}
constructor() {
super();
this.attachShadow({ mode:'open' });
this._data = {
name: '',
address: ''
};
}
attributeChangedCallback(attrName, oldVal, newVal) {
if (oldVal !== newVal) {
const el = this;
fetch(newVal).then(resp => resp.json()).then(
data => {
el._data = data;
el.render();
}
);
}
}
render() {
this.shadowRoot.innerHTML = `
<div>
<div>${this._data.name}</div>
<div>${this._data.address}</div>
</div>`;
}
set address(val) {
this._data.address = val;
this.render();
}
set name(val) {
this._data.name = val;
this.render();
}
}
customElements.define('my-elem', MyElement);
setTimeout(() => {
let el = document.querySelector('my-elem');
el.name = 'The Joker';
el.address = 'Gothem';
setTimeout(() => {
el.setAttribute('src', 'data:application/json,%7B%22name%22:%22Thanos%22,%22address%22:%22Titan%22%7D')
}, 1500);
}, 1500);
<my-elem src="data:application/json,%7B%22name%22:%22Darth%20Vader%22,%22address%22:%22Death%20Star%22%7D"></my-elem>
The advantage to using a src attribute is that you can pass in JSON or you can pass in a URL that will return the JSON.
The properties allow you to change individual values in your DOM.
Changing the entire innerHTML may not be the right thing to do, but with small amounts of DOM it could be. You can also change individual values on the DOM or use something like LitHtml.
You should wait for the content to be present.
In most cases a simple delay could resolve the problem:
connectedCallback() {
setTimeout( () => JSON.parse(this.innerHTML) )
}
Alternatly, actually <slot> could help with the slotchange event. You can hide the rendering or remove the content if you don't want it.
I am creating a PDF like this inside a react Component.
export class Test extends React.PureComponent {
savePDF() {
const source = document.getElementById('printContainer');
/* eslint new-cap: ["error", { "newIsCap": false }]*/
let pdf = new jspdf('p', 'pt', 'letter');
let margins = { top: 50,
left: 60,
width: 612
};
pdf.fromHTML(
source,
margins.left,
margins.top,
{
width: margins.width
},
() => {
pdf.save('worksheet.pdf');
}
);
}
and I am getting warning Expected 'this' to be used by class method 'savePDF' class-me
this is being called an click like this onClick={this.savePDF} see below
render() {
<Link
name="save-to-pdf"
onClick={this.savePDF}
button="secondary">
Save to PDF</Link>
<div id="printContainer" className="cf-app-segment--alt cf-hearings-worksheet">...
There are two different answers to this question, depending on how you want to handle it.
First, the reason you get this error is because of the ESLint rule https://eslint.org/docs/rules/class-methods-use-this. Specifically, this is because if something is a class method, e.g. if you are calling this.foo() to call a function, the whole reason to make it a method is because there are properties on this that you need to use.
While in many languages with class, most functions are methods, that is not the case in JS. If you have a class like
class Example {
constructor(){
this.data = 42;
}
someMethod() {
this.someHelper(this.data);
}
someHelper(value){
console.log(value);
}
}
the someHelper function would trigger the same error you are getting, because it never uses this, so you can just as easily do
class Example {
constructor(){
this.data = 42;
}
someMethod() {
someHelper(this.data);
}
}
function someHelper(value){
console.log(value);
}
In your case, you can do this. Your whole savePDF function could be moved outside of the class object.
That said, it is important to ask yourself why something like this isn't using this. In most cases, you'd expect any function that works with HTML to absolutely use this, because how else, in React, is it supposed to access the element's that React has created.
So the real answer to your question would be to drop the
const source = document.getElementById('printContainer');
line. If you need access to the HTML element being created by React, you should be using React's APIs to do so. That would be done with something like
class SavePDFButton extends React.Component {
constructor(props) {
super(props);
this.printContainer = null;
this.savePDF = this.savePDF.bind(this);
this.handlePrintContainerRef = this.handlePrintContainerRef.bind(this);
}
render() {
return (
<div>
<Link
name="save-to-pdf"
onClick={this.savePDF}
button="secondary"
>
Save to PDF
</Link>
<div
id="printContainer"
className="cf-app-segment--alt cf-hearings-worksheet"
ref={this.handlePrintContainerRef}
/>
</div>
);
}
handlePrintContainerRef(el) {
// When React renders the div, the "ref={this.handlePrintContainerRef}" will
// make it call this function, which will store a reference.
this.printContainer = el;
}
savePDF() {
// OLD: const source = document.getElementById('printContainer');
const source = this.printContainer;
// ...
}
}
I believe that's caused by the class-methods-use-this ESLint rule.
It's just letting you know that your function doesn't use this, so you can probably make it a static function.
turn it into static function
static savePDF() { ... }
Its happening because this function isnt using this meaning it dosnt need to be dynamic