Is there a way to wrap each individual element slotted into the shadow dom for a specific slot name?
Assume the Markup looks similar to this
<custom-element>
<div name="item">item 1</div>
<div name="item">item 2</div>
</custom-element>
Currently, the render is similar to:
<custom-element>
<div class="wrap">
<div name="item">item 1</div>
<div name="item">item 2</div>
</div>
</custom-element>
How would I go about wrapping the slotted elements to output similar to:
<custom-element>
<div class="wrap">
<div name="item">item 1</div>
</div>
<div class="wrap">
<div name="item">item 2</div>
</div>
</custom-element>
My current (flawed) approach:
customElements.define('custom-element', class MyCustomElement extends HTMLElement {
constructor(...args) {
super(...args);
let shadow = this.attachShadow({mode: open});
shadow.innerHTML = `
<div class="wrap">
<slot name="item"></slot>
</div>
`;
}
});
I would recommend that you change your approach as to the wrapping of the children. The simplest way would be to just add the missing HTML as a child of the slotted div like the example below. You would still be able to style the slotted element with the ::slotted pseudo selector.
customElements.define('custom-element', class MyCustomElement extends HTMLElement {
constructor(...args) {
super(...args);
let shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
::slotted([slot="item"]) {
border: 1px solid black;
padding: 15px;
}
</style>
<slot name="item"></slot>
`;
}
});
<custom-element>
<div slot="item">
<div class="wrap">item 1</div>
</div>
<div slot="item">
<div class="wrap">item 2</div>
</div>
</custom-element>
The reason behind this approach is that the wrap ultimately belongs to the child element and should come with each child. The result would be similar to what you request.
Although, if you do want to add wrapping to the element dynamically, then you could do with the slotchange event. The event is fired whenever a slot has been filled and can be listened to from the ShadowRoot element. In the event callback loop over the assignedElements (which are the elements in the slot) and alter their innerHTML value.
customElements.define('custom-element', class MyCustomElement extends HTMLElement {
constructor(...args) {
super(...args);
let shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `<slot name="item"></slot>`;
shadow.addEventListener('slotchange', event => {
const { target } = event;
const assignedElements = target.assignedElements();
for (const element of assignedElements) {
if (element.querySelector('.wrap') === null) {
const wrapper = document.createElement('div');
wrapper.className = 'wrap';
for (const node of element.childNodes) {
wrapper.appendChild(node);
}
element.append(wrapper);
}
}
});
}
});
.wrap {
border: 1px solid black;
padding: 15px;
}
<custom-element>
<div slot="item">item 1</div>
<div slot="item">
<div class="wrap">item 2</div>
</div>
</custom-element>
I have faced the same problem, and this is the solution I came up with: re-targeting the item slots. I have a single "main" slot, which should always be empty - on every slotchange event, I iterate over the new items, create a new wrapper for each of them and create a new with dynamically generated name inside each wrapper. Then I change the "slot" attribute of the original item to move it to that slot.
customElements.define('buttons', class extends HTMLElement {
__allocateId()
{
var i = 0;
while ( this.__buttons.has( "btn" + i ) ) {
++i;
}
return "btn" + i;
}
__onButtonSlotChange(button)
{
if (button.contentSlot.assignedNodes({flatten: true}).length == 0) {
this.__buttons.delete(button.id);
button.remove();
}
}
__onMainSlotChange() {
for (var element of this.mainSlot.assignedElements({flatten: true})) {
var button = document.createElement('arrow-button');
button.id = this.__allocateId();
this.shadowRoot.appendChild(button);
var subSlot = document.createElement('slot');
subSlot.name = button.id;
button.appendChild(subSlot);
button.contentSlot = subSlot;
subSlot.addEventListener('slotchange', this.__onButtonSlotChange.bind(this, button));
element.setAttribute("slot", button.id);
this.__buttons.set(button.id, button);
}
}
constructor() {
super();
this.__buttons = new Map();
}
connectedCallback() {
var shadowRoot = this.attachShadow({mode: 'open'});
this.mainSlot = document.createElement('slot');
shadowRoot.appendChild(this.mainSlot);
var buttonList = document.createElement('div');
shadowRoot.appendChild(buttonList);
this.mainSlot.addEventListener('slotchange', this.__onMainSlotChange.bind(this));
}
});
This has minimal impact on the original objects and the entire component behaves correctly upon any modifications to the "buttoned" code - eg. item removal triggers removal of the corresponding button.
Related
I have the following function component. Within it, when a user clicks on any of the 4 divs, note_b, note_g, note_p, note_y, I want that class name to then be appended to the div with className note
This is my (incomplete) code
import React from 'react-dom';
import DraggableCore from 'react-draggable';
function Note(props) {
return (
<DraggableCore defaultPosition={{x: 1000, y: 200}}>
<div className={"note " + }>
<div id="note_head">
<div id="note_bin"></div>
<div className="note_b" onClick={}></div>
<div className="note_p" onClick={}></div>
<div className="note_g" onClick={}></div>
<div className="note_y" onClick={}></div>
<div id="note_exit"></div>
</div>
<p>
{props.message}
</p>
</div>
</DraggableCore>
)
}
export default Note;
Thank you #Andy, I took a second look at state hooks and came up with this:
import React, { useState } from 'react';
import DraggableCore from 'react-draggable';
function Note(props) {
const [bg, setBG] = useState('note_bg_b');
return (
<DraggableCore defaultPosition={{x: 1000, y: 200}}>
<div className={"note " + bg}>
<div id="note_head">
<div id="note_bin"></div>
<div className="note_b" onClick={() => setBG('note_b')}></div>
<div className="note_p" onClick={() => setBG('note_b')}></div>
<div className="note_g" onClick={() => setBG('note_b')}></div>
<div className="note_y" onClick={() => setBG('note_b')}></div>
<div id="note_exit"></div>
</div>
<p>
{props.message}
</p>
</div>
</DraggableCore>
)
}
export default Note;
This can probably be done in a cleaner, more efficient fashion. It is however functional.
You can use the onClick event handler for each of the four divs and add the className that was clicked on to the div with the className note.
EDIT for additional question: To prevent the added className from unloading when clicking within the note div, we can use an event listener to check where the click originated from and if it did not come from an element with the note_b, note_p, note_g, or note_y className, then the className should not be removed.
import React, { useState, useRef } from 'react';
import DraggableCore from 'react-draggable';
function Note(props) {
const [className, setClassName] = useState('');
const noteDiv = useRef(null);
const handleClick = e => {
setClassName(e.target.className);
}
//This function prevents the default event action from occurring when the page is unloaded.
//If the target element of the event does not have one of the specified class names, it removes the class from the element with the class "note".
const handleUnload = e => {
e.preventDefault();
const noteDiv = document.querySelector('.note');
if (!['.note', 'note_b', 'note_p', 'note_g', 'note_y'].includes(e.target.className)) {
noteDiv.classList.remove(e.target.className);
}
}
return (
<DraggableCore defaultPosition={{ x: 1000, y: 200 }}>
<div ref={noteDiv} className={`note ${className}`} onClick={handleUnload}>
<div id="note_head">
<div id="note_bin"></div>
<div className="note_b" onClick={handleClick}></div>
<div className="note_p" onClick={handleClick}></div>
<div className="note_g" onClick={handleClick}></div>
<div className="note_y" onClick={handleClick}></div>
<div id="note_exit"></div>
</div>
<p>
{props.message}
</p>
</div>
</DraggableCore>
)
}
export default Note;
If you separate out your classes a little, and add a data attribute for each note, you might get closer to what you need.
Instead of a className that looks like node_b use two classes note b - note can be the general class for all notes, and b can be the one that specifies one particular note. I've used colours here for clarity.
Adding the data attribute makes it more easy to identify each note in the code. In the click handler you can destructure that note id from the dataset of the clicked element, and then use it to set state, and you can use that state in the containing element.
Note: I've only used one click handler on the notes' containing element so that I can use event delegation.
const { useState } = React;
function Note({ message }) {
// Initialise a new state to hold the note id
const [ noteClass, setNoteClass ] = useState('');
// The handler first checks to see if the
// clicked element is a "note" element.
// if it is it destructures the note id from the
// element's dataset, and then uses it to set state
function handleClick(e) {
if (e.target.matches('.note')) {
const { note } = e.target.dataset;
setNoteClass(note);
}
}
// When the state changes the containing element's
// class changes too.
return (
<div className={noteClass}>
<div id="note_head">
<div id="note_bin" onClick={handleClick}>
<div data-note="b" className="note b">B</div>
<div data-note="p" className="note p">P</div>
<div data-note="g" className="note g">G</div>
<div data-note="y" className="note y">Y</div>
</div>
<p>{message}</p>
</div>
</div>
);
}
ReactDOM.render(
<Note message="Message" />,
document.getElementById('react')
);
.note { padding: 0.25em; border: 1px solid #4444; }
.note:not(:last-child) { margin-bottom: 0.25em; }
.note:hover { background-color: #fffff0; cursor: pointer; }
.b { color: red; }
.p { color: blue; }
.g { color: green; }
.y { color: gray; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
I'm a beginner in javascript with HTML and CSS. I want to try is there a way to access child container class via parent container class. or can I add a new class("second_new") to "second" class via "first" class.
/* CSS */
.first {
background-color: red;
}
.first_new {
background-color: pink;
}
.second {
background-color: blue;
}
.second_new {
background-color: purple;
}
<!-- HTML -->
<div class="row">
<div class="first">
<h1>This is first class</h1>
<div class="second"> <!-- I want to change this -->
<h2>This is Second class</h2>
</div>
</div>
<div class="first">
<h1>This is first class</h1>
<div class="second"> <!-- I want to change this -->
<h2>This is Second class</h2>
</div>
</div>
</div>
<!-- JAVASCRIPT -->
<script>
var firstClass = document.getElementsByClassName("first");
function Mousein() {
this.classList.add("first_new");
};
function Mouseout() {
this.classList.remove("first_new");
};
for (var i = 0; i < firstClass.length; i++) {
firstClass[i].addEventListener('mouseover', Mousein);
firstClass[i].addEventListener('mouseout', Mouseout);
}
</script>
yes you can
Method 1
document.querySelector('.first .second');
Medthod 2
let parent = document.querySelector('.first');
parent.querySelector('.second');
Thanks Guys I found the answer this
/* CSS */
.first {
background-color: red;
}
.first_new {
background-color: pink;
}
.second {
background-color: blue;
}
.second_new {
background-color: purple;
}
<!-- HTML -->
<div class="row">
<div class="first">
<h1>This is first class</h1>
<div class="second"> <!-- I want to change this -->
<h2>This is Second class</h2>
</div>
</div>
<div class="first">
<h1>This is first class</h1>
<div class="second"> <!-- I want to change this -->
<h2>This is Second class</h2>
</div>
</div>
</div>
<!-- JAVASCRIPT -->
<script>
var firstClass = document.getElementsByClassName("first");
var child;
function Mousein() {
this.classList.add("first_new");
child = this.querySelector(".second");
child.classList.add("second_new")
};
function Mouseout() {
this.classList.remove("first_new");
child.classList.remove("second_new")
};
for (var i = 0; i < firstClass.length; i++) {
firstClass[i].addEventListener('mouseover', Mousein);
firstClass[i].addEventListener('mouseout', Mouseout);
}
</script>
Yes you can access bey selector. document.querySelector('parent child') . In your case would be: const childEl = document.querySelector('.first .second');
You can use getElementsByTagName() on any type of element.
This would be
var parents = document.getElementsByClassName('parent');
var child = [];
for (let i = 0; i < parents.length; i++) {
var child = parents.getElementsByTagName('div')[0];
children.push(child);
}
Or, Even Simpler:
var parents = document.querySelectorAll('.parent');
var children = document.querySelectorAll('.parent > div');
Note: Elements selected by querySelectorAll() are like arrays and array methods can be applied.
Note: To select one element use querySelector() method.
I have two div's within a parent div. I need to change the classes for the child div which I clicked. For that I am writing a method to check which child was clicked and respectively I am trying to hide the other child div.
But I am not able to add classes or remove classes since the index is showing always as undefined. I am feeling there is some problem with the return statement.
function changeClass() {
const list = document.getElementById('my_div').children;
const indx = this.getIndexOfParent(list);
for (let i = 0; i < list.length; i++) {
if (indx === 0) {
list[indx + 1].classList.add("d-none d-sm-block");
list[indx].classList.remove("col-6 d-none d-sm-block");
} else if (indx === 1) {
list[indx - 1].classList.add("d-none d-sm-block");
list[indx].classList.remove("col-6 d-none d-sm-block");
}
list[indx].classList.add("d-xs-block");
}
}
function getIndexOfParent(child_list) {
for (var i = 0, len = child_list.length; i < len; i++) {
((index) => {
child_list[i].onclick = () => {
return index;
};
})(i);
}
}
.row {
background: #f8f9fa;
margin-top: 20px;
}
.row > div {
border: solid 1px black;
padding: 10px;
}
<div class="container">
<div class="row">
<div onclick="changeClass()" class="col-md-6 col-6">
child-div-1
</div>
<div onclick="changeClass()" class="col-md-6 col-6">
child-div-2
</div>
</div>
</div>
All I want is that, when I click on child-div-1 it should hide child-div-2 and vice versa only for small screens (which is why I am handling it by col-6 and d-xs-block classes)
Can anyone help me to solve the below problem.
You have added onclick within the for loop. Instead add the class to the clicked child div and remove the class from it's sibling div.
document.querySelectorAll('div.row > div')
.forEach((div) => {
div.addEventListener('click', function({
target
}) {
target.classList.add('d-none', 'd-sm-block');
const sibDiv = Array.prototype.filter.call(target.parentNode.children, div => div != target)[0];
sibDiv.classList.remove('col-6', 'd-none', 'd-sm-block');
});
});
.row {
background: #f8f9fa;
margin-top: 20px;
}
.row>div {
border: solid 1px black;
padding: 10px;
}
<div class="container">
<div class="row">
<div class="col-md-6 col-6">
child-div-1
</div>
<div class="col-md-6 col-6">
child-div-2
</div>
</div>
</div>
-- Edit --
The return statement will return the value of index to the callback function, you also need to add return to the callback function, so whatever result the callback function get will return to the function getIndexOfParent.
function getIndex() {
let i = 0;
((index) => { // No return, logs undefined
return index;
})(i);
}
console.log(getIndex());
function getIndex() {
let i = 0;
return ((index) => { // with return
return index;
})(i);
}
console.log(getIndex());
I made a pen for solving this problem.
Check the pen here
the solution is easy you simply have to write this line
e.stopPropagation();
this will stop the event from triggering on parent divs
basically what you are describing is called event bubbling.
you can read about it more on medium
Okay so I'm making a dropdown for my social media website, and I wanted to add a slider button:
<div class = \"dropdown\">
<img id = \"navPFP\" style = \"margin-top: 2px;margin-left:20px; margin-right: 20px;\" class = \"pfp\" width = \"30\" height = \"30\" src=\"$pfpNAV\" alt=\"$userNAV's pfp\">
<div id = \"dropdown\" class = \"dropdown-content\">
<div id = \"names\" style = \"border-bottom: thin solid #BDBDBD;\">
<h2>$fnNAV $lnNAV</h2>
<p style = \"color:grey;margin-top:-40px;\">#$userNAV</p>
</div>
<div id = \"settings\" style = \"border-bottom: thin solid #BDBDBD;\">
Accout Settings
</div>
<label class = \"switch\">
<input type = \"checkbox\">
<span class = \"slider round\"></span>
</label>
Log out #$userNAV
Reset password #$userNAV
</div>
Problem is: unless it is just the #dropdown div, not any children, when you click it the dropdown closes
I have the following JS code to close the dropdown when anything other than the dropdown content is clicked:
window.onclick = function(event) {
if (!event.target.matches('#dropdown') && !event.target.matches('#navPFP')) {
var dropdowns = document.getElementsByClassName("dropdown-content");
var i;
for (i = 0; i < dropdowns.length; i++) {
var openDropdown = dropdowns[i];
if (openDropdown.classList.contains('show')) {
openDropdown.classList.remove('show');
}
}
}
}
Even if I add a && !event.target.matches('.switch') (or any of the other div ids) to the if statement, the dropdown still closes when the slider is clicked. How can I fix this so that the dropdown stays open?
Instead of matches(), use closest():
if (!event.target.closest('#dropdown')) {
// target is neither #dropdown or one of its descendants; close the dropdown
}
element.closest('#dropdown') starts at element and walks upward through the DOM looking for #dropdown. If closest() finds #dropdown, it returns it, and element must be a child of #dropdown. If not, it returns null, and element must be outside #dropdown.
Recently, I have the same problem with event.target.matches().
Apart from using event.target.closest(), you can set the pointer-events CSS property to none.
document.querySelector("#container").addEventListener("click", (event) => {
if (event.target.matches(".click-here")) {
console.log("You clicked inside 'click-here' div.");
const result = document.querySelector("#result");
result.innerText = "You clicked inside 'click-here' div.";
}
})
.no-click {
pointer-events: none;
}
<div id="container">
<div class="click-here">
<div class="no-click">
<div>div1</div>
<div>
<div>nested</div>
</div>
<p>text</p>
</div>
</div>
</div>
<div id="result">
</div>
Whenever I click on a container which is also the h1. But I want to do when I click on the container. I want to make its h1 color blue. Im stuck on the part making the h1 blue.
var container = document.getElementsByClassName('container');
var h1 = document.getElementsByTagName('h1');
// Put event listener on each container
for(var i = 0; i < container.length; i++) {
container[i].addEventListener('click', function() {
// This isn't working
h1[i].style.color = 'blue';
})
}
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
You are referring to the wrong element when you used h1[i].style...
Use this instead and it will work fine. See code below:
var container = document.getElementsByClassName('container');
var h1 = document.getElementsByTagName('h1');
// Put event listener on each container
for(var i = 0; i < container.length; i++) {
container[i].addEventListener('click', function() {
// Get the 1st H1 inside current container
this.getElementsByTagName('h1')[0].style.color = 'blue';
})
}
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
var container = document.getElementsByClassName('container');
var h1 = document.getElementsByTagName('h1');
// Put event listener on each container
for(var i = 0; i < container.length; i++) {
container[i].addEventListener('click', function() {
this.firstElementChild.style.color="blue"
})
}
Inside the container[i].addEventListener('click', ....) this is the HTML Element of the container. Therefore, calling this.firstElementChild will grab the H1 of that container and change it's color to blue. If you add anything before the H1, just call the children function on this and grab the h1 element.
Neither of the answers that have been posted directly address the issue. They rely on changing the entire color of the .container element, or on the <h1> always being the immediate first child of .container.
The problem that you have is that you have is that your i variable is out of scope, and cannot be used inside the event handler you're defining. We can get around this by wrapping the handler function in a closure as follows:
var container = document.getElementsByClassName('container'),
h1 = document.getElementsByTagName('h1');
for(var i = 0; i < container.length; i++)
{
container[i].onclick = (function(i) {return function() {
h1[i].style.color = 'blue';
};})(i);
};
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
<div class="container">
<h1>HELLO</H1>
</div>
Notice that the above will only change the colour of the <h1>. Of course, this assumes that all of your <h1> elements are always matched in number and index by the .container elements (but I assume that they are, from your question).