moving element from light DOM to ShadowDOM without disconnectedCallback - javascript

I wonder if I am on the right track here
Objective: Need to ensure that all elements end up in the shadowDOM
So the manually created HTML file
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>
creates the cards in lightDOM of <cardts-pile>
if I then move them to shadowDOM (ofcourse):
► <cardts-card> is removed from the DOM (triggering disconnectedCallback())
► <cardts-card> is added again (triggering connectedCallback())
[see console.log on Run Code Snipper below]
I have more fancy code in card.connectedCallback()
on 're-connect' it is basically triggering the exact same code again.
Questions
Is it possible to move nodes without DOM changes?
Is there OOTB code to check if an existing <cardts-card> is only being moved,
so connectedCallback knows it doesn't need to run code again.
Should I be doing something different,
making those lightDOM elements end up in shadowDOM immediatly?
customElements.define('cardts-pile', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
}
connectedCallback() {
console.log('connect pile');
}
});
customElements.define('cardts-card', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
}
connectedCallback() {
console.log('connect card',this.innerText);
if (!this.getRootNode().host) // not in shadowDOM
this.parentNode.shadowRoot.insertBefore(this,null);//or appendChild
}
disconnectedCallback() {
console.log('disconnect card',this.innerText);
}
});
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>

Is it possible to move nodes without DOM changes?
No (as far as I know about Shadow DOM).
Is there OOTB code to check if an existing is only being moved?
I would use a boolean flag:
connectedCallback() {
if ( !this.connected )
console.log( 'creation' )
else {
console.log( 'move' )
this.connected = true
}
(or in disconnectedCallack)
customElements.define('cardts-pile', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
this.shadowRoot.addEventListener( 'slotchange', ev => {
let node = this.querySelector( 'cardts-card' )
node && this.shadowRoot.append( node )
})
}
connectedCallback() {
console.log('connect pile');
}
});
customElements.define('cardts-card', class extends HTMLElement {
constructor(){
super();
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>';
}
connectedCallback() {
if ( !this.connected )
console.log( this.innerText + ' created' )
else
console.log( this.innerText + ' moved' )
this.connected = true
}
disconnectedCallback() {
if ( !this.moved )
console.log( 'moving ' + this.innerText );
else
console.log( 'really disconnected' )
this.moved = true
}
});
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>
Should I be doing something different?
You could instead define or upgrade <cardts-card> only after the unknown elements are moved, if possible though I don't think it's a good practice unless you can control the whole execution timing, for example with whenDefined() or with ordered HTML and Javascript code:
customElements.define('cardts-pile', Pile)
customElements.whenDefined('cardts-pile').then(() =>
customElements.define('cardts-card', Card)
)
In the example below, you define class Pile before or after class Card (depending on how they are related).
class Card extends HTMLElement {
constructor(){
super()
this.attachShadow({mode: 'open'}).innerHTML='<slot></slot>'
}
connectedCallback() {
console.log(this.innerText + ' connected')
}
disconnectedCallback() {
console.log(this.innerText + ' disconnected')
}
}
class Pile extends HTMLElement {
constructor() {
super()
this.attachShadow({mode: 'open'})
}
connectedCallback() {
console.log('connect pile')
this.shadowRoot.append(...this.querySelectorAll('cardts-card'))
}
}
window.onload = () => customElements.define('cardts-pile', Pile)
customElements.whenDefined('cardts-pile').then(() =>
customElements.define('cardts-card', Card)
)
<cardts-pile>
<cardts-card>A</cardts-card>
<cardts-card>B</cardts-card>
</cardts-pile>

Related

Javascript class members become null after page finished loading

I'm confused about how the states of members of classes in Javascript work.
I'm trying to create a dialog class which has the ability to hide the dialog when a user clicks the cancel button. I've simplified the implementation to show the problem I'm facing.
class FooClass {
bar
createBar() {
this.bar = document.createElement("ons-dialog");
this.bar.innerHTML = "this is bar content";
document.querySelector("#home").appendChild(this.bar);
this.bar.addEventListener(
'click',
this.hideBar);
this.bar.show();
}
hideBar() {
if(this.bar) {
console.log("bar exists: " + this.bar);
this.bar.hide();
} else {
console.log("bar is null!")
}
}
}
foo = new FooClass();
foo.createBar();
When I click the dialog that comes up when the page completes loading, nothing happens and the output in the console is always:
bar is null!
Why does the class lose this.bar after the page completes loading? Furthermore, how can I assign an event listener to some button which closes the dialog within this class?
Here is a codepen.io link with implementation:
https://codepen.io/moonlightcheese/pen/NWpxxMj?editors=1111
In Javascript classes, methods are not automatically bound to the instance. E.g. when using a method from a class as an event handler, like you are doing, this will point to the element you attach the listener to.
To fix that, you have two options:
In the constructor of your class, bind the method to the instance:
constructor() {
this.hideBar = this.hideBar.bind(this);
}
class FooClass {
bar;
constructor() {
this.hideBar = this.hideBar.bind(this);
}
createBar() {
this.bar = document.createElement("ons-dialog");
this.bar.innerHTML = "this is bar content";
document.querySelector("#home").appendChild(this.bar);
this.bar.addEventListener(
'click',
this.hideBar);
this.bar.hidden = false;
}
hideBar() {
if (this.bar) {
console.log("bar exists: " + this.bar.outerHTML);
this.bar.hidden = true;
} else {
console.log("bar is null!")
}
}
}
foo = new FooClass();
foo.createBar();
<div id="home"></div>
Use a class property and assign an arrow function:
hideBar = () => { /* your function code */ }
class FooClass {
bar;
createBar() {
this.bar = document.createElement("ons-dialog");
this.bar.innerHTML = "this is bar content";
document.querySelector("#home").appendChild(this.bar);
this.bar.addEventListener(
'click',
this.hideBar);
this.bar.hidden = false;
}
hideBar = () => {
if (this.bar) {
console.log("bar exists: " + this.bar.outerHTML);
this.bar.hidden = true;
} else {
console.log("bar is null!")
}
}
}
foo = new FooClass();
foo.createBar();
<div id="home"></div>
Please note that due to the missing custom element definition, the custom element ons-dialog does not have a show and hide method (it's simply an HTMLUnknownElement here in the snippet), which is why I replaced that functionality in the snippets using the standard DOM hidden API.

Is there a way to append html to web component?

I've a custom js web component:
class MainDiv extends HTMLElement {
constructor() { super(); }
connectedCallback() {
this.innerHTML = '<div id="mydiv"><input type="text" oninput="onInput(event)"/></div>'
}
}
and in main.js I've the function onInput*()
function onInput(event) {
const mainDiv = document.getElementById("mydiv");
const newLabel = document.createElement('span');
newLabel.innerText = `${event.target.value.length}/255`; // output: 5/255...n/255
mainDiv.appendChild(newLabel);
}
If I add a log in the onInput function it prints and does not return any error, but is not updating the webcomponent. Why?
There must be something else at play, your code works:
<script>
function onInput(event) {
const mainDiv = document.getElementById("mydiv");
const newLabel = document.createElement('div');
newLabel.innerText = `${event.target.value.length}/255`; // output: 5/255...n/255
mainDiv.appendChild(newLabel);
}
customElements.define("my-element", class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<div id="mydiv"><input type="text" oninput="onInput(event)"/></div>'
}
});
</script>
<my-element></my-element>
Note
constructor() {
super();
}
Is not required, it says run the constructor from my parent, which is also done when you leave out the constructor in your Component.
Only when you do more in the constructor is it required.

What is the equivalent of document.getElementById("root").appendChild(node) in React?

How do I get this functionality using React?
function myFunction() {
var node = document.createElement("div");
var textnode = document.createTextNode("Button was clicked.");
node.appendChild(textnode);
document.getElementById("root").appendChild(node);
}
in your render function write the div you want and make a state
then change a state to true to the div to be shown
somthing like this
constructor(props) {
super(props)
this.state = {
showDiv: false
}
}
render (){
return (
{
this.state.showDiv ? your div : <React.Fragment />
}
)
}
changeDivDisplay(){
this.setState({showDiv:true})
}
also i do recommend you to read about the state and the refs of react

Uncaught TypeError: xyz is not a function (when calling it from inside string)

Trying to bind the validate() function on blur of input as declared inside render function of class Textbox extends HTMLElement (Part-A), but getting the below error
detail.html:1 Uncaught TypeError: this.validate is not a function
(but the validate() function is accessible when placing outside the call of the function outside string)
Part - A
class HTMLElement {
constructor( ) {
this.value = false;
}
validate() {
console.log('A')
}
}
class Textbox extends HTMLElement {
constructor( ) {
super();
}
render( elem_id, default_value, display_name, placeholder, permission ) {
/*** this.validate() will work here but not inside onblur function ***/
var string = "";
string += "<input type='text' class='form-control' id='"+elem_id+"' value='"+default_value+"' placeholder='" + placeholder + "' onblur='this.validate()' >"
return string;
}
}
Part B
const ELEMENT_ARR = {
"text":Textbox
}
class Elements {
constructor( className, opts ) {
return new ELEMENT_ARR[className](opts);
}
}
function renderForm(){
var ds = {
"fields":[
{"id":"f_name","type":"text","is_mandatory":1,"display_name":"First Name","default":"", "permission":"hidden/editable/readonly","placeholder":"Sample Placeholder 1" },
{"id":"f_name","type":"text","is_mandatory":0,"display_name":"Hellp Name","default":"","permission":"hidden/editable/readonly", "placeholder":"Sample Placeholder 2" }
]
}, ks = JSON.parse(JSON.stringify(ds)), form_str="", elementor;
for (let u = 0 ; u < ks['fields'].length; u++) {
let type = ks['fields'][u].type,
elem_id = ks['fields'][u].id,
is_mandatory = ks['fields'][u].is_mandatory,
default_value = ks['fields'][u].default,
display_name = ks['fields'][u].display_name,
placeholder = ks['fields'][u].placeholder,
permission = ks['fields'][u].permission;
elementor = new Elements( type )
form_str += elementor.render( elem_id, default_value, display_name, placeholder, permission )
}
return form_str;
}
Part - C
window.onload = function(){
document.getElementById('formGenx').innerHTML = renderForm();
};
What possibly wrong I am doing?
The problem is that you're generating a string then appending it to the DOM and only at that point it will be parsed into a DOM element and added to the page. This happens outside the context of your render method and has no link to either it or the object that created it. Moreover, the event handler is just set to the string "this.validate()" which will be executed in the global context.
To retain access to your validate method you have to attach it as a handler at the time render executes. The best way to go about this, in my opinion, is to just avoid the step where you generate a string and programmatically create the element:
document.createElement will create the node
.setAttribute() can add attributes
.classList and .add() any classes you need.
instead of setting the onblur attribute, .addEventListener() to add a proper handler as the direct attribute but note that sometimes you might prefer the attribute.
This will generate the elements, you can then accumulate them in a DocumentFragment before adding them all at once to the DOM which will save you multiple insertions and potential performance hit because of triggering reflow.
Finally, the elements can be added via the normal DOM API but usually .appendChild is enough.
So, we get the following:
class HTMLElement {
constructor( ) {
this.value = false;
}
validate() {
console.log('A')
}
}
class Textbox extends HTMLElement {
constructor( ) {
super();
}
render( elem_id, default_value, display_name, placeholder, permission ) {
/*** programmatically create the input element and set all relevant attributes ***/
var element = document.createElement("input");
element.setAttribute("type", "text");
element.classList.add("form-control");
element.setAttribute("id", elem_id);
element.setAttribute("value", default_value);
element.setAttribute("placeholder", placeholder);
element.addEventListener("blur", () => this.validate())
return element;
}
}
const ELEMENT_ARR = {
"text":Textbox
}
class Elements {
constructor( className, opts ) {
return new ELEMENT_ARR[className](opts);
}
}
function renderForm(){
var ds = {
"fields":[
{"id":"f_name","type":"text","is_mandatory":1,"display_name":"First Name","default":"", "permission":"hidden/editable/readonly","placeholder":"Sample Placeholder 1" },
{"id":"f_name","type":"text","is_mandatory":0,"display_name":"Hellp Name","default":"","permission":"hidden/editable/readonly", "placeholder":"Sample Placeholder 2" }
]
},
ks = JSON.parse(JSON.stringify(ds)),
//just a "container" for the elements.
//can be cahged to document.createElement("form") if needed
form = document.createDocumentFragment(),
elementor;
for (let u = 0 ; u < ks['fields'].length; u++) {
let type = ks['fields'][u].type,
elem_id = ks['fields'][u].id,
is_mandatory = ks['fields'][u].is_mandatory,
default_value = ks['fields'][u].default,
display_name = ks['fields'][u].display_name,
placeholder = ks['fields'][u].placeholder,
permission = ks['fields'][u].permission;
elementor = new Elements( type );
let newElement = elementor.render( elem_id, default_value, display_name, placeholder, permission );
form.appendChild(newElement);//append as DOM elements
}
return form;
}
window.onload = function(){
document.getElementById('formGenx')
.appendChild(renderForm()); //append as DOM element
};
.form-control {
margin: 5px;
border-style: dashed;
border-color: black
}
<div id="formGenx"></div>
The normal JavaScript API tends to be verbose when programmatically creating an element. Libraries like jQuery can actually make it a bit easier at the cost of another dependency, of course. While jQuery is not strictly needed, I'll just demonstrate how programmatic the element creation would work with it for reference:
var element = $("<input>")
.attr("type", "text")
.addClass("form-control")
.id(elem_id)
.val(default_value)
.attr("placeholder", placeholder)
.on("blur", () => this.validate())
return element;

How to handle onClick events for array objects in ReactJS?

I am trying to add an onClick event handler to objects in an array where the class of a clicked object is changed, but instead of only changing one element's class, it changes the classes of all the elements.
How can I get the function to work on only one section element at a time?
class Tiles extends React.Component {
constructor(props) {
super(props);
this.state = {
clicked: false,
content : []
};
this.onClicked = this.onClicked.bind(this);
componentDidMount() {
let url = '';
let request = new Request(url, {
method: 'GET',
headers: new Headers({
'Content-Type': 'application/json'
})
});
fetch(request)
.then(response => response.json())
.then(data => {
this.setState({
content : data
})
} );
}
onClicked() {
this.setState({
clicked: !this.state.clicked
});
}
render() {
let tileClass = 'tile-content';
if (this.state.clicked) {
tileClass = tileClass + ' active'
}
return (
<div className = 'main-content'>
{this.state.pages.map((item) =>
<section key = {item.id} className = {tileClass} onClick = {this.onClicked}>
<h4>{item.description}</h4>
</section>)}
<br />
</div>
)
}
class App extends React.Component {
render() {
return (
<div>
<Tiles />
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('content-app'))
You have onClicked() define in your 'main-content' class. So that's where it fires.
constructor(props) {
// super, etc., code
this.onClicked = this.onClicked.bind(this); // Remove this line.
}
Remove that part.
You can keep the onClicked() function where it is. Your call in render() is incorrect, though: onClick = {this.onClicked}>. That accesses the onClicked ATTRIBUTE, not the onClicked FUNCTION, it should be this.onClicked().
Let me cleanup your call in render() a little bit:
render() {
let tileClass = 'tile-content';
return (
<div className = 'main-content'>
// some stuff
<section
key={item.id}
className={tileClass}
onClick={() => this.onClicked()} // Don't call bind here.
>
<h4>{item.description}</h4>
</section>
// some other stuff
</div>
)
}
It is happening for you, because you are assigning active class to all sections once user clicked on one of them. You need somehow to remember where user clicked. So I suggest you to use array, where you will store indexes of all clicked sections. In this case your state.clicked is an array now.
onClicked(number) {
let clicked = Object.assign([], this.state.clicked);
let index = clicked.indexOf(number);
if(index !== -1) clicked.splice(index, 1);
else clicked.push(number)
this.setState({
clicked: clicked
});
}
render() {
let tileClass = 'tile-content';
return (
<div className = 'main-content'>
{this.state.pages.map((item, i) => {
let tileClass = 'tile-content';
if(this.state.clicked.includes(i)) tile-content += ' active';
return (
<section key = {item.id} className = {tileClass} onClick = {this.onClicked.bind(this, i)}>
<h4>{item.description}</h4>
</section>
)
})}
<br />
</div>
)
}
StackOverflow does a particularly poor job of code in comments, so here's the implementation of onClicked from #Taras Danylyuk using the callback version of setState to avoid timing issues:
onClicked(number) {
this.setState((oldState) => {
let clicked = Object.assign([], this.state.clicked);
let index = clicked.indexOf(number);
if(index !== -1) {
clicked.splice(index, 1);
} else {
clicked.push(number);
}
return { clicked };
});
}
The reason you need this is because you are modifying your new state based on the old state. React doesn't guarantee your state is synchronously updated, and so you need to use a callback function to make that guarantee.
state.pages need to keep track of the individual click states, rather than an instance-wide clicked state
your onClick handler should accept an index, clone state.pages and splice your new page state where the outdated one used to be
you can also add data-index to your element, then check onClick (e) { e.currentTarget.dataset.index } to know which page needs to toggle clickstate

Categories