Intern: loop on Promise.<Array.<leadfoot/Element>> - javascript

Let's say I have the following DOM structure, for simplicity:
<div class='myparent'>
<div class='child'>
<div class="label">A</div>
<div class="ico"/>
</div>
<div class='child'>
<div class="label">B</div>
<div class="ico"/>
</div>
<div class='child'>
<div class="label">C</div>
<div class="ico"/>
</div>
</div>
I would like to loop within all child Element returned by the function findAllByCssSelector('.child'). In particular, I would click on the ico div subelement ONLY if the label of the div is B.
I would remember, that findAllByCssSelector() returns Promise.<Array.<leadfoot/Element>>.
Typically I should do something like:
var my_label = null;
this.remote
.findAllByCssSelector('.my-selector').then(function (elementArray) {
for(.....) {
elementArray[i]
.getVisibileText()
.then(function (text) {
if(text == my_label)
elementArray[i].findByCssSelector('.ico').click().end()
}
}
})
I tried this code but did not work, because the elementArray[i] within the getVisibleText().then() function does not exist - it's like I lose its reference. Furthermore, I need also that if the label is not found at the end of the loop, an exception should be thrown.
How can I achieve that? Could anyone help, please?

The simplest way to do this would be to use an Xpath expression to select the item directly, like:
.findByXpath('//div[#class="child" and div[#class="label" and text()="B"]]/div[#class="ico"]')
The expression above will find the first div with class "ico" that's the child of a div with class "child" that has a child div with class "label" and text content "B".
Update
Using an Xpath expression is almost always preferable to looping through elements using Leadfoot commands because it's significantly more efficient, but if looping is desired for some reason, you can do something like:
var my_label = null;
this.remote
.findAllByCssSelector('.my-selector')
.then(function (elementArray) {
return Promise.all(elementArray.map(function (element) {
return element.getVisibleText()
.then(function (text) {
if (text === my_label) {
return element.findByCssSelector('.ico')
.then(function (ico) {
return ico.click();
});
}
});
});
});
A couple of key points to note:
You need to return Promises/Commands from then callbacks when you're performing async operations in the then callbacks
Element methods (like element.findByCssSelector) return Promises, not Commands, so you can't call click on the result.

Related

Filter NodeList by CSS3 selector

I have a NodeList object - let's say, returned by $element.children - and I would like to filter it by a CSS3 selector to only have certain elements left which meet my criteria. The function should basically look as following:
var filter = (function($elementsToFilter, selector){
var $elementsFiltered;
// ...?
return $elementsFiltered;
});
filter(document.querySelector('#element').children, '.two.three')
<div id="element">
<div class="one two three">Yes</div>
<div class="two three">Yes</div>
<div class="two">No</div>
<div class="three">No</div>
</div>
Might this not be useful for production (Bad practice)?
What is the fastest way to dynamically select the elements with 'Yes' in the 'filter' function?
Please note, that I am NOT looking for a framework solution, e.g. in jQuery. Thanks in advance!
Question 1:
You most efficient method I can think of without using document.querySelector() is to use the contains() function on Element.classList.
for (elem in document.getElementById('element').children) {
if (elem.classList.contains('two') && elem.classList.contains('three')) {
// Do something
}
}
Alternatively, you could merge your selectors into one like this:
for (elem in document.querySelector('#element > .two.three')) {
// Do something
}
Question 2:
If you control the server-side code which generates this HTML, you may find it easier to use a data attribute like this:
<div id="element">
<div class="one two three" data-select>Yes</div>
<div class="two three" data-select>Yes</div>
<div class="two">No</div>
<div class="three">No</div>
</div>
Then with similar options to question 1:
for (elem in document.getElementById('element').children) {
if (elem.dataset.select !== undefined) {
// Do something
}
}
The explicit check for undefined is necessary here (ref).
for (elem in document.querySelector('#element > [data-select]') {
// Do something
}
As an aside, javascript now has the Array.filter() function. You can use this with Array.from() to replace your homebrew filter function (example taken from my first code snippet):
let filtered = Array.from(document.getElementById('element').children).filter(elem => {
return elem.classList.contains('two') && elem.classList.contains('three');
});
Or create a filterByClasses function:
function filterByClasses(elements, classes) {
return Array.from(elements).filter(elem => {
return !classes.map(elem.classList.contains).includes(false);
}
}
// Usage:
let filtered = filterByClasses(
document.getElementById('#element').children,
['two', 'three']
);

querySelectorAll for grouped data-attributes

Suppose I have the following markup...
<div data-namespace-title="foo"></div>
<div data-namespace-description="bar"></div>
<div data-namespace-button="foo"></div>
Is there anyway in which I can select of them with a querySelectorAll?
I've tried document.querySelectorAll([data-namespace-*]), but that doesn't work of course
There is no easy way to do it, simply because the browser does not implement wildcard selectors on the attribute name/key (only on its value). What you can do is to simply iterate through your element set (in this case, their common denominator is div), and then filter them out.
You can access the list of attributes of each DOM node by calling <Node>.attributes, and then convert that into an array, and check if one or more of each attribute's name matches the regex pattern /^data-namespace-.*/gi:
var els = document.querySelectorAll("div");
var filteredEls = Array.prototype.slice.call(els).filter(function(el) {
var attributes = Array.prototype.slice.call(el.attributes);
// Determine if attributes matches 'data-namespace-*'
// We can break the loop once we encounter the first attribute that matches
for (var i = 0; i < attributes.length; i++) {
var attribute = attributes[i];
// Return the element if it contains a match, and break the loop
if (attribute.name.match(/^data-namespace-.*/gi))
return el;
}
});
console.log(filteredEls);
<div data-namespace-title="foo">title</div>
<div data-namespace-description="bar">description</div>
<div data-namespace-button="foobar">button</div>
<div data-dummy>dummy</div>
Update: if you're familiar with ES6, it gets a lot cleaner, because:
We can use Array.from in place of the cumbersome Array.prototype.slice.call(...). Pro-tip: you can also use the spread operator, i.e. const els = [...document.querySelectorAll("div")].
We can use Array.some in place of manually creating a for loop with return logic
const els = Array.from(document.querySelectorAll("div"));
const filteredEls = els.filter(el => {
const attributes = Array.from(el.attributes);
return attributes.some(attribute => attribute.name.match(/^data-namespace-.*/gi));
});
console.log(filteredEls);
<div data-namespace-title="foo">title</div>
<div data-namespace-description="bar">description</div>
<div data-namespace-button="foobar">button</div>
<div data-dummy>dummy</div>
Not sure if you would be up for changing the format of you attributes, but making them all the same and adding an extra attribute could be of use if you want to using querySelectorAll
Array.from(document.querySelectorAll('[data-namespace]')).forEach(el => {
console.log(el.getAttribute('data-value'))
})
<div data-namespace="title" data-value="foo"></div>
<div data-namespace="description" data-value="bar"></div>
<div data-ns="button" data-value="foo"></div>
<div data-namespace="button" data-value="foo"></div>
Your other option is to use xpath.
Note: When using iterateNext() it will break if you modify the document before calling it.
var divs = document.evaluate('//#*[starts-with(name(.), "data-namespace")]', document, null, XPathResult.ANY_TYPE, null);
var div = divs.iterateNext()
while (div) {
alert(div.ownerElement.textContent)
div = divs.iterateNext()
}
<div data-namespace-title="foo">Foo</div>
<div data-namespace-description="bar">Bar</div>
<div data-ns-button="foo">NS Foo</div>
<div data-namespace-button="foo">Foo</div>
There's no built-in selector for such a thing, but you can still accomplish it easily enough, by selecting all elements and then filtering for those which have an attribute that starts with data-namespace:
console.log(
[...document.querySelectorAll('*')]
.filter(({ attributes }) =>
[...attributes].some(({ name }) => name.startsWith('data-namespace'))
)
);
<div data-baz="baz"></div>
<div data-namespace-title="foo"></div>
<div data-namespace-description="bar"></div>
<div data-namespace-button="foo"></div>

Why can't I access `element.firstChild` in this case?

function setText() {
// doesn't change... why not!?
document.getElementById("demo").firstChild.innerHTML = 'changed!';
}
//calling the function with setTimeout to make sure the HTML is loaded
setTimeout(setText, 500);
<div id="demo">
<p>first</p>
<p>second</p>
</div>
I can't seem to be able to change <p>first</p> to <p>changed!</p>. Why not?
Whitespace inside elements is considered as text, and text is considered as nodes.
If you change the HTML to:
<div id="demo"><p>first</p><p>second</p></div>
it works. Try it.
Or you can use node.firstElementChild to ignore leading text, or use a library like jQuery which takes care of this.
On consoling document.getElementById("demo").firstChild I get this
.
The highlighted part show empty text. That may be the reason as it is not the first p element.
Instead you can use firstElementChild
function setText() {
document.getElementById("demo").firstElementChild.innerHTML = 'changed!';
}
setTimeout(setText, 1000);
<div id="demo">
<p>first</p>
<p>second</p>
</div>
You can always use children method or querySelector
function SetText(){
document.getElementById('demo').children[0].textContent = "changed"
}
Or
function SetText() {
document.querySelector('#demo > *').textContent = "changed"
}
Using ".firstChild" will select the whitespace in a div if there is any. In your case there is some. You have two options, either take our the whitespace/newlines or use this function instead...
function setText() {
// doesn't change because the firstChild isn't the <p> element...
// try using firstElementChild
document.getElementById("demo").firstElementChild.innerHTML = 'changed!';
}

event for class change using jQuery and recieving the element's content

Is there any jQuery event that fires when a class is added to some object, which can tell me what is the element's content?
let me explain using an example.
Let's say I have a series of divs, all having the same class but different content.
<div class="block">content a</div>
<div class="block">content b</div>
<div class="block">content c</div>
<div class="block">content d</div>
At some moment, one of them will get an additional class, let's say selected:
<div class="block">content a</div>
<div class="block">content b</div>
<div class="block selected">content c</div>
<div class="block">content d</div>
I can't know whitch one id the selected one. So I want to run a function when one of these items gets the selected class and I want that function to receive the content of the selected element.
$('.block').on('event?', function(content){
//content is equal to "content c"
});
Is there something like that available in jQuery? Can I create one?
I have a plugin for you.
Insert this into you script:
//#Author Karl-André Gagnon
$.hook = function(){
var arg = Array.prototype.slice.call(arguments)
$.each(arg, function(){
var fn = this
if(!$.fn['hooked'+fn]){
$.fn['hooked'+fn] = $.fn[fn];
$.fn[fn] = (function(){
this['hooked'+fn].apply(this, arguments);
this.trigger(fn, arguments);
})
}
})
}
Then activate it like that:
$.hook('addClass');
This will add an "event launcher" on add class.
Then bind it on you block :
$('.block').on('addClass', function(e,a){ //e == events a == first arguments when calling addClass()
if(a === "selected"){//Just a validation
//Your code
}
})
You can get its text using this:
$('element').text();
This will provide you with the text of the element!
For you, this would be
$('.selected').text();
Now you can show it as an alert, or write it somewhere or use it as a variable! It will have the value of
content c
Or what ever the value the element would provide you with!
For more: http://api.jquery.com/text/
function test(someClass){
var content;
if($('.block').hasClass(someClass)) {
content = $('.' + someClass).html();
}
}
Then you call the function with the class of your wish as a parameter: test('classname');

Return default value if condition can not be satisfied

I have HTML like that :
<div id="MyArea">
<div class="data-content">The content is not satisfied </div>
<div class="data-content">[value] </div>
<div class="data-content">[valueme] </div>
</div>
Now, I want run a Function in each class (data-content) that have brackets. Others class (have no brackets), keep default and do not change anything. I use script below, but it is be error.
$("#MyArea .data-content").each(function() {
var content = $(this).html(),
values = content.match(/[^[\]]+(?=])/g);
if (value !== "") {
$(this).closest(".data-content").myFunc({
//Run myFunc in this .data-content if values has brackets.
}
else {
//return the default value and do not change the content inside of that class if it has no brackets. How can I do that?}
});
}
);
I'm not sure exactly what you're trying to do here given the numerous syntax problems, but you can use test in the condition to see if the regex matches anything. Also, you cannot return anything from each iteration in a $.each loop, so that is redundant.
Try this:
$("#MyArea .data-content").each(function () {
var content = $(this).html();
if (/[^[\]]+(?=])/g.test(content)) {
$(this).closest(".data-content").css('color', '#C00');
};
});
Example fiddle
In place of .css() in my example you could call your function.

Categories

Resources