How can I access an element which is deep inside the shadow-root?
<vaadin-combo-box>
#shadow-root
<vaadin-text-field id="input">
<vaadin-combo-box-dropdown-wrapper id="overlay">
#shadow-root(open)
<vaadin-combo-box-dropdown id="dropdown">
#shadow-root(open)
<vaadin-combo-box-overlay id="overlay">
#shadow-root(open)
<div part="overlay" id="overlay">
<div part="content" id="conent">
#shadow-root(open)
<div id="scroller">
<iron-list id="selector">
#shadow-root(open)
<vaadin-combo-box-item>
......
I want to style a vaadin-combo-box-item element but I don't know how to access this element.
There's no easy answer to that because you have to access a very deep DOM element.
To make it little bit less painful you have to make a function which access provided shadow dom of element like this:
const getShadowRoot = (elem, selector) => elem.shadowRoot.querySelector(selector);
const vaadinComboBox = getShadowRoot(document, 'vaadin-combo-box');
const vaadinTextField = getShadowRoot(vaadinComboBox, '#input');
const vaadinComboBoxWrapper = getShadowRoot(vaadinTextField, '#overlay');
const vaadinComboBoxDropdown = getShadowRoot(vaadinComboBoxWrapper, '#dropdown');
const vaadinComboBoxOverlay = getShadowRoot(vaadinComboBoxDropdown, '#overlay');
const vaadinComboBoxContent = getShadowRoot(vaadinComboBoxOverlay, '#conent');
const vaadinComboBoxSelector = getShadowRoot(vaadinComboBoxContent, '#selector');
const vaadinComboBoxItem = getShadowRoot(vaadinComboBoxContent, 'vaadin-combo-box-item');
Nevertheless, this amount of shadowDom elements looks like an architectural mistake
I couldn't find an answer to get an element at an arbitrary depth. This is what I came up with; you can use a function of the sorts to recursively descend into the shadow DOM, to get either the parent or the element itself:
function* descend(el, sel, parent) {
if (el.matches(sel)) {
yield parent ? el.parentElement : el;
}
if (el.shadowRoot) {
for (const child of el.shadowRoot.children) {
yield* descend(child, sel, parent);
}
}
for (const child of el.children) {
yield* descend(child, sel, parent);
}
};
Example use:
const vid = [...descend(window.parent.document.querySelector("body"), "video", false)][0]
Related
I was trying to test a few novice tricks from a project tutorial. Wanted to create a small scale task app and ran into a weird problem. The last document.addEventListener below should theoretically call the closest element with the class name of ".name" should be detected since its in the same parent div with the button. However it is returning NULL. Am I applying the .closest() method wrong?
The event listener detects the button after everytime a task is created. Not sure why it returns NULL when, after creating the task via addTaskButton, the task with the class name of ".name". I even tried to create a data attribute id based off of the taskName itself to see if it'll detect, but still NULL / erroring.
const list = []
const container = document.querySelector('.container');
const itemContainer = document.querySelector('.item');
const addTaskButton = document.querySelector('.add-task');
const taskInput = document.querySelector('#task-name');
function renderTasks(){
itemContainer.innerHTML = ''
list.forEach(task => {
const itemElement = document.createElement('div')
itemElement.innerHTML = `
<div class="name">
${task.taskName}
</div>
<button class="retrieval">Retrieve ID</button>
`
itemElement.dataset.itemName = task.taskName
itemContainer.appendChild(itemElement);
})
}
addTaskButton.addEventListener('click', (e)=>{
e.preventDefault();
list.push({ taskName: taskInput.value})
renderTasks()
})
document.addEventListener('click', (e)=>{
if(e.target.matches('.retrieval')){
const taskName = e.target.closest('.name');
console.log(taskName)
}
})
Ok, I double checked the mdn article it says:
closestElement is the Element which is the closest ancestor of the
selected element. It may be null.
That means it only looks for parents and parents of parents and so on, not 'siblings'.
I have an array of data from which I am creating elements. And when I hang the event handler, then the dataset.container is undefined. How do I get to the attributes, or how do I create elements differently to get the data-category?
const cards = [
[],
[],
[]
]
function renderCategory(card) {
let str = '';
for (let i = 0; i < card.length; i++) {
str += `<div class="card card-category" data-category="${i}">
<div class="card-image">
<img src="${card[i].image}">
</div>
<div class="card-desc">
<div class="card-text">${card[i].name}</div>
</div>
</div>`;
}
return str;
}
let categoryCard = cards[0];
function createCard(cards) {
const main = document.getElementById('category');
main.innerHTML += renderCategory(cards);
}
createCard(categoryCard)
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('#category').addEventListener('click', ({
target: {
dataset
}
}) => {
console.log(dataset.category) // undefined
})
})
So several things
For one, I could not recognise your addEventListener('click', ({ target: { dataset } construction as valid JavaScript.
I have tried to not change a lot but I
return complete set of cards
delegate from container correctly - clicking anywhere in the card will return the category
let categoryCard;
const cards = [
{ image : "https://via.placeholder.com/128x90.png?text=Card1", name:"Card 1"},
{ image : "https://via.placeholder.com/128x90.png?text=Card2", name:"Card 2"},
{ image : "https://via.placeholder.com/128x90.png?text=Card3", name:"Card 3"}
]
function renderCategory(cards) {
return cards.map((card,i) => (
`<div class="card card-category" data-category="${i}">
<div class="card-image">
<img src="${card.image}">
</div>
<div class="card-desc">
<div class="card-text">${card.name}</div>
</div>
</div>`)).join("");
}
function createCard(cards) {
const main = document.getElementById('category');
main.innerHTML += renderCategory(cards);
categoryCard = main.querySelector(".card-category"); // first card
}
window.addEventListener('load', () => {
document.querySelector('#category').addEventListener('click', e => {
const tgt = e.target;
const parent = tgt.classList.contains("card-category") ? tgt : tgt.closest(".card-category")
console.log(parent && parent.dataset.category ? parent.dataset.category : "clicked somewhere else")
})
createCard(cards)
})
<div id="category"></div>
event.target refers to the lowest Element of the DOM tree on which the event was triggered. In other words, depending on where you click the target can be the card-category DIV, the card-image DIV, the card-desc DIV, the card-text DIV or the IMG element. That's why you're not able to access the data-attribute which is only set on the parent.
If you want to always get the Element to which you bound the event, use event.currentTarget instead. But in your case, you bound the event to the main #category wrapper. If you want to bind the event to the element that has the data-category attribute, you'll have to reorganize your code to use document.createElement instead of producing HTML strings.
Keep in mind you should append elements to the page before binding events to them.
I'm currently trying to make a recursive function that takes html elements as an array so I can take html elements like the querySelector function
The reason i'm doing this is because I can't use getElementsByTagName() or querySelector()
Here is my code:
function flatten(items)
{
const flat = [];
items.forEach(item => {
if (Array.isArray(item)) {
flat.push(...flatten(item));
}
else {
flat.push(item);
}
});
return flat;
}
var button = flatten(footer).flatten(div);
count = 0;
button.onclick = function() {
count += 1;
button.innerHTML = count;
};
I get the following error: ReferenceError: footer is not defined
Thanks
Here is my HTML code:
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
Edit:
footer is defined in my HTML, I want to select footer in my function
Also, I can't add class or id to my html, I can't edit it
If, for the sake of practice (or a lost bet), you'd want to write your own querySelectorAll, you could write a recursive function that walks the DOM tree... The only thing you rely on is an entrance to the DOM: window.document.
Note that this will never be able to compete with the performance of your browser's default query implementations. We're just doing it to show we can.
Step 1: recursively walking the document (depth-first)
const walk = (el) => {
console.log(el.nodeName);
Array.from(el.children).forEach(walk);
};
walk(document);
<div class="wrapper">
<div class="container">
<footer>
<div>
</div>
</footer>
</div>
</div>
As you can see, this function loops over each element in the document and its children.
Step 2: Adding the filter logic
If you want it to actually find and return elements, you'll have to pass some sort of filtering logic. querySelectorAll works with string inputs, which you could try to recreate... Since we're redoing this for fun, our select will work with functions of HTMLElement -> bool.
const selectIn = (pred, el, result = []) => {
if (pred(el)) result.push(el);
Array.from(el.children)
.filter(e => e)
.map(el2 => selectIn(pred, el2, result));
return result;
}
// EXAMPLE APP
// Define some selectors
const withClass = className => el =>
el && el.classList && el.classList.contains(className);
const withTag = tagName => el =>
el && el.nodeName === tagName.toUpperCase();
// Select some elements
const footer = selectIn(withTag("footer"), document)[0];
const container = selectIn(withClass("container"), document)[0];
const divsInFooter = selectIn(withTag("div"), footer);
// Log the results
console.log(`
footer:
${footer.outerHTML}
container:
${container.outerHTML}
divsInFooter:
${divsInFooter.map(d => d.outerHTML)}
`);
<div class="wrapper"><div class="container"><footer><div></div></footer></div></div>
I would like to use a javascript loop to create multiple HTML wrapper elements and insert JSON response API data into some of the elements (image, title, url, etc...).
Is this something I need to go line-by-line with?
<a class="scoreboard-video-outer-link" href="">
<div class="scoreboard-video--wrapper">
<div class="scoreboard-video--thumbnail">
<img src="http://via.placeholder.com/350x150">
</div>
<div class="scoreboard-video--info">
<div class="scoreboard-video--title">Pelicans # Bulls Postgame: E'Twaun Moore 10-8-17</div>
</div>
</div>
</a>
What I am trying:
var link = document.createElement('a');
document.getElementsByTagName("a")[0].setAttribute("class", "scoreboard-video-outer-link");
document.getElementsByTagName("a")[0].setAttribute("url", "google.com");
mainWrapper.appendChild(link);
var videoWrapper= document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video-outer-link");
link.appendChild(videoWrapper);
var videoThumbnailWrapper = document.createElement('div');
document.getElementsByTagName("div")[0].setAttribute("class", "scoreboard-video--thumbnail");
videoWrapper.appendChild(videoThumbnailWrapper);
var videoImage = document.createElement('img');
document.getElementsByTagName("img")[0].setAttribute("src", "url-of-image-from-api");
videoThumbnailWrapper.appendChild(videoImage);
Then I basically repeat that process for all nested HTML elements.
Create A-tag
Create class and href attributes for A-tag
Append class name and url to attributes
Append A-tag to main wrapper
Create DIV
Create class attributes for DIV
Append DIV to newly appended A-tag
I'd greatly appreciate it if you could enlighten me on the best way to do what I'm trying to explain here? Seems like it would get very messy.
Here's my answer. It's notated. In order to see the effects in the snippet you'll have to go into your developers console to either inspect the wrapper element or look at your developers console log.
We basically create some helper methods to easily create elements and append them to the DOM - it's really not as hard as it seems. This should also leave you in an easy place to append JSON retrieved Objects as properties to your elements!
Here's a Basic Version to give you the gist of what's happening and how to use it
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com" });
let div = create("div", { id: "myDiv" });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
//create element function
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//append child function
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//example:
//get wrapper div
let mainWrapper = document.getElementById("mainWrapper");
//create link and div
let link = create("a", { href:"google.com", textContent: "this text is a Link in the div" });
let div = create("div", { id: "myDiv", textContent: "this text is in the div! " });
//add link as a child to div, add the result to mainWrapper
ac(mainWrapper, ac(div, link));
div {
border: 3px solid black;
padding: 5px;
}
<div id="mainWrapper"></div>
Here is how to do specifically what you asked with more thoroughly notated code.
//get main wrapper
let mainWrapper = document.getElementById("mainWrapper");
//make a function to easily create elements
//function takes a tagName and an optional object for property values
//using Object.assign we can make tailored elements quickly.
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
//document.appendChild is great except
//it doesn't offer easy stackability
//The reason for this is that it always returns the appended child element
//we create a function that appends from Parent to Child
//and returns the compiled element(The Parent).
//Since we are ALWAYS returning the parent(regardles of if the child is specified)
//we can recursively call this function to great effect
//(you'll see this further down)
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
//these are the elements you wanted to append
//notice how easy it is to make them!
//FYI when adding classes directly to an HTMLElement
//the property to assign a value to is className -- NOT class
//this is a common mistake, so no big deal!
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
//here's where the recursion comes in:
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//keep in mind that it might be easiest to read the ac functions backwards
//the logic is this:
//Append videoImage to videoThumbnailWrapper
//Append (videoImage+videoThumbnailWrapper) to videoWrapper
//Append (videoWrapper+videoImage+videoThumbnailWrapper) to link
//Append (link+videoWrapper+videoImage+videoThumbnailWrapper) to mainWrapper
let mainWrapper = document.getElementById('mainWrapper');
function create(tagName, props) {
return Object.assign(document.createElement(tagName), (props || {}));
}
function ac(p, c) {
if (c) p.appendChild(c);
return p;
}
var link = create("a", {
className: "scoreboard-video-outer-link",
url: "google.com"
});
var videoWrapper = create("div", {
className: "scoreboard-video-outer-link"
});
var videoThumbnailWrapper = create("div", {
className: "scoreboard-video--thumbnail"
});
var videoImage = create("img", {
src: "url-of-image-from-api"
});
ac(mainWrapper, ac(link, ac(videoWrapper, ac(videoThumbnailWrapper, videoImage))));
//pretty fancy.
//This is just to show the output in the log,
//feel free to just open up the developer console and look at the mainWrapper element.
console.dir(mainWrapper);
<div id="mainWrapper"></div>
Short version
Markup.js's loops.
Long version
You will find many solutions that work for this problem. But that may not be the point. The point is: is it right? And you may using the wrong tool for the problem.
I've worked with code that did similar things. I did not write it, but I had to work with it. You'll find that code like that quickly becomes very difficult to manage. You may think: "Oh, but I know what it's supposed to do. Once it's done, I won't change it."
Code falls into two categories:
Code you stop using and you therefore don't need to change.
Code you keep using and therefore that you will need to change.
So, "does it work?" is not the right question. There are many questions, but some of them are: "Will I be able to maintain this? Is it easy to read? If I change one part, does it only change the part I need to change or does it also change something else I don't mean to change?"
What I'm getting at here is that you should use a templating library. There are many for JavaScript.
In general, you should use a whole JavaScript application framework. There are three main ones nowadays:
ReactJS
Vue.js
Angular 2
For the sake of honesty, note I don't follow my own advice and still use Angular. (The original, not Angular 2.) But this is a steep learning curve. There are a lot of libraries that also include templating abilities.
But you've obviously got a whole project already set up and you want to just plug in a template into existing JavaScript code. You probably want a template language that does its thing and stays out of the way. When I started, I wanted that too. I used Markup.js . It's small, it's simple and it does what you want in this post.
https://github.com/adammark/Markup.js/
It's a first step. I think its loops feature are what you need. Start with that and work your way to a full framework in time.
Take a look at this - [underscore._template]
It is very tiny, and useful in this situation.
(https://www.npmjs.com/package/underscore.template).
const targetElement = document.querySelector('#target')
// Define your template
const template = UnderscoreTemplate(
'<a class="<%- link.className %>" href="<%- link.url %>">\
<div class="<%- wrapper.className %>">\
<div class="<%- thumbnail.className %>">\
<img src="<%- thumbnail.image %>">\
</div>\
<div class="<%- info.className %>">\
<div class="<%- info.title.className %>"><%- info.title.text %></div>\
</div>\
</div>\
</a>');
// Define values for template
const obj = {
link: {
className: 'scoreboard-video-outer-link',
url: '#someurl'
},
wrapper: {
className: 'scoreboard-video--wrapper'
},
thumbnail: {
className: 'scoreboard-video--thumbnail',
image: 'http://via.placeholder.com/350x150'
},
info: {
className: 'scoreboard-video--info',
title: {
className: 'scoreboard-video--title',
text: 'Pelicans # Bulls Postgame: E`Twaun Moore 10-8-17'
}
}
};
// Build template, and set innerHTML to output element.
targetElement.innerHTML = template(obj)
// And of course you can go into forEach loop here like
const arr = [obj, obj, obj]; // Create array from our object
arr.forEach(item => targetElement.innerHTML += template(item))
<script src="https://unpkg.com/underscore.template#0.1.7/dist/underscore.template.js"></script>
<div id="target">qq</div>
Suppose I created and inserted an element like this
<template id="react-web-component">
<span>template stuff</span
<script src="/static/js/bundle.js" type="text/javascript"></script>
</template>
<script>
(function (window, document) {
const doc = (document._currentScript || document.currentScript).ownerDocument;
const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function () {
const template = doc.querySelector('template#react-web-component').content;
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.appendChild(template.cloneNode(true));
},
},
});
document.registerElement('react-web-component', { prototype: proto });
})(window, document);
</script>
<react-web-component></react-web-component>
Now I wanna use a querySelector to access the open shadow dom of my element. Like this:
document.querySelector('react-web-component::shadow')
But this does not work. Is there any other way?
edit in response to #Supersharp 's answer
Sorry, I wasn't making myself clear. I am using webpack's style-loader that only accepts a CSS selector that it uses with document.querySelector, so what I am asking is for a CSS selector I can use this way.
Get it by the shadowRoot property of the Shadow host:
document.querySelector('react-web-component').shadowRoot
Update
There used to be this kind of CSS selector, but now it is deprecated.
Maybe you could try to use normal DOM instead of Shadow DOM.
This should solve the problem:
const querySelectorAll = (node,selector) => {
const nodes = [...node.querySelectorAll(selector)],
nodeIterator = document.createNodeIterator(node, Node.ELEMENT_NODE);
let currentNode;
while (currentNode = nodeIterator.nextNode()) {
if(currentNode.shadowRoot) {
nodes.push(...querySelectorAll(currentNode.shadowRoot,selector));
}
}
return nodes;
}
There is an editor draft for it (hence it does not exist yet), if i understood you right.
https://drafts.csswg.org/css-scoping/#selectors
So answer is "no", you can't do it.