Issue with recursive function converting dom element to json - javascript

I have a recursivce function that takes a dom tree and converts it to JSON.
However I want to exclude any nodes that have a specific data attribute data-exclude
const htmlToJSON = node => {
const exclude = node.attributes?.getNamedItem('data-exclude');
if (!exclude) {
let obj = {
nodeType: node.nodeType
};
if (node.tagName) {
obj.tagName = node.tagName.toLowerCase();
} else if (node.nodeName) {
obj.nodeName = node.nodeName;
}
if (node.nodeValue) {
obj.nodeValue = node.nodeValue;
}
let attrs = node.attributes;
if (attrs) {
length = attrs.length;
const arr = (obj.attributes = new Array(length));
for (let i = 0; i < length; i++) {
const attr = attrs[i];
arr[i] = [attr.nodeName, attr.nodeValue];
}
}
let childNodes = node.childNodes;
if (childNodes && childNodes.length) {
let arr = (obj.childNodes = []);
for (let i = 0; i < childNodes.length; i++) {
arr[i] = htmlToJSON(childNodes[i]);
}
}
return obj;
}
};
const parser = new DOMParser();
const { body } = parser.parseFromString(page, 'text/html');
let jsonOutput = htmlToJSON(body);
console.log(jsonOutput);
I am clearly missing something with the way I am excluding because when I log the results it is returning undefined instead of just excluding it.

It's most likely because you're not returning anything from htmlToJSON in the case of "exclude == true". Notice how your lambda function doesn't have a "return " in that case. So the function will by default return "undefined."
And if you fill an array element with "undefined" it becomes a sparse array. So those elements in the array with "undefined" values become interpreted as "empty" slots by console.log() when printing the contents of any array to the console.
Update: I tried your code and, yup, my explanation above is correct. However, if you don't care about implicitly returning undefined from your htmlToJSON(), then you can just modify your inner for loop:
for (let i = 0; i < childNodes.length; i++) {
let json = htmlToJSON(childNodes[i]);
json && arr.push(json);
}
This way, only if json is truthy, will it add an element to the arr array.
I tried this code with your original function, and also with a modified version that returns null if exclude == true, and both ways worked.
Here's a Codepen example.

Did not execute the code. As far as I can see htmlToJSON will return obj or undefined. If exclude is truthy, the function will return undef, thats what you are seeing.
Change your for loop:
for (let i = 0, temp; i < childNodes.length; i++) {
temp = htmlToJSON(childNodes[i]);
temp && (arr[i] = temp);
}
that way you make sure if temp is defined you assign, otherwise not.
Another option is to use Array.prototype.filter on the resultant array.

Related

Running into error creating a Javascript function

I am trying to write a function to change a string written in Snake Case to Camel Case, but running into an error.
function snakeToCamel(string) {
arr = [...string];
for (i of arr) {
if (i === "_") {
let upperCaseLetter = arr[i+1].toUpperCase();
arr.splice(i+1,1,upperCaseLetter);
arr.splice(i,1)
}
};
return arr;
}
The error is here. I can't find what is wrong in the line stated in the error. What is going on?
snakeToCamel("you_dont_know")
snake-to-camel.js:5 Uncaught TypeError: Cannot read property 'toUpperCase' of undefined
at snakeToCamel (snake-to-camel.js:5)
at <anonymous>:1:1
In a for-of loop, the control variable is the array element, not an index. So i in your example is a string. So arr[i+1].toUpperCase(); will do string concatenation and try to look up a property with a name like s1, not 1.
If you want to use the index, you want a for loop or a forEach call (or even map, since that's kind of what you're doing), not a for-of loop.
A couple of other notes:
You need to be sure to declare your variables; right now, your code is falling prey to what I call The Horror of Implicit Globals, creating a global called arr. Add const or let before arr.
You don't put ; after blocks attached to control-flow statements. (You can have them there, because empty statements are allowed, but they're not supposed to be there.)
For example, using a for loop:
function snakeToCamel(string) {
// `const` because we never reassign `arr`
const arr = [...string];
// Traditional `for` so we have the index
for (let i = 0, len = arr.length; i < len; ++i) {
const ch = arr[i];
if (ch === "_") {
if (i === len - 1) {
// Trailing _, just remove it
arr.splice(i, 1);
--i;
--len;
} else {
let upperCaseLetter = arr[i + 1].toUpperCase();
// You can remove the _ and the lowr case letter
// in a single `splice` rather than multiple ones
arr.splice(i, 2, upperCaseLetter);
--i; // Need to do this to allow for multiple `_` in a row
--len;
}
}
};
return arr;
}
console.log(snakeToCamel("one_two__three_"));
Or using map:
function snakeToCamel(string) {
let lastWasUnderscore = false;
const result = [...string]
.map(ch => {
const thisIsUnderscore = ch === "_";
if (lastWasUnderscore && !thisIsUnderscore) {
lastWasUnderscore = false;
return ch.toUpperCase(); // or `.toLocaleUpperCase()`
}
lastWasUnderscore = thisIsUnderscore;
return thisIsUnderscore ? null : ch;
})
.filter(ch => ch !== null);
return result;
}
console.log(snakeToCamel("one_two__three_"));
You were actually fairly close to the solution!
Instead of using a for-of loop I would suggest you use a normal for loop.
This way you can access the index i
function snakeToCamel(string) {
const arr = [...string];
for (let i = 0; i < arr.length; i++) {
if (arr[i] === "_") {
let upperCaseLetter = arr[i+1].toUpperCase();
arr.splice(i+1,1,upperCaseLetter);
arr.splice(i,1)
}
}
return arr.join("");
}
Here's the code I wrote. (needs to be refined for DRY principle)
function STC(string) {
const arr = [...string];
let output = '';
for (const letter of arr) {
if (letter == '_') {
output += arr[arr.indexOf(letter)+1].toUpperCase()
arr.splice(arr.indexOf(letter), 1)
} else {
output += letter;
}
}; return output;
}
I just add to the string instead of an array. I also delete the underscore. Tell me if it works.
function snakeToCamel(string) {
const arr = [...string];
arr.forEach((v, i) => {
if (v === "_") {
arr.splice(i+1,1, arr[i+1].toUpperCase());
arr.splice(i,1)
}
});
return arr.join("");
}
const result = snakeToCamel('a_b_c');
console.log(result);

How can I use variables to decide what to change on a DOM element?

I am writing a function to iterate over an array that will determine what gets changed in an element. I have tried a few ideas including template literals but am not achieving desired results. Any ideas how to pass desired dom changes through an array into a function?
testArray = [["background", "yellow"]];
const changeElement =(id, array)=>{
let element = getElementById(id);
for(let i = 0; i<=array.length-1; i++){
for(let j = 0; j<=array.length-1; j++){
`${element}.style.${array[i][j]} = "${array[i][j+1]}"`;
}}
}
You don't need template literals. It won't work.
Simply using rectangular brackets ([ ]) will work for you.
In JavaScript, you can access the properties of objects using the dot (.) operator or using rectangular brackets ([ ]).
For example, if you have an object as follows:
var obj = {
x: "Hii",
y: 5,
};
Now if you want to access x of obj, you can access it in 2 ways:
console.log(obj.x); // Hii
// This will also work
console.log(obj["x"]); // Hii
Similarly, for y:
console.log(obj.y); // 5
// This will also work
console.log(obj["y"]); // 5
Now, in this case, element.style is an object. If you want to access property background of element.style, you can do the following:
// This won't work for your case as the property to be modified is stored in array
element.style.background = "yellow";
// But this will work!
element.style["background"] = "yellow";
So, while iterating, you can do this:
let testArray = [["background", "yellow"]];
const changeElement =(id, array) => {
let element = document.getElementById(id);
for(let i = 0; i<=array.length-1; i++){
for(let j = 0; j<=array.length-1; j++){
element.style[array[i][j]] = array[i][j+1];
}
}
}
But I think your testArray will have this format:
let testArray = [["prop1", "value1"], ["prop2", "value2"], ... ];
If so, your code will not work and can be simplified to use just one for loop as follows:
let testArray = [["background", "yellow"], ["color", "red"]];
const changeElement =(id, array) => {
let element = document.getElementById(id);
for(let i = 0; i < array.length; i++){
element.style[array[i][0]] = array[i][1];
}
}
Hope this helps :)
1) You can use Object.fromEntries to transform testArray into
{
background: "yellow"
}
and then iterate through this object.
2) look at this
const changeElement = (id, array)=>{
const element = document.getElementById(id);
for(let i = 0; i<=array.length-1; i++){
for(let j = 0; j<=array.length-1; j++){
element.style[array[i][j]] = array[i][j+1];
}
}
}
changeElement("myId", [["background", "yellow"]]);
you can get the value from an object by parentheses
3) You shouldn't using for loop. You can write less by using Array.prototype.forEach e.g.
const changeElement = (id, array)=>{
const element = document.getElementById(id);
array.forEach(value => {
element.style[value[0]] = value[1];
});
}
changeElement("myId", [["background", "yellow"]]);
https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Array/forEach
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries
https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Array/map

Reading data from Firebase to Javascript

I am trying to list all data from Javascript keys Object when I put it in console log there is all information, but when I want to use InnerHTML I keep getting the first object only shown.
function gotData(data){
var scores = data.val();
var keys = Object.keys(scores);
for (var i = 0; i < keys.length; i++) {
var k = keys[i];
var pro = scores[k].result;
var doc = document.getElementById("result").innerHTML = pro;
}
}
In this case, it will give me only a result of first element from my Firebase
Thanks
Please check out this stackblitz-demo, looks like your missing one small thing if I am understanding what your expected outcome is.
onClick() {
const scores = [{
'one': 1
}, {
'two': 2
}, {
'three': 3
}, {
'four': 4
}, {
'five': 5
}];
var keys = Object.keys(scores);
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
const pro = scores[k].result;
// here the += is what i think you're missing.
const doc = document.getElementById("example").innerHTML += k;
}
}
The issue is that you are overriding innerHTML each time. Instead, you need to append to the existing innerHTML. Change the last line to...
const doc = document.getElementById("example").appendChild(document.createTextNode(k))
appendChild is also much faster than setting innerHTML
.hasOwnProperty is how to see just your stored values. Does this help?
d = snap.val();
for (var k in d) {
if (d.hasOwnProperty(k)) {
if (isObject(d[k]){
console.log(k, d[k]);
} else {
console.log (k);
}
}
}
function isObject(obj) {
return obj === Object(obj);
}

js rewrite previous elements in loop

I want to push elements to array in loop but when my method returns a value, it always rewrites every element of array(probably returned value refers to the same object). I'm stuck with this problem for one day and I can't understand where is the problem because I've always tried to create new objects and assign them to 'var' not to 'let' variables. Here is my code:
setSeason(competitions, unions) {
var categories = this.sortCategories(competitions);
var unionsByCategories = new Array();
let k = 0;
for (; k < categories.length; k++) {
unionsByCategories[k] = this.assignCompetitionsToUnions(unions[0], categories[k]);
}
this.setState({categories: unionsByCategories, refreshing: false})
}
and
assignCompetitionsToUnions(unions1, competitions) {
var unions2 = this.alignUnions(unions1);
let tempUnions = [];
for (var i = 0; i < unions2.length; i++) {
var tempUnionsCompetitions = new Array();
var tempSubsCompetitions = new Array();
if (Globals.checkNested(unions2[i], 'union')) {
tempUnionsCompetitions = unions2[i].union;
tempUnionsCompetitions['competitions'] = this.getCompetitionsById(unions2[i].union.id, competitions);
}
if (Globals.checkNested(unions2[i], 'subs')) {
for (var j = 0; j < unions2[i].subs.length; j++) {
if (Globals.checkNested(unions2[i].subs[j], 'union')) {
tempSubsCompetitions[tempSubsCompetitions.length] = {union: unions2[i].subs[j].union};
tempSubsCompetitions[tempSubsCompetitions.length - 1]['union']['competitions'] =
this.getCompetitionsById(unions2[i].subs[j].union.id, competitions)
}
}
}
tempUnions.push({union: tempUnionsCompetitions, subs: tempSubsCompetitions});
}
return tempUnions;
}
Many thanks for any help.
Answer updated by #Knipe request
alignUnions(unions3) {
let newUnions = unions3.subs;
newUnions = [{union: unions3.union}].concat(newUnions);
return newUnions.slice(0, newUnions.length - 1);
}
getCompetitionsById(id, competitions) {
let tempCompetitions = [];
for (let i = 0; i < competitions.length; i++) {
if (competitions[i].union.id === id) {
tempCompetitions.push(competitions[i]);
}
}
return tempCompetitions;
}
sortCategories(competitions) {
if (competitions.length === 0) return [];
let categories = [];
categories.push(competitions.filter((item) => {
return item.category === 'ADULTS' && item.sex === 'M'
}));
categories.push(competitions.filter((item) => {
return item.category === 'ADULTS' && item.sex === 'F'
}));
categories.push(competitions.filter((item) => {
return item.category !== 'ADULTS'
}));
return categories;
}
it always rewrites every element of array(probably returned value
refers to the same object).
You are probably unintended mutating the content of the source array. I would recommend creating a copy of the array.
This is example of array mutation.
let array1 = [1,2,3];
let array2 = array1;
array2[0] = 4; // oops, now the content of array1 is [4,2,3]
To avoid mutating the source array you can create a copy of it
let array1 = [1,2,3];
let array2 = array1.slice();
array2[0] = 4; // the content of array1 is still the same [1,2,3]
I've always tried to create new objects and assign them to 'var' not
to 'let' variables.
Using let/var will not prevent from rewrites. Creating new object with new Array() will not prevent rewrites.
It's hard to read where the bug is exactly from your code and description but you could try to avoid passing an array by reference and instead create a copy and pass the copy in function calls.
this.assignCompetitionsToUnions(unions[0].slice(), categories[k])
This is a shallow copy example, you might need to apply deep copy to make it work for your case.

Finding an Object's value from an Array

Is there a simpler (or more efficient) way of achieving the following:
var _dataObjects = [{id:0, data:"data0", nextID:1},
{id:1, data:"data1", nextID:2},
{id:2, data:"data2", nextID:3} .. etc.];
generateNextPieceOfData();
function generateNextPieceOfData(){
var len = _dataObjects.length;
for ( var i = 0; i < len; i ++ ) {
var nextDataID = _dataObjects[i].nextID;
var nextData;
for ( var j = 0; j < len; j ++ ) {
if( _dataObjects[j].id == nextDataID ){
nextData = _dataObjects[j].data;
break;
}
}
}
}
The above example is abstracted from the problem I'm having and I realise the ID numbers are sequential in this instance but in the real problem nextID numbers do not run sequentially.
Thanks in advance.
Use the right data structure for your problem. Since you want to find an object by ID, create a hash map with the IDs as keys and objects as values:
var object_map = {};
for(var i = 0, l = _dataObjects.length; i < l; i++) {
objects[_dataObjects[i].id] = _dataObjects[i];
}
Then getting the next object is simply:
var next_object = object_map[someObject.nextID];
You still have iterate until some terminal condition is met though. For example:
function generatePath(id_a, id_b) {
var obj = object_map[id_a];
var path = [obj];
while (obj && obj.id !== id_b) {
obj = object_map[obj.nextID];
path.push(obj);
}
return path;
}
If your code works sequentially only, then you can sort the items by id or whatever and your code should work right? Try this:
_dataObjects = _dataObjects.sort(function(a, b) {
return a.id > b.id;
});

Categories

Resources