JS function that outputs a DOM-tree by button click - javascript

I need to write a js function that outputs a DOM-tree by button click. The tree should be output as an unnumbered list (ul) with attachments and it's needed to use the name of the element, i.e. head, body, p, div, etc., and the element id as the text output in the list item (of course if it is specified). I've tried to write it but I don't know how to make it work and what's wrong here
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body><div id="container1" style="background-color: cyan;">
<h1 id="header1">Header</h1>
<p id="paragraph1">Paragraph</p>
<div id="container2" style="background-color: red;">
</div>
</div>
<ul id="tree"></ul>
<input type="text" id="formText">
<br>
<button id= "confirmButton" style="margin-top: 5px;">Build a DOM tree</button>
</body>
</html>
function DOM_Tree(e) {
for (let i = 0; i < document.body.childNodes.length - 1; i++) {
if (document.body.childNodes[i].id != 'tree') {
let ul = document.getElementById('tree');
let li = document.createElement('li');
let el = document.body.childNodes[i];
let ul1 = document.createElement('ul');
if (el.hasChildNodes()) {
li.innerText = document.body.childNodes[i].id;
ul.append(li);
for (let j = 0; j < el.childNodes.length; j++) {
if (el.childNodes[j].id != undefined) {
let li1 = document.createElement('li');
li1.innerText = el.childNodes[j].id;
ul1.append(li1);
}
let li1 = document.createElement('li');
li1.innerText = el.childNodes[j].id;
ul1.append(li1);
}
ul.append(ul1);
}
else {
if (document.body.childNodes[i].id != undefined) {
li.innerText = document.body.childNodes[i].id;
ul.append(li);
}
}
}
}
}
confirmButton.onclick = function() {
DOM_Tree(document.body);
alert('click');
}

Your code had a number of logical flaws.
Here's a working solution:
function DOM_Tree(e, ul = document.getElementById('tree')) {
for (let i = 0; i < e.childNodes.length - 1; i++) {
if (e.childNodes[i].id != 'tree') {
let li = document.createElement('li');
let el = e.childNodes[i];
if (e.childNodes[i].id != undefined) {
li.innerText = e.childNodes[i].nodeName + ' ' + e.childNodes[i].id;
ul.append(li);
}
let ul1 = document.createElement('ul');
DOM_Tree(e.childNodes[i], ul1);
ul.append(ul1);
}
}
}
confirmButton.onclick = function() {
DOM_Tree(document.body);
// alert('click');
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body><div id="container1" style="background-color: cyan;">
<h1 id="header1">Header</h1>
<p id="paragraph1">Paragraph</p>
<div id="container2" style="background-color: red;">
</div>
</div>
<ul id="tree"></ul>
<input type="text" id="formText">
<br>
<button id= "confirmButton" style="margin-top: 5px;">Build a DOM tree</button>
</body>
</html>
And here's all the changes I did
A high level explanation of what your code was doing:
DOM_Tree got called with document.body as a parameter (but you didn't actually use that parameter anywhere)
You iterate over all of the nodes in document.body
If the node contains children, then you put that node's info into the DOM, then start iterating over the node's children. If it does not contain children, then you just add it's info to the DOM
You pretty much do the exact same thing that you did in step 3, but for the grandchildren.
Hopefully this high level explanation helps show why it wasn't working. If a human was following those instructions, they wouldn't end up with the desired results either.
What I did was
eliminate step 4 (the inner loop) - it's the exact same logic as step 3, and only needs to be done once
I cleaned it up just a tad. In step 3, weather or not there's children, you're going to add that node's info to the DOM. So, I just move that outside the if, which eliminated the need for an else, and actually the if wasn't needed either, because if there were no children, then the loop wouldn't happen.
I stopped hard coding in the usage of document.body. Instead, I referred to the "e" parameter you were passing in, which was initially set to document.body anyways.
This allowed me to recursively call this function. Once I got to the point where I needed to process the children of document.body, I just called DOM_Tree again, passing in each child. Each child in turn would call DOM_Tree for each of its children. Thus, the whole DOM would get looked at, not just the first two levels like you had in your implementation.
I made it take in an additional parameter - the node where we're currently inserting the information at. This is important so that the recursive DOM_Tree calls can insert the information it finds into the right spot in the DOM.

Are you getting the error:
Uncaught TypeError: document.body.getElementById is not a function
at DOM_Tree (<anonymous>:4:32)
at HTMLButtonElement.confirmButton.onclick (<anonymous>:26:7)
DOM_Tree # VM167:4
confirmButton.onclick # VM167:26
You can check this error in the console output in your browser (in Chrome, press F12, or right click and 'inspect', you'll see the console output, with useful errors or debugging console logs:
You need document.getElementById not document.body.getElementById.
You're also adding two click events. Your first onclick="DOM_Tree(e)" I suspect didn't work because you meant onclick="DOM_Tree(event)" or onclick="DOM_Tree(this)". You might not even want the event (you don't really want to loop through the children of the button!) in which case onclick="DOM_Tree()" would work.

Related

Adding an event listener to a procedurally assigned attribute on a single page application

I'm writing a single page note application in JavaScript with no libraries. The app lets the user add a note, and shows a shortened form beneath the input.
When they want to see the full text of their note, they can click on the relevant note to expand it.
So far, the input notes are saved in an array, then written in a shortened version (presented in HTML list elements), which have the array position assigned to them as an ID (assigning an index variable as the array length -1, then setting it as an attribute)
The view code is
<html lang="en">
<head>
<script src='./note.js'></script>
<script src='./controller.js'></script>
<title>JS Notes</title>
</head>
<body>
<h1 id="title">JS Notes</h1>
<textarea id="input"></textarea><br>
<button id="create">Create</button>
<ul id="note area"></ul>
</body>
</html>
The controller code is
window.onload = function() {
var note, createButton;
note = new Note;
createButton = document.getElementById("create")
createButton.addEventListener("click", processNote)
function processNote(){
var input, output, index, newLI;
input = document.getElementById('input').value;
note.addNote(input);
index = note.showFullNote().length - 1
document.getElementById('input').value = ""
newLI = document.createElement('li');
newLI.setAttribute("id", index)
output = input.substring(0,20)
newLI.appendChild(document.createTextNode(output + "..."))
document.getElementById('note area').appendChild(newLI)
}
The model for processing notes
(function(exports) {
function Note() {
this._list = new Array;
};
Note.prototype.addNote = function(input) {
this._list.push(input)
};
Note.prototype.showFullNote = function() {
return this._list
};
exports.Note = Note;
})(this);
I was trying to add an event listener for clicking on the list element, and passing the id of that element as the index number.
I thought this could be done by getElementsByTag, but I'm not sure how to then take the index of a specifically clicked list item, rather than the first list item on the page.
All you need to do is add the event listener to the li element before you append it to the note area.
newLI.addEventListener('click', function(e) {
let elementId = e.id;
// any other logic here
})

My javascript hint script does not work. Accessing and manipulating the DOM

Knowledge level: Beginner
What I expect from my code:
The user clicks on a class named open.
The textNode within 'open' gets replaced with a - sign.
Then I go to the first child of the parent of that class which is an h2 tag and get the title in order to place it within the sibling of 'open' named 'info'.
At last info turns visible.
The ternary operator is to check if we have only a nodeType of 3 within the firstChild. If yes get the text, if not then get the entire innerHTML.
Since I get a html collection from getElementsByClassName I tend to create a loop so that I can modify the style.
Why I do this or don't use jQuery:
I am trying to push myself and learn how to effectively manipulate the dom without third party libraries. I would appreciate hints on improving my code but please keep the basic structure the same as I am still not into advanced short cuts and I am trying to learn not copy.
Problem I am not sure how "correct" my idea of manipulating the dom is. I could not get this to work, neither do I know how to effectively tell javascript to handle only the currently clicked element.
http://jsfiddle.net/r7bL6vLy/28/
function wrapper () {
var open = document.getElementsByClassName(open);
function trigger (){
var info = this.nextSibling;
var getTitle = this.parentNode.firstChild.(nodeType == 3 ? textContent : innerHTML)
this.removeChild(textContent);
this.appendChild(document.createTextNode('-'));
info.appendChild(document.createTextNode(getTitle + 'details'));
info.style.visibility = 'visible';
}
for (i = 0; i < open.length; i++) {
open[i].addEventListener('click', trigger, false);
}
}
HTML
<div id='A'>
<h1>Stackoverflow Question</h1>
<div class='open'>+</div>
<div class='info'>Content A...</div>
</div>
<div id='B'>
<h1>Stackoverflow Question</h1>
<div class='open'>+</div>
<div class='info'>Content B...</div>
</div>
this.nextSibling will give you the textNode representing the whitespace between the elements. Use .this.nextElementSibling instead.
You don't need to do any traversal to change the + to a - since you already have the open element. Just assign it the new value.
this.textContent = "-";
To assign the h2 content, simple use .previousElementSibling.textContent and assign it to info.textContent
info.textContent = this.previousElementSibling.textContent
Some things you were doing wrong were:
using invalid syntax here:
var getTitle = this.parentNode.firstChild.(nodeType == 3 ? textContent : innerHTML)
Should have been an if statement, though the condition doesn't really seem necessary. You can use .textContent on an element too, as long as you don't need the HTML representation.
Technically you could do this:
var child = this.parentNode.firstElementChild;
var getTitle = child[child.nodeType === 3 ? "textContent" : "innerHTML"];
...but that's pretty ugly. Avoid clever tricks like this.
Using textContent as a reference to an element:
this.removeChild(textContent);
Things that could be improved:
When changing text, favor manipulating .textContent over creating new text nodes. The existing nodes are mutable and so can be reused.
If you want to copy a section of the DOM to a new location, don't use .innerHTML but instead use .cloneNode(true).
var copy = myElem.cloneNode(true);
targetElem.appendChild(copy);
Otherwise you're taking the DOM nodes, serializing them to HTML and then immediately parsing the HTML into new nodes. All that string manipulation can be avoided simply by cloning.
You almost had it:
function trigger (){
var info = this.nextElementSibling,
getTitle = this.parentNode.firstElementChild.textContent;
this.textContent = '-';
info.appendChild(document.createTextNode(getTitle + 'details'));
info.style.visibility = 'visible';
}
var open = document.getElementsByClassName('open');
for (i = 0; i < open.length; i++)
open[i].addEventListener('click', trigger, false);
function trigger (){
var info = this.nextElementSibling,
getTitle = this.parentNode.firstElementChild.textContent;
this.textContent = '-';
info.appendChild(document.createTextNode(getTitle + 'details'));
info.style.visibility = 'visible';
}
var open = document.getElementsByClassName('open');
for (i = 0; i < open.length; i++)
open[i].addEventListener('click', trigger, false);
#a, #b {
width:50%;
height:100%;
margin:auto;
}
h1 {
width:100%;
font-size:160%;
text-align:center;
}
.open {
width:22%;
margin:auto;
padding:10% 0;
line-height:0;
font-size:150%;
text-align:center;
font-weight:bold;
background:yellow;
border-radius:100%;
}
.info {
width:100%;
padding:5%;
margin:5% auto 0 auto;
text-align:center;
background:ghostwhite;
visibility:hidden;
}
<div id='A'>
<h1>Stackoverflow Question</h1>
<div class='open'>+</div>
<div class='info'>Content A...</div>
</div>
<div id='B'>
<h1>Stackoverflow Question</h1>
<div class='open'>+</div>
<div class='info'>Content B...</div>
</div>
Remember that whitespace between elements becomes a text node. So better use firstElementChild and nextElementSibling instead of firstChild and nextSibling.

How can access bunch of <li>?

I have a list of texts and I want to change their innerHTML. how can I do that by javascript if I have thousands of li tag (whose data come from database)?
<div>
<ul>
<li>a</li>
<li>as</li>
<li>asd</li>
<li>asds</li>
<li>asdsa</li>
<li>asdsad</li>
<li>asdsadz</li>
<li>asdsadzc</li>
....
.....
</ul>
</div>
-Thanks.
Update
JS code being used:
function a(){
var parent = document.getElementById("z");
var i = 0;
for(i = 0; i <= parent.children.length; i++){
if(parent.children[i].tagName == "LI"){
if(i%2!=0){
parent.children[i].innerHTML="ok";
}
}
}
}
document.onload=a(); // this didn't work. so I called the function in body tag instead of that.
<body onload="a();">
Have you tried using getElementsByTagName ? Sonds like it would help you find the elements you're trying to work with.
Edit
If you can give an Id to the UL element that holds the li's you're trying to process, you could do something like this:
var parent = document.getElementById("yourID");
var i = 0;
for(i = 0; i < parent.children.length; i++){
if(parent.children[i].tagName == "LI") {
//do what you want...
}
}
EDit 2
You have to change the last line on your script:
document.onload=a();
With this one: window.onload=a;
That'll get your function to execute on the onLoad event. Note that there might be some crossbrowser incompatibility, I would suggest researching a bit on how to execute functions on the onload event on a crossbrowser manner, or just adding this to your body tag:
<body onload="a();">
Given the - not so far fetched - precondition you wish to use jQuery, you can select them and iterate over them with "each".
$("li").each(
function() { $(this).html("changed content"); }
);
If you are not using jQuery, using a js-library that helps you out with the quircky dom is probably not a bad idea...
The general idea
Select nodes
Iterate and change html
is always the same.

Parsing through DOM get all children and values

Container is a div i've added some basic HTML to.
The debug_log function is printing the following:
I'm in a span!
I'm in a div!
I'm in a
p
What happened to the rest of the text in the p tag ("aragraph tag!!"). I think I don't understand how exactly to walk through the document tree. I need a function that will parse the entire document tree and return all of the elements and their values. The code below is sort of a first crack at just getting all of the values displayed.
container.innerHTML = '<span>I\'m in a span! </span><div> I\'m in a div! </div><p>I\'m in a <span>p</span>aragraph tag!!</p>';
DEMO.parse_dom(container);
DEMO.parse_dom = function(ele)
{
var child_arr = ele.childNodes;
for(var i = 0; i < child_arr.length; i++)
{
debug_log(child_arr[i].firstChild.nodeValue);
DEMO.parse_dom(child_arr[i]);
}
}
Generally when traversing the DOM, you want to specify a start point. From there, check if the start point has childNodes. If it does, loop through them and recurse the function if they too have childNodes.
Here's some code that outputs to the console using the DOM form of these nodes (I used the document/HTML element as a start point). You'll need to run an if against window.console if you're allowing non-developers to load this page/code and using console:
recurseDomChildren(document.documentElement, true);
function recurseDomChildren(start, output)
{
var nodes;
if(start.childNodes)
{
nodes = start.childNodes;
loopNodeChildren(nodes, output);
}
}
function loopNodeChildren(nodes, output)
{
var node;
for(var i=0;i<nodes.length;i++)
{
node = nodes[i];
if(output)
{
outputNode(node);
}
if(node.childNodes)
{
recurseDomChildren(node, output);
}
}
}
function outputNode(node)
{
var whitespace = /^\s+$/g;
if(node.nodeType === 1)
{
console.log("element: " + node.tagName);
}else if(node.nodeType === 3)
{
//clear whitespace text nodes
node.data = node.data.replace(whitespace, "");
if(node.data)
{
console.log("text: " + node.data);
}
}
}
Example: http://jsfiddle.net/ee5X6/
In
<p>I\'m in a <span>p</span>aragraph tag!!</p>
you request the first child, which is the text node containing "I\'m in a".
The text "aragraph tag!!" is the third child, which is not logged.
Curiously, the last line containing "p" should never occur, because the span element is not a direct child of container.
I'm not sure it is what you need or if it is possible in your environment but jQuery can accomplish something similar quite easily. Here is a quick jQuery example that might work.
<html>
<head>
<script src="INCLUDE JQUERY HERE">
</script>
</head>
<body>
<span>
<span>I\'m in a span! </span><div> I\'m in a div! </div><p>I\'m in a <span>p</span>aragraph tag!!</p>
</span>
<script>
function traverse(elem){
$(elem).children().each(function(i,e){
console.log($(e).text());
traverse($(e));
});
}
traverse($("body").children().first());
</script>
</body>
<html>
Which gives the following console output:
I\'m in a span!
I\'m in a div!
I\'m in a paragraph tag!!
p

Remove parent element after removing last child element

I have a list of elements on a page, for the sake of discussion we can say I have the following:
<div id="group_01">
<div id="entry_1-01">stuff x</div>
<div id="entry_1-02">stuff x</div>
</div>
<div id="group_02">
<div id="entry_2-01">stuff x</div>
<div id="entry_2-02">stuff x</div>
</div>
The delete link calls an Ajax request and deletes the entry, after a succesful Ajax call, the entry div is removed from the page. My question is:
How can I remove the containing group div once all of it's entries have been deleted?
I hope that's a detailed enough question. I feel like this isn't anything new, yet two days of search has resulted in nothing.
Before you delete the child element, get its parent, count the number of children, and then after deleting the child, delete the parent if the child count is zero. Here is a quicky piece of sample code:
function d (x)
{
var e = document.getElementById(x);
var p = e.parentNode;
p.removeChild (e);
if (p.childNodes.length == 0) {
var pp = p.parentNode;
pp.removeChild (p);
}
}
I added onclicks to your divs like this:
<div id="group_01">
<div id="entry_1_01">stuff 11<a onclick="d('entry_1_01');" href="#delete">x</a></div>
<div id="entry_1_02">stuff 12<a onclick="d('entry_1_02');" href="#delete">x</a></div>
</div>
I also changed the link to "#delete". You could tidy this up in various ways.
A function like this should would work:
function removeNodeIfEmpty(node) {
var container = document.getElementById(node);
var nodeCount = 0;
for (i = 0; i < container.childNodes.length, i++) {
if (container.childNodes[i].nodeType == 1) {
nodeCount += 1;
}
}
if (nodeCount < 1) {
container.parentNode.removeChild(node);
}
}
This should account for the whitespace issue.
Assuming you do something like this to remove an entry:
entryDiv.parentNode.removeChild(entryDiv);
then you should be able to use the following code to remove the group div when the last child is removed:
var groupDiv = entryDiv.parentNode;
groupDiv.removeChild(entryDiv);
if (!groupDiv.firstChild) {
groupDiv.parentNode.removeChild(groupDiv);
}
...although you need to watch out for whitespace-only text nodes, if these entries haven't been created directly by script.
Really depends what library you're using
http://docs.jquery.com/Traversing/parent#expr
should be a suitable expression

Categories

Resources