JavaScript: Create Object from DOM - javascript

I'm trying to make a walkable DOM tree, like so:
Input:
<div>
<span>Foo</span>
<span>Bar</span>
</div>
Output (Python-like):
{'div': [{'span': 'Foo'},
{'span': 'Bar'}]}
I'd like to traverse it like so:
elements['div']['span']; // Output is "Foo".
My current code is this:
function createObject(element) {
var object = {};
if (element.childNodes.length > 0) {
for (var i = 0; i < element.childNodes.length; i++) {
object[element.tagName] = createObject(element.childNodes[i]);
}
return object;
} else {
return element.nodeValue;
}
}
But it doesn't work (the loop doesn't run). Could anyone help with this problem?

What should happen?
If no child {name: value}
if childs {name: [
{childname: childvalue}
]}
Following that logic, this is the result. Note nodeName should be used instead of tagName. Text nodes are also selected, which have nodeName #Text. If you want to only select elements, addif(element.childNodes[i].nodeType == 1)`:
function createObject(element) {
var object, childs = element.childNodes;
if (childs.length > 0) {
object = [];
for (var i = 0; i < childs.length; i++) {
//Uncomment the code if you want to ignore non-elements
// if(childs.nodeType == 1) {
object.push(createObject(childs[i]));
// }
}
return object;
} else {
object = {};
object[element.nodeName] = element.nodeValue;
return object;
}
}

Without trying to test this, it looks like the main problem is your for ... in loop - it doesn't work the same way in Javascript it does in Python.
for (child in element.childnodes)
should probably be an iterator-based loop:
for (var x=0, child; x<element.childNodes.length; x++) {
child = element.childNodes[x];
// etc
}
You'll also get text nodes you don't expect, and should check child.nodeType != Node.TEXT_NODE before recursing.

It looks like childNodes.length differs between browsers, maybe you should use hasChildNodes instead?
Also, did you use firebug (or any js debugger) to see if element was correctly filled in?
Edit : I found what is wrong. You can't create object of objects. Instead, you have to create array of objects. Check if you have childNodes, and create an object if there is none. Otherwise, create an array.
Just like your python-like output shows :-)

Related

How to write Javascript to search nodes - without getElementsByClassName

I'm very new at recursion, and have been tasked with writing getElementsByClassName in JavaScript without libraries or the DOM API.
There are two matching classes, one of which is in the body tag itself, the other is in a p tag.
The code I wrote isn't working, and there must be a better way to do this. Your insight would be greatly appreciated.
var elemByClass = function(className) {
var result = [];
var nodes = document.body; //<body> is a node w/className, it needs to check itself.
var childNodes = document.body.childNodes; //then there's a <p> w/className
var goFetchClass = function(nodes) {
for (var i = 0; i <= nodes; i++) { // check the parent
if (nodes.classList == className) {
result.push(i);
console.log(result);
}
for (var j = 0; j <= childNodes; j++) { // check the children
if (childNodes.classList == className) {
result.push(j);
console.log(result);
}
goFetchClass(nodes); // recursion for childNodes
}
goFetchClass(nodes); // recursion for nodes (body)
}
return result;
};
};
There are some errors, mostly logical, in your code, here's what it should have looked like
var elemByClass = function(className) {
var result = [];
var pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
(function goFetchClass(nodes) {
for (var i = 0; i < nodes.length; i++) {
if ( pattern.test(nodes[i].className) ) {
result.push(nodes[i]);
}
goFetchClass(nodes[i].children);
}
})([document.body]);
return result;
};
Note the use of a regex instead of classList, as it makes no sense to use classList which is IE10+ to polyfill getElementsByClassName
Firstly, you'd start with the body, and check it's className property.
Then you'd get the children, not the childNodes as the latter includes text-nodes and comments, which can't have classes.
To recursively call the function, you'd pass the children in, and do the same with them, check for a class, get the children of the children, and call the function again, until there are no more children.
Here are some reasons:
goFetchClass needs an initial call after you've defined it - for example, you need a return goFetchClass(nodes) statement at the end of elemByClass function
the line for (var i = 0; i <= nodes; i++) { will not enter the for loop - did you mean i <= nodes.length ?
nodes.classList will return an array of classNames, so a direct equality such as nodes.classList == className will not work. A contains method is better.
Lastly, you may want to reconsider having 2 for loops for the parent and children. Why not have 1 for loop and then call goFetchClass on the children? such as, goFetchClass(nodes[i])?
Hope this helps.

Show/hide in javascript when no html access

I have this:
<div id="myid1">Text 1</div>
<div id="myid2">Text 2</div>
<div id="myid3">Text 3</div>
and I would hide all these elements by default. Then when I click on a link, I would like show all them at once. I looked for some solution in Javascript but it seem is not possible to declare multiple ID when using document.getElementById.
Precision: I seen many solution who suggest to use class instead ID. The problem is I work with an external application integrated in my site and I have access partially to html, but I can set javascript code inside a dedicated JS file.
You could create a function that retrieves several elements by their id, and simply iterate over that collection of elements to hide or show them:
function getElementsByIds(idArray) {
// initialise an array (over which we'll iterate, later)
var elements = [];
// if no arguments have been passed in, we quit here:
if (!arguments) {
return false;
}
else {
/* we're running a basic check to see if the first passed-argument
is an array; if it is, we use it: */
if (Object.prototype.toString.call(arguments[0]) === '[object Array]') {
idArray = idArray;
}
/* if a string has been passed-in (rather than an array), we
make an array of those strings: */
else if ('string' === typeof arguments[0]) {
idArray = [].slice.call(arguments);
}
// here we iterate over the array:
for (var i = 0, len = idArray.length; i < len; i++) {
// we test to see if we can retrieve an element by the id:
if (document.getElementById(idArray[i])) {
/* if we can, we add that found element to the array
we initialised earlier: */
elements.push(document.getElementById(idArray[i]));
}
}
}
// returning the elements:
return elements;
}
// here we toggle the display of the elements (between 'none' and 'block':
function toggle (elems) {
// iterating over each element in the passed-in array:
for (var i = 0, len = elems.length; i < len; i++) {
/* if the current display is (exactly) 'none', we change to 'block'
otherwise we change it to 'none': */
elems[i].style.display = elems[i].style.display === 'none' ? 'block' : 'none';
}
}
function hide (nodes) {
// iterating over the passed-in array of nodes
for (var i = 0, len = nodes.length; i < len; i++) {
// setting each of their display properties to 'none':
nodes[i].style.display = 'none';
}
}
// getting the elements:
var relevantElements = getElementsByIds('myid1','myid2','myid3'),
toggleButton = document.getElementById('buttonThatTogglesVisibilityId');
// binding the click-handling functionality of the button:
toggleButton.onclick = function(){
toggle (relevantElements);
};
// initially hiding the elements:
hide (relevantElements);
JS Fiddle demo.
References:
arguments keyword.
Array.prototype.push().
Array.protoype.slice().
document.getElementById().
Function.prototype.call().
Object.prototype.toString().
typeof.
Three options (at least):
1) Wrap them all in a parent container if this is an option. Then you can just target that, rather than the individual elements.
document.getElementById('#my_container').style.display = 'block';
2) Use a library like jQuery to target multiple IDs:
$('#myid1, #myid2, #myid3').show();
3) Use some ECMA5 magic, but it won't work in old browsers.
[].forEach.call(document.querySelectorAll('#myid1, #myid2, #myid3'), function(el) {
el.style.display = 'block'; //or whatever
});
If id="myid< increment-number >" then you can select these elements very easily.
Below code will select all elements that START WITH "myid".
$("div[id^='myid']").each(function (i, el) {
//do what ever you want here
}
See jquery doc
http://api.jquery.com/attribute-contains-selector/

How to search all levels nested Javascript object

I have a Javascript Object that represents a nested JSON string like this:
http://pastebin.com/vwZb1XrA
There are many levels that contain a "name" element. For example "SVI" or "Population" or "Human Development Index".
I am trying to find a way to iterate over every level of the object to find the name "SVI" or "Population" or "Human Development Index". When a match is made I want to then replace the weight value with something like this:
if (data[key].name == name) {
data[key].weight = 55;
}
You can recursively check each and every object, like this
function rec(currentObject, values, replacement) {
if (values.some(function(currentValue) {
return currentObject.name === currentValue;
})) {
currentObject.weight = replacement;
}
(currentObject.children || []).forEach(function(currentItem) {
rec(currentItem, values, replacement);
});
}
rec(data, ["SVI", "Population", "Human Development Index"], 55);
console.log(data);
Working demo
You could use a recursive function, so if you just want to update the first object with a certain name, something like this:
function search(name, value, object) {
if (object.name == name) {
object.weight = value;
return;
}
if(object.children) {
var i = object.children.length;
while(i--) {
search(name, value, object.children[i]);
}
}
}
This would update without returning anything. If you remove the return statement it should update all objects with the name you specify.
You need a recursive function, that will go through each element and process its children. Something like this might work:
function ChangeWeightsOfName(arr, nameVal) {
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
if (item.name === nameVal) {
item.weight = 42;
}
if (item.children && Array.isArray(item.children)) {
ChangeWeightsOfName(item.children, nameVal);
}
}
}
Your pasted JSON does not seem to be valid, so I included test one in this Fiddle

Native Javascript Selections

I need to use native Javascript and for some of these I need to select more than one attribute (ex. a div with a class and id). Here is a code sample of what I've got so far. The example has all single selections.
var $ = function (selector) {
var elements = [];
var doc = document, i = doc.getElementsByTagName("div"),
iTwo = doc.getElementById("some_id"), // #some_id
iThree = doc.getElementsByTagName("input"),
// ^ Lets say I wanted to select an input with ID name as well. Should it not be doc.getElementsByTagName("input").getElementById("idname")
iFour = doc.getElementsByClassName("some_class"); // some_class
elements.push(i,iTwo,iThree,iFour);
return elements;
};
Oh yes, I forgot to mention I cannot use querySelector at all...
It depends on the properties you want to select on. For example, you might pass an object like:
{tagname: 'div', class: 'foo'};
and the function might be like:
function listToArray(x) {
for (var result=[], i=0, iLen=x.length; i<iLen; i++) {
result[i] = x[i];
}
return result;
}
function getByProperties(props) {
var el, elements;
var baseProps = {id:'id', tagName:'tagName'};
var result = [];
if ('tagName' in props) {
elements = listToArray(document.getElementsByTagName(props.tagName));
} else if ('id' in props) {
elements = [document.getElementById(props.id)];
}
for (var j=0, jLen=elements.length; j<jLen; j++) {
el = elements[j];
for (var prop in props) {
// Include all with tagName as used above. Avoids case sensitivity
if (prop == 'tagName' || (props.hasOwnProperty(prop) && props[prop] == el[prop])) {
result.push(el);
}
}
}
return result;
}
// e.g.
getByProperties({tagName:'div', className:'foo'});
However it's a simplistic approach, it won't do things like child or nth selectors.
You can perhaps look at someone else's selector function (there are a few around) and follow the fork to support non–qSA browsers. These are generally based on using a regular expression to tokenise a selector, then apply the selector manually similar to the above but more extensivly.
They also allow for case sensitivity for values (e.g. tagName value) and property names to some extent, as well as map HTML attribute names to DOM property names where required (e.g. class -> className, for -> htmlFor, etc.).

Is there a way that I can check if a data attribute exists?

Is there some way that I can run the following:
var data = $("#dataTable").data('timer');
var diffs = [];
for(var i = 0; i + 1 < data.length; i++) {
diffs[i] = data[i + 1] - data[i];
}
alert(diffs.join(', '));
Only if there is an attribute called data-timer on the element with an id of #dataTable?
if ($("#dataTable").data('timer')) {
...
}
NOTE this only returns true if the data attribute is not empty string or a "falsey" value e.g. 0 or false.
If you want to check for the existence of the data attribute, even if empty, do this:
if (typeof $("#dataTable").data('timer') !== 'undefined') {
...
}
if (typeof $("#dataTable").data('timer') !== 'undefined')
{
// your code here
}
In the interest of providing a different answer from the ones above; you could check it with Object.hasOwnProperty(...) like this:
if( $("#dataTable").data().hasOwnProperty("timer") ){
// the data-time property exists, now do you business! .....
}
alternatively, if you have multiple data elements you want to iterate over you can variablize the .data() object and iterate over it like this:
var objData = $("#dataTable").data();
for ( data in objData ){
if( data == 'timer' ){
//...do the do
}
}
Not saying this solution is better than any of the other ones in here, but at least it's another approach...
Or combine with some vanilla JS
if ($("#dataTable").get(0).hasAttribute("data-timer")) {
...
}
All the answers here use the jQuery library.
But the vanilla javascript is very straightforward.
If you want to run a script only if the element with an id of #dataTable also has a data-timer attribute, then the steps are as follows:
// Locate the element
const myElement = document.getElementById('dataTable');
// Run conditional code
if (myElement.dataset.hasOwnProperty('timer')) {
[... CODE HERE...]
}
You can use jQuery's hasData method.
http://api.jquery.com/jQuery.hasData/
The primary advantage of jQuery.hasData(element) is that it does not create and associate a data object with the element if none currently exists. In contrast, jQuery.data(element) always returns a data object to the caller, creating one if no data object previously existed.
This will only check for the existence of any data objects (or events) on your element, it won't be able to confirm if it specifically has a "timer" object.
If you want to distinguish between empty values and missing values you can use jQuery to check like this.
<div id="element" data-foo="bar" data-empty=""></div>
<script>
"foo" in $('#element').data(); // true
"empty" in $('#element').data(); // true
"other" in $('#element').data(); // false
</script>
So from the original question you'd do this.
if("timer" in $("#dataTable").data()) {
// code
}
You can create an extremely simple jQuery-plugin to query an element for this:
$.fn.hasData = function(key) {
return (typeof $(this).data(key) != 'undefined');
};
Then you can simply use $("#dataTable").hasData('timer')
Gotchas:
Will return false only if the value does not exist (is undefined); if it's set to false/null it hasData() will still return true.
It's different from the built-in $.hasData() which only checks if any data on the element is set.
You can check by css attribute selection with
if ($('#dataTable').is('[data-timer]')) {
// data-timer attribute exists
}
This is the easiest solution in my opinion is to select all the element which has certain data attribute:
var data = $("#dataTable[data-timer]");
var diffs = [];
for(var i = 0; i + 1 < data.length; i++) {
diffs[i] = data[i + 1] - data[i];
}
alert(diffs.join(', '));
Here is the screenshot of how it works.
I've found this works better with dynamically set data elements:
if ($("#myelement").data('myfield')) {
...
}
Wrong answer - see EDIT at the end
Let me build on Alex's answer.
To prevent the creation of a data object if it doesn't exists, I would better do:
$.fn.hasData = function(key) {
var $this = $(this);
return $.hasData($this) && typeof $this.data(key) !== 'undefined';
};
Then, where $this has no data object created, $.hasData returns false and it will not execute $this.data(key).
EDIT: function $.hasData(element) works only if the data was set using $.data(element, key, value), not element.data(key, value). Due to that, my answer is not correct.
I needed a simple boolean to work with. Because it's undefined of not present, and not false, I use the !! to convert to boolean:
var hasTimer = !!$("#dataTable").data('timer');
if( hasTimer ){ /* ....... */ }
An alternative solution would be using filter:
if( $("#dataTable").filter('[data-timer]').length!==0) { /* ....... */ }
var data = $("#dataTable").data('timer');
var diffs = [];
if( data.length > 0 ) {
for(var i = 0; i + 1 < data.length; i++) {
diffs[i] = data[i + 1] - data[i];
}
alert(diffs.join(', '));
}
And what about:
if ($('#dataTable[data-timer]').length > 0) {
// logic here
}

Categories

Resources