I just got a major bug that was really hard to solve and found out after a lot of work it was because two HTML elements had the same ID attribute.
Is there a command that spots double IDs across the whole DOM?
Update:
After reading all the comments I tested the different approaches in a relatively big website (http://www.powtoon.com html5 studio) and here are the results.
Please comment if you have better ways then these two.
Both answers returned the same results, but looks like the querySelectorAll was indeed faster:
QuerySelectorAll
function getDuplicateIdsUsingQuerySelector(){
const idsOccurances = Array.from(document.querySelectorAll('[id]'))
.map(elem => elem.id)
.reduce((allIDs, id) => {
allIDs[id] = (allIDs[id] || 0) + 1
return allIDs
}, {})
return Object.entries(idsOccurances)
.filter(([id, occurances]) => occurances > 1)
.map(([id]) => id)
}
1.5ms per call on average.
Regex
function getDuplicateIdsUsingRegEx(){
const idRegex = / id=".*?"/ig
const ids = document.body.innerHTML.match(idRegex)
const idsOccurances = ids.reduce((allIDs, id) => {
allIDs[id] = (allIDs[id] || 0) + 1
return allIDs
}, {})
return Object.entries(idsOccurances)
.filter(([id, occurrences]) => occurrences > 1)
.map(([id]) => id.slice(4, -1))
}
5.5ms per call average.
document.querySelectorAll('[id="dupid"]') would give you multiple results
[edit]
I made this jsfiddle with another way of finding all duplicates if you don't know what IDs to expect.
https://jsfiddle.net/edc4h5k2/
function checkAllIds() {
var counts = {};
var duplicates = new Set();
function addCount(id) {
if (counts[id]) {
counts[id] += 1;
duplicates.add(id);
} else {
counts[id] = 1;
}
}
document.querySelectorAll("[id]").forEach(function(elem) {
addCount(elem.id);
})
return Array.from(duplicates);
}
Is there a command that spots double IDs across the whole DOM?
Simply, put this in your console
$("[id='idValue']").length
If the value is more than one, then you have a duplicate!
Once, you have spotted the fact that there are duplicates, then you need to check the hierarchy of the individual elements
$("[id='idValue']").each( function(){
//traverse parents till body to see the dom structure to locate those duplicates
});
If you don't know the id value in advance, then first create the array of duplicate ids
var allIds = {};
var duplicateIds = [];
$( "[id]" ).each( function(){
var idValue = $(this).attr("id");
if ( allIds[ idValue ] )
{
duplicateIds.push ( idValue );
}
else
{
allIds[ idValue ] = true;
}
});
Now duplicateIds is the array containing the duplicate ids. Iterate the same to check its DOM hierarchy so that you can spot it in the entire DOM.
duplicateIds.forEach( function( idValue ){
var $elementWithDuplicateId = $( "#" + idValue );
//logic to traverse parents
});
If you are using Chrome you could run an audit by opening the console->Audits and check the checkbox for Best practices. If there are any double id's they show up there.
This will give you an array of all IDs that occur more than once.
First all elements that have an ID are selected, then their IDs are extracted, then everything is reduced into an object that counts the occurrences. Of that object, the entries are converted into an array, which is filtered to only contain those that occur more than once, then only the ID names are extracted.
const duplicateIDs = Object.entries(Array.from(document.querySelectorAll("[id]"), ({id}) => id)
.reduce((allIDs, id) => (allIDs[id] = (allIDs[id] || 0) + 1, allIDs), {}))
.filter(([id, occurrences]) => occurrences > 1)
.map(([id]) => id);
console.log(duplicateIDs);
<div id="a"></div>
<div id="b"></div>
<div id="c"></div>
<div id="a"></div>
<div id="e"></div>
<div id="f"></div>
<div id="a"></div>
<div id="b"></div>
<div id="c"></div>
<div id="a"></div>
Here’s an alternative that uses Sets and Maps, and a lower number of intermediate arrays between Array methods:
const duplicateIDs = Array.from(Array.from(document.querySelectorAll("[id]"), ({id}) => id)
.reduce((allIDs, id) => {
if(allIDs.map.has(id)){
allIDs.dupes.add(id);
}
else{
allIDs.map.set(id, 1);
}
return allIDs;
}, {
map: new Map(),
dupes: new Set()
}).dupes);
console.log(duplicateIDs);
<div id="a"></div>
<div id="b"></div>
<div id="c"></div>
<div id="a"></div>
<div id="e"></div>
<div id="f"></div>
<div id="a"></div>
<div id="b"></div>
<div id="c"></div>
<div id="a"></div>
Ideally you shouldn't have multiple elements with same id. I know your question is about detecting if there are elements with same id. IMHO, whatever you are trying with this probably would be a short term fix. You need to fix the root of the problem i.e. no two elements should have the same id.
After reading all the comments I tested the different approaches in a relatively big website (http://www.powtoon.com html5 studio) and here are the results.
Both answers returned the same results, but looks like the querySelectorAll was indeed faster:
QuerySelectorAll
function getDuplicateIdsUsingQuerySelector(){
const idsOccurances = Array.from(document.querySelectorAll('[id]'))
.map(elem => elem.id)
.reduce((allIDs, id) => {
allIDs[id] = (allIDs[id] || 0) + 1
return allIDs
}, {})
return Object.entries(idsOccurances)
.filter(([id, occurances]) => occurances > 1)
.map(([id]) => id)
}
1.5ms per call on average.
Regex
function getDuplicateIdsUsingRegEx(){
const idRegex = / id=".*?"/ig
const ids = document.body.innerHTML.match(idRegex)
const idsOccurances = ids.reduce((allIDs, id) => {
allIDs[id] = (allIDs[id] || 0) + 1
return allIDs
}, {})
return Object.entries(idsOccurances)
.filter(([id, occurrences]) => occurrences > 1)
.map(([id]) => id.slice(4, -1))
}
5.5ms per call average.
Related
i have this html:
<div id="helo">
<span id="hjhn">sample text</span>
<span id="uhed">melaps xtet</span>
<span id="kdhs">elpmas txet</span>
</div>
then i need something that you tell the id and it returns the number of the element.
so for example:
helo('#hjhn'); // Output: 0
helo('#uhed'); // Output: 1
helo('#kdhs'); // Output: 2
I've tried a lot of like 3 different ways but I just dont know how, so it would be great that you'd try to help me!
You could do something like this.
const parent = document.querySelector("#helo");
const getChildIndex = (_id) => {
let index = 0;
for (const child of parent.children) {
index++;
if (child.id === _id){
return index;
}
}
}
console.log(getChildIndex("kdhs"))
The main goal is to loop through the child elements and map them to an index number. It can be done in various ways.
Yes, there are a number of ways to do this. Here’s another.
function getSpanIndex(id) {
const div = document.getElementById('helo');
const spans = div.querySelectorAll('span');
for (let i = 0; i < spans.length; ++i) {
if (spans[i].id === id)
return i;
}
return -1;
}
The Following function is flexible in places where you don't know the parent of the selected element.
const getIndex = (id) => {
const c = document.getElementById(id);
const parent = c.parentNode;
return Array.from(parent.children).findIndex((i) => i.id == id);
};
console.log(getIndex("uhed"));
It selects an element with the given id and loops through the children of the parent of the selected element.
Additionally, you can add if statements to check whether c is undefined to avoid run time errors in cases where an element with the given id doesn't exist in the DOM.
Template string can be well done:
function test(ele) {
const currentEle = document.querySelector(`${ele}`);
return Array.from(currentEle.parentNode.children).findIndex(item => item === currentEle);
}
You can use the querySelector function for selecting nth-child. See the below example.
const document = document.querySelector('#helo span:nth-child(2)');
console.log(document); // 👉️ <span id="kdhs">elpmas txet</span>
Note: nth-child(2) this "2" is the index of your item. It starts from 0
Check this blog for more details -
https://bobbyhadz.com/blog/javascript-get-nth-child-of-element
I am aiming to search an array of objects for one that's title is similar or matches a search term exactly. The problem is I would like to prioritise exact matches over matches that only contain the string.
The current code does this by looping multiple times, each time with a different condition, and returns the object if it matches.
class Item {
constructor(title) {
this.title = title;
}
}
function findMatch(term) {
const items = [new Item("term"), new Item("Longer Term"), new Item("Completely Different Title")];
// Check if any match the search term exactly
for (var item of items) {
if (item.title === term) return item;
}
// Check if any match the search term, ignoring case
for (var item of items) {
if (item.title.toLowerCase() === term.toLowerCase()) return item;
}
// Check if any start with the search term
for (var item of items) {
if (item.title.toLowerCase().startsWith(term.toLowerCase())) return item;
}
// Check if any end with the search term
for (var item of items) {
if (item.title.toLowerCase().endsWith(term.toLowerCase())) return item;
}
// Check if any contain the search term
for (var item of items) {
if (item.title.toLowerCase().includes(term.toLowerCase())) return item;
}
return null;
}
console.log(findMatch("different")); // Item with title "Completely Different Title"
Is there a way to do this more efficiently, like in one loop - or is there a better way to search strings?
I have looked into using the Levenshtein algorithm however this does not work for searching "Comp" and getting the Item with title "Completely Different Title", as a lot more is different between "Comp" and "Completely Different Title" than there is between "Comp" and "term" - Is there a way to incorporate the same idea into this search?
If you're looking for efficiency, the only improvement I can think of that would reduce processing would be to lowercase the strings in advance, instead of lowercasing each value inside each loop. Though, it'd probably be a very marginal improvement and be unnoticeable in most cases.
class Item {
constructor(title) {
this.title = title;
this.lowerTitle = title.toLowerCase();
}
}
function findMatch(term) {
const lowerTerm = term.toLowerCase();
// use item.lowerTitle and lowerTerm when appropriate
The logic you want to implement fundamentally requires a loop over all elements looking for one condition, followed by another loop over all elements looking for another, etc. So there's no real way to improve the computational complexity over your current implementation.
You could combine some or all of the conditions with a regular expression, but that would break the priority sequence of match types to be returned.
If you want to make the code shorter and easier to maintain, that's easy enough - you could use an array of callbacks that get called for every item in order:
const comparers = [
(a, b) => a === b,
(a, b) => a.startsWith(b),
(a, b) => a.endsWith(b),
(a, b) => a.includes(b),
]
for (const fn of comparers) {
if (fn(item.lowerTitle, lowerTerm)) return item;
}
Is there a way to incorporate the same idea into this search?
Checking the Levenshtein distance would be a bit different. Instead of looping over items and returning one when it matches, you'd need to loop over all items unconditionally and return the best match after the loop finishes.
let bestItem;
let lowestDistance = Infinity;
for (const item of items) {
const dist = compare(item.lowerTitle, lowerTerm);
if (dist < lowestDistance) {
bestItem = item;
lowestDistance = dist;
}
}
return bestItem;
You'd do this at least instead of the .includes check at the end. Depending on what logic you want, you might also remove the startsWith and endsWith checks in exchange too.
You might assign a score to your items depending on "similarity" whatever it is, then filter and sort the items by that score and return the matches:
class Item {
constructor(title) {
this.title = title;
}
}
const items = [new Item("term"), new Item("Longer Term"), new Item("Completely Different Title")];
function findMatch(term) {
for (var item of items) {
// Check if any match the search term exactly
if (item.title === term) item.score = 10000;
// Check if any match the search term, ignoring case
else if (item.title.toLowerCase() === term.toLowerCase()) item.score = 1000;
// Check if any start with the search term
else if (item.title.toLowerCase().startsWith(term.toLowerCase())) item.score = 100;
// Check if any end with the search term
else if (item.title.toLowerCase().endsWith(term.toLowerCase())) item.score = 10;
// Check if any contain the search term
else if (item.title.toLowerCase().includes(term.toLowerCase())) item.score = 1;
}
return items.filter(i => 0 < i.score).sort((a, b) => b.score - a.score).map(i => delete i.score && i)
}
console.log(findMatch("different")); // Item with title "Completely Different Title"
console.log(findMatch("term"));
I'm struggling to return a value for last event containing "SearchResults" in the case below:
schema of my datalayer where I want to collect the information
At the moment I achieved to write to following code in order to know if a SearchResult event exists or not:
//Check if an existing event contains Search Results
if (digitalData.event.filter(e => e.componentID === 'SearchResults').length > 0) {
// If there are any that exist, I want to take the last one that was generated and return the pageIndex value of that one
console.log("exist");
} else {
// If it doesn't exist return 1
return 1;
};
Now I'm struggling to find a way to select only the last event generated and return the value contained in "digitalData.event.attributes.pageIndex".
Does anyone have any solution regarding this point ?
Thank you,
You could just save that in a variable temporarily and return the last result. Is this what you wanted?
const searchResults = digitalData.event.filter(e => e.componentID === 'SearchResults')
if (searchResults.length > 0) {
const yourAnswer = searchResults[searchResults.length -1]
console.log("exist");
} else {
return 1;
};
Filter the items first, and if there are any take the last item:
const searchResults = digitalData.event.filter(e => e.componentID === 'SearchResults');
if (searchResults.length > 0) {
// If there are any that exist, I want to take the last one that was generated and return the pageIndex value of that one
return searchResults[searchResults.length-1].pageLength;
} else {
// If it doesn't exist return 1
return 1;
};
Hello bro you can do what you want in this way.
let getResult = digitalData.event.filter(e => e.componentID === 'SearchResults' && e.componentID.length > 0 )
if (getResult.length > 0) {
const getIt = getResult[getResult.length - 1];
getResult = getIt
}
console.log(getResult)
Is there a better way to get just the tag information (tagName, classes, styles, other attributes, whether it is empty or not, etc.) without the innerHTML content, with starting and ending tag separated than:
const outer = el.outerHTML
const inner = el.innerHTML
const tag_only = outer.replace(inner, '');
const MATCH_END = /^<([a-zA-Z][a-zA-Z0-9_-]*)\b[^>]*>(<\/\1>)$/;
const match = MATCH_END.exec(tag_only);
if (match === null) { // empty tag, like <input>
return [tag_only, inner, ''];
} else {
const end_tag = match[2];
const start_tag = tag_only.replace(end_tag, '');
return [start_tag, inner, end_tag];
}
This works, but it does not seem particularly efficient, requiring two calls to query the DOM, two replace calls, and a regular expression search (ugg) to get back some information that the browser/DOM already has separately.
(FWIW, I'm working on an Element/Node processor that needs to walk all childNodes, changing some, before reconstructing mostly the original HTML, so I'm going to need to recursively call this function a lot and it would be good for speed to have a faster way)
methods like innerHTML, outerHTML are expensive since they parse the whole element tree on which they are called, building the DOM tree like this is exponentially expensive, so they should be avoided in performant applications,
in fact a seemingly okay childNodes is expensive too,
so for maximum performance you shoud build the tree node-by-node.
Below is a possible solution for your case:
const collect = function (el) {
const inner = [];
if (el && (el.nodeType === Node.ELEMENT_NODE
|| el.nodeType === Node.TEXT_NODE)) {
let clone = el.cloneNode();
clone.setAttribute?.('data-clone', clone.tagName);
let tag_only = clone.outerHTML;
let elm;
const MATCH_END = /^<([a-zA-Z][a-zA-Z0-9_-]*)\b[^>]*>(<\/\1>)$/;
const match = MATCH_END.exec(tag_only);
if (match === null) { // empty tag, like <input>
elm = [tag_only, inner, ''];
} else {
const end_tag = match[2];
const start_tag = tag_only.replace(end_tag, '');
elm = [start_tag, inner, end_tag];
}
this.push(elm);
}
el = el.firstChild;
while (el) {
collect.call(inner, el);
el = el.nextSibling;
}
return this;
};
console.log(collect.call([], document.body).flat(Infinity).join(''));
<div data-id="a" class="b">
<input type="text">
<div data-id="c" class="d">
<input type="text"/>
<div data-id="e" class="f">
<input type="text"/>
</div>
</div>
</div>
Suppose in jQuery I push DOM elements into an array,
var elemarray = [];
elemarray.push($('#elem1'));
elemarray.push($('#elem2'));
Would it be possible to then use $.inArray to determine if the array contains an element?
if ( $.inArray($('#elem2'), elemarray) > -1 ) { .. }
The only examples I saw for primitive types, strings and numbers.
JSFiddle, not working: https://jsfiddle.net/5knyrcph/2/
The problem is that you are not storing DOM elements, but jQuery wrappers. Every time you create such a wrapper, it is a new jQuery object.
Instead use the real DOM elements themselves, which with jQuery you can get with .get():
var elemarray = [];
elemarray.push($('#elem1').get(0));
elemarray.push($('#elem2').get(0));
if ($.inArray($('#elem1').get(0), elemarray) > -1) {
console.log('Found');
}
else {
console.log('Not found');
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="elem1">
Test
</div>
<div id="elem2">
Test2
</div>
In HTML5 / ES6 you can do this without jQuery in shorter code:
const elemSet = new Set([elem1, elem2]);
if (elemSet.has(elem1)) {
console.log('Found');
}
else {
console.log('Not found');
}
<div id="elem1">
Test
</div>
<div id="elem2">
Test2
</div>
You really should use !== -1
And here is the fiddle to the corrected JS: https://jsfiddle.net/5knyrcph/3/
Code:
var elemarray = [];
elemarray.push($('#elem1').text());
elemarray.push($('#elem2').text());
alert(elemarray[0]);
if ($.inArray($('#elem1').text(), elemarray) !== -1) {
alert('Found');
}
else {
alert('Not found');
}
Jquery return an Array of items that match the criteria, so when ever you select an element it always will construct a brand new array of objects that match your criteria, in order to compare the elements you need to either get the element inside the array with these two options:
$(".element")[position]; //or
$(".element").get(position);
If in your fiddle instead of pushing the Jquery object, you push the actual element, then you'll get your results right.
Fiddle Updated
Hope this help