Today i got the problem to reset my <input type="file"> after uploading with XmlHttpRequest. After some search for a solution i only found jQuery related stuff, but i rather want to implement it in pure Javascript (learning). So i created a function for it, named document.replaceElement, and the alias functions document.replaceElementById and document.replaceElementsByTagName.
I want to share this functions, and my question is now: How to optimize them? I'm not a freak as some others out there, so if someone can help optimizing them, try out. I'm really interested in it.
Please to not refer to a jQuery again, i wont use it, and i know how to make this possible with jQuery...
THANK YOU
The Javascript (replace-element.js)
// define replaceElement
// param E = element to replace
// param N = replace with this element
// if not given (undefined), recreate the one from the dom!
// if empty replace with empty text node
// if given string replace with innerHTML of string
// if given as array or nodeList, replace with the full list
// param R = used to return the new element instead of replace E
// primarily used for recursive call
document.replaceElement = function(E,N,R) {
// check if given plain string
if (typeof N == 'string') {
// in case of string use innerHTML on a new Element (used <p> here)
var X = document.createElement('p');
X.innerHTML = N;
// if multiple elements are inserted, check all the nodes and push
// them to N, which will laterly be inserted using insertBefore
var S = X.firstChild;
if (S != null) {
N = [];
}
while (S != null) {
if (S.nodeType == 1) {
N.push(S);
}
S = S.nextSibling;
}
// if theres only one element, there is no need to use as array
if (N.length && N.length == 1) {
N = N[0];
}
}
// if the given replace-to value (see N parameter) is not given
// lookup the dom and recreate the element with its originial contents
else if (!N) {
// checked for elements itself here...
if (E.nodeType == 1) {
// create new element
N = document.createElement(E.nodeName);
// i needed to walk through all the attributed on the given element
// and push them to the attributes of the new element
for(var i = 0;i < E.attributes.length;i++) {
// some struggling here with .value and .nodeValue, as well as setAttribute
// and setAttributeNode here, because of different js versions
if (typeof N.setAttribute !== undefined) {
if (E.attributes[i].value) {
N.setAttribute(E.attributes[i].nodeName,E.attributes[i].value);
} else {
N.setAttribute(E.attributes[i].nodeName,E.attributes[i].nodeValue);
}
} else {
N.setAttributeNode(E.attributes[i].cloneNode(false));
}
}
// now walk the given elements child list and append them
// to the element using recursive calls for creation
var S = E.firstChild;
while (S != null) {
N.appendChild(document.replaceElement(S,undefined,true));
S = S.nextSibling;
}
}
// got some errors here, so text nodes are also checked
// and inserted with createTextNode. TODO: use .data here?
else if (E.nodeType == 3) {
N = document.createTextNode(E.nodeValue);
}
// all other types of elements are cloned directly
else {
N = E.cloneNode(true);
}
}
// check if this is not an recursive call...
if (!R) {
// if the new element has length, it is a nodeList or array!
// walking that and use insertBefore on the parent node
if (N.length !== undefined) {
for (i=0;i<N.length;i++) {
E.parentNode.insertBefore(N[i],E);
}
// the original element is deleted if empty string given
E.parentNode.removeChild(E);
} else {
// the given element is replaced with the new one using replaceChild...
E.parentNode.replaceChild(N,E);
}
}
return N;
}
// simple *ById implementation for replaceElement
document.replaceElementById = function(E,C) {
return document.replaceElement(document.getElementById(E),C);
}
// another simple *ByName for replaceElement
document.replaceElementsByName = function(E,C) {
E = document.getElementsByName(E);
var Z = [];
for (var i=0;i<E.length;i++) {
Z.push(document.replaceElement(E[i],C));
}
return Z;
}
HTML Code (replace-element.html)
<html>
<head>
<meta charset="utf-8">
<title>Test replaceElement</title>
<script type="text/javascript" src="replace-element.js"></script>
</head>
<body>
<div id="my-div" name="named-div">
<input type="file" id="my-button" name="named-button" multiple="true" />
<input type="text" value="" placeholder="type in text" />
</div>
<br /><br />
<button onclick="document.replaceElementById('my-button')">replace BUTTON by id</button>
<button onclick="document.replaceElementsByName('named-button')">replace BUTTON by name</button>
<br /><br />
<button onclick="document.replaceElementById('my-div')">replace DIV by id</button>
<button onclick="document.replaceElementsByName('named-div')">replace DIV by name</button>
<br /><br />
<button onclick="document.replaceElementById('my-div','<p>YES, <strong>REPLACED</strong>!</p>')">replace DIV by p tag</button>
<button onclick="document.replaceElementsByName('named-div','<p>YES, <strong>REPLACED</strong>!</p>')">replace DIV by p tag</button>
<br /><br />
<button onclick="document.replaceElementById('my-button','')">delete BUTTON by id</button>
<button onclick="document.replaceElementsByName('named-button','')">delete BUTTON by name</button>
<button onclick="document.replaceElementById('my-div','')">delete DIV by id</button>
<button onclick="document.replaceElementsByName('named-div','')">delete DIV by name</button>
</body>
</html>
Related
I am trying to do a web app similar to google calendar. I have done the object and methods within it but now it's time to be able to add what I want as a task. My idea is for the user to add something to the input and that input being console.logged for now.
Any idea?
HTML
<div class="new-task" id="task-input">
<div id="add-new-task">Task: <input type="text"></div>
<div id="add-time">Time: <input type="text"></div>
<button class ="save-task" onclick="">Save task</button>
</div>
Javascript
var idCounter = 0
var tasksManager = {
array: [],
add: function(task){
taskObject = {
title: task,
idVerification: idCounter ++
}
tasksManager.array.push(taskObject)
},
show:function(id){
var i;
for (i = 0; i < tasksManager.array.length; i++) {
if(id === tasksManager.array[i].idVerification){
return tasksManager.array[i]
}
}
},
delete:function(task){
if(this.show){
tasksManager.array.splice(task)
}
}
}
var newTask = document.getElementById("add-new-task")
newTask.addEventListener('click',tasksManager.add())
console.log(tasksManager.array)
As you can see with console.log above the array index [0] is logged as undefined but I wanted the user to write in the input " Go to the gym" and this to be logged within the array.
Thanks
Some issues:
You are not assigning the click handler. Instead you execute it immediately (not on click).
When you call .add() you don't provide an argument: the name of the task
The click handler should be on the button element, not on the div that has the input element. And so it will be useful to give that button an id attribute.
You should retrieve the value from the input element, and so it would be more appropriate to give that element an id and not so much the div that wraps it.
The console.log at the end of your script is executed immediately. It should be done only when the user has clicked the button.
Snippet with some corrections (also in the HTML!):
var idCounter = 0
var tasksManager = {
array: [],
add: function(task){
let taskObject = {
title: task,
idVerification: idCounter ++
}
tasksManager.array.push(taskObject)
},
show:function(id){
var i;
for (i = 0; i < tasksManager.array.length; i++) {
if(id === tasksManager.array[i].idVerification){
return tasksManager.array[i]
}
}
},
delete:function(task){
if(this.show){
tasksManager.array.splice(task)
}
}
}
var button = document.getElementById("save-task"); // <-- the button
var input = document.getElementById("add-new-task"); // <-- the input (move the ID attribute to the input!)
button.addEventListener('click', () => {
tasksManager.add(input.value);
console.log(tasksManager.array)
})
<div class="new-task" id="task-input">
<div >Task: <input id="add-new-task" type="text"></div>
<div id="add-time">Time: <input type="text"></div>
<button class ="save-task" id ="save-task" onclick="">Save task</button>
</div>
I got error of Uncaught TypeError: Cannot set property 'innerHTML' of null for below script.
I added the script tags after the body. But still I get the error.
I want to show the text boxes in the same page within the div with the ID showTextBoxes.
Below is the HTML and JS.
function showArray(){
var numofArr = document.getElementById("numofArr").value;
for (let i = 0; i < numofArr; i++) {
var a = document.writeln('<input type="text" name="Fname"><br/><br/>');
document.getElementById('showTextBoxes').innerHTML = a;
}
document.writeln('<input type="submit" name="submit">');
}
<p>Number of arrays(array within 0-9)</p>
<input type="text" id="numofArr" pattern="[0-9]">
<input type="submit" onclick="showArray()" value="Submit"><br><br>
<div id="showTextBoxes"></div>
Actually document.write()and document.writeln() works in a different ways you think.
It actually clears all the document in your case you you are getting null.
See this
If you wanna add some element to your body you can use document.body.innerHTML += string.appendChild() can also be used but its not for stings
function showArray(){
var numofArr = parseInt(document.getElementById("numofArr").value);
for (let i = 0; i < numofArr; i++) {
var a = '<input type="text" name="Fname" /><br/><br/>'
document.getElementById('showTextBoxes').innerHTML += a;
}
document.body.innerHTML += '<input type="submit" name="submit"/>'
}
<body>
<p>Number of arrays(array within 0-9)</p>
<input type="text" id="numofArr" pattern="[0-9]">
<input type="submit" onclick="showArray()" value="Submit"><br><br>
<div id="showTextBoxes"></div>
I think there are several ways, but I would recommend looking at append. Something like this should work:
function showArray(){
var numofArr = document.getElementById("numofArr").value;
for (let i = 0; i < numofArr; i++) {
var textBox = document.createElement("input");
var enter = document.createElement("br");
document.getElementById('showTextBoxes').append( textBox );
document.getElementById('showTextBoxes').append( enter );
}
}
There are various places in your script which prevent it from running correctly. I'll address them step by step so you can follow along.
First of all, you should avoid inline event handlers in your HTML for the same reasons you should avoid inline style declarations. So don't use onclick=" ... " inside your HTML and instead add eventlisteners in your JS. This also gives you the ability to cancel the default behaviour or stop event propagation and such things.
Next thing is, you try to use the value of your numofArr input as upper bounds for your loop without casting it to a Number. Because <input> elements return their value as a String, this is very likely to fail. Besides, you should use the type="number" attribute instead of type="text" on that element. It's not required to do so, but just good measure.
OK, now for the showArray function:
Instead of using document.writeln (or document.write), create elements with document.createElement and add them to the DOM with appendChild.
You can see a working example below. Don't be confused by the byId and makeEl functions, they are just utilities so you don't have to write document.getElementById and document.createElement all the time.
// ====== UTILITY FUNCTIONS ======
function byId (id, root = document) {
return root.getElementById(id);
}
function makeEl (tag) {
return document.createElement(tag);
}
// ====== PROGRAM STUFF ======
function showArray (e) {
e.preventDefault();
let numofArr = parseInt(byId('numofArr').value, 10);
let output = byId('showTextBoxes');
for (let i = 0; i < numofArr; i++) {
let textIn = makeEl('input');
textIn.type = 'text';
textIn.name = 'Fname';
output.appendChild(textIn);
output.appendChild(makeEl('br'));
output.appendChild(makeEl('br'));
}
let submit2 = makeEl('input');
submit2.type = 'submit';
submit2.value = 'Submit';
document.body.appendChild(submit2);
}
byId('submit1').addEventListener('click', showArray, false);
<p>Number of arrays(array within 0-9)</p>
<input type="number" id="numofArr">
<input id="submit1" type="submit" value="Submit"><br><br>
<div id="showTextBoxes"></div>
The function does not seem to delete the Node containing the specified value unless it is first value (in this case 'apples'). The for loop also has to execute twice before deletion of any kind. Why is that so?
function removeSpec()
{
var query = document.getElementById('spec').value; /* Value inputted by user */
elements = document.getElementsByClassName('fruit'); /* Get the li elements in the list */
var myList = document.getElementById("myList3"); /* Var to reference the list */
var length = (document.getElementsByClassName('fruit').length); /* # of li elements */
var checker = 'false'; /* boolean-ish value to determine if value was found */
for(var counter = 0; counter < length; counter ++)
{
if (elements[counter].textContent == query )
{
alert("Counter : " + counter);
myList.removeChild(myList.childNodes[ (counter) ]);
checker="true";
}
}
if ( checker == "false")
{
alert("Not Found");
}
}
The corresponding HTML:
<ul id="myList3">
<li class="fruit" >Apples</li>
<li class="fruit" >Oranges</li>
<li class="fruit" >Banannas</li>
<li class="fruit">Strawberry</li>
</ul>
<form>
Value: <input type="text" name="" value="" id="spec">
<br><br>
</form>
<button type="button" style="height:20px;width:200px" href="javascript:void(0)" onclick="removeSpec()" >
Remove Specified
</button>
childNodes returns a list of all child nodes. That includes text nodes. Between every <li> element you have a text node that contains spaces and a line break. So, childNodes returns a list of 9 nodes, but you are assuming list of 4 nodes (document.getElementsByClassName('fruit').length).
You could use .children instead of .childNodes. .children returns a list of only element nodes. Or better yet, use elements, since that's what you are iterating over.
You also need to stop iterating after you found an removed a node, otherwise you will be trying to access a position that doesn't exist anymore.
function removeSpec()
{
var query = document.getElementById('spec').value; /* Value inputted by user */
elements = document.getElementsByClassName('fruit'); /* Get the li elements in the list */
var myList = document.getElementById("myList3"); /* Var to reference the list */
var length = (document.getElementsByClassName('fruit').length); /* # of li elements */
var checker = 'false'; /* boolean-ish value to determine if value was found */
for(var counter = 0; counter < length; counter ++)
{
if (elements[counter].textContent == query )
{
myList.removeChild(myList.children[ (counter) ]);
// better: myList.removeChild(elements[counter]);
checker="true";
break;
}
}
if ( checker == "false")
{
alert("Not Found");
}
}
<ul id="myList3">
<li class="fruit" >Apples</li>
<li class="fruit" >Oranges</li>
<li class="fruit" >Banannas</li>
<li class="fruit">Strawberry</li>
</ul>
<form>
Value: <input type="text" name="" value="" id="spec">
<br><br>
</form>
<button type="button" style="height:20px;width:200px" href="javascript:void(0)" onclick="removeSpec()" >
Remove Specified
</button>
There are other things that could be improved (e.g. why not assign an actual boolean value to checker?), but they are not related to your question.
I run this code. you should add this line
elements[counter].remove();
instead of this line
myList.removeChild(myList.childNodes[ (counter) ]);
Instead of for loop you can consider of doing it the below way.
check this snippet
function removeSpec() {
var query = document.getElementById('spec').value; /* Value inputted by user */
var elements = document.getElementsByClassName('fruit'); /* Get the li elements in the list */
var myList = document.getElementById("myList3"); /* Var to reference the list */
var length = (document.getElementsByClassName('fruit').length); /* # of li elements */
var checker = 'false'; /* boolean-ish value to determine if value was found */
myList.querySelectorAll('li').forEach(function(item) {
if (item.innerHTML == query)
item.remove();
});
}
<ul id="myList3">
<li class="fruit">Apples</li>
<li class="fruit">Oranges</li>
<li class="fruit">Banannas</li>
<li class="fruit">Strawberry</li>
</ul>
<form>
Value:
<input type="text" name="" value="" id="spec">
<br>
<br>
</form>
<button type="button" style="height:20px;width:200px" href="javascript:void(0)" onclick="removeSpec()">
Remove Specified
</button>
Hope it helps
This might sound crazy, but Chrome seems to parse your HTML unordered list into the following:
NodeList[9]
0: text
1: li.fruit
2: text
3: li.fruit
4: text
5: li.fruit
6: text
7: li.fruit
8: text
length: 9
__proto__: NodeList
Essentially, it appears to be creating a text node in your unordered list for each newline inside the tag. This also explains why deletion only occurs after you call the function a second time - it deletes the text node first, then it deletes the actual element on its second try.
Simple converting your HTML to the following form solves the problem (but is not very pretty):
<ul id="myList3"><li class="fruit">Apples</li><li class="fruit">Oranges</li><li class="fruit">Banannas</li><li class="fruit">Strawberry</li></ul>
There are some workarounds that you can try using. For example, you could try using the childNode.remove() method instead, though not all browsers support this.
Alternatively, something like this might also work:
selectedChildNode.parentNode.removeChild(selectedChildNode);
here the problem is in myList.removeChild(myList.childNodes[ (counter) ]); because myList.childNodes node return 8 values instead of 4. We have elements array with 4 nodes, hence the removing from elements array yields a proper result
Try the code snippet below,
function removeSpec() {
var query = document.getElementById('spec').value;
elements = document.getElementsByClassName('fruit');
var myList = document.getElementById("myList3");
var length = elements.length;
var checker = 'false';
for(var counter = 0; counter < length; counter ++)
{
if (elements[counter].textContent == query )
{
alert("Counter : " + counter);
myList.removeChild(elements[counter]);
checker="true";
}
}
if ( checker == "false")
{
alert("Not Found");
}
}
myList is an array of li element so removeChild on myList is logically not correct.
Also, myList.childNodes doesn't make sense here.
Try
myList[counter].parentNode.removeChild(myList[counter]);
I have this text inside a blockquote:
<blockquote class="tr_bq">
4 Text<br />
20 TExt<br />
2 Another text a little longer<br />
<br />
20 text</blockquote>
I want to add for each line a tag or convert the br to include a class. if the br was including all the line i would know how to do it. This is how i want to end like:
<blockquote class="tr_bq">
<strike>4 Text</strike><br/>
<strike>20 TExt</strike><br/>
<strike>2 Another text a little longer</strike><br/>
<br />
<strike>20 text</strike></blockquote>
or
<blockquote class="tr_bq">
<br class="X">4 Text<br>
<br class="X">20 TExt<br>
<br class="X">2 Another text a little longer<br>
<br />
<br class="X"> 20 text</br></blockquote>
I've tried with wrap but with no sucess, any way to do this?
You can do this by manipulating the inner HTML of the blockquote.
$('.tr_bq').each(function () {
var html = $(this).html();
var newHtml = html.split('<br>').map(function (str) {
return '<strike>' + str + '</strike>';
}).join('<br>');
$(this).html(newHtml);
});
Just to offer a plain-JavaScript means of achieving this, avoiding the (unnecessary) use of a library:
function wrapNodesWith(nodes, tag) {
// if we have neither nodes to wrap, nor a tag to wrap
// them with, we quit here:
if (!nodes || !tag) {
return false;
}
// otherwise:
// we convert the nodes to an array (using Array.prototype.slice,
// in conjunction with Function.prototype.call):
nodes = Array.prototype.slice.call(nodes, 0);
// if the tag parameter passed to the function is a string ('strike'),
// we create that element using document.createElement(tag),
// otherwise we assume we've got an HTMLElement (this is a very
// naive check) and so we use that:
tag = 'string' === typeof tag ? document.createElement(tag) : tag;
// an unitialised variable for use within the following forEach:
var clone;
nodes.forEach(function(n) {
// n is the node over which we're iterating,
// cloning the tag (to avoid multiple calls
// to document.createElement):
clone = tag.cloneNode();
// setting the textContent of the clone to the nodeValue
// of the node (if it's a textNode), or to the textContent of
// element (again a simple check):
clone.textContent = n.nodeType === 3 ? n.nodeValue : n.textContent;
// replacing the childNode, using parentNode.replaceChild(),
// inserting clone and removing n:
n.parentNode.replaceChild(clone, n);
});
}
// finding the first <blockquote> element:
var blockquote = document.querySelector('blockquote'),
// creating an array of the childNodes of the <blockquote>:
children = Array.prototype.slice.call(blockquote.childNodes, 0),
// filtering the children array, retaining only those nodes for
// which the assessment returns true:
textNodes = children.filter(function(n) {
return n.nodeType === 3;
});
// can be called with:
wrapNodesWith(textNodes, 'strike');
// or:
wrapNodesWith(textNodes, document.createElement('strike'));
function wrapNodesWith(nodes, tag) {
if (!nodes || !tag) {
return false;
}
nodes = Array.prototype.slice.call(nodes, 0);
tag = 'string' === typeof tag ? document.createElement(tag) : tag;
var parent, clone;
nodes.forEach(function(n) {
clone = tag.cloneNode();
clone.textContent = n.nodeType === 3 ? n.nodeValue : n.textContent;
n.parentNode.replaceChild(clone, n);
});
}
var blockquote = document.querySelector('blockquote'),
children = Array.prototype.slice.call(blockquote.childNodes, 0),
textNodes = children.filter(function(n) {
return n.nodeType === 3;
});
wrapNodesWith(textNodes, 'strike');
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<blockquote class="tr_bq">
4 Text
<br />20 TExt
<br />2 Another text a little longer
<br />
<br />20 text
</blockquote>
References:
Array.prototype.filter().
Array.prototype.forEach().
Array.prototype.slice().
Conditional ('ternary') operator.
document.createElement().
document.querySelector().
Function.prototype.call().
Node.nodeValue.
Node.replaceChild().
Well,
i managed to get it working with this:
var pre = document.getElementsByTagName('blockquote'),pl = pre.length;
for (var i = 0; i < pl; i++) {
var pro = pre[i].innerHTML.split(/<br>/), pz = pro.length;
pre[i].innerHTML = '';
for (var a=0; a < pz ; a++) {
pre[i].innerHTML += '<strike>' + pro[a] + '</strike><br/>';
}
}
I have a list with about 10 000 customers on a web page and need to be able to search within this list for matching input. It works with some delay and I'm looking for the ways how to improve performance. Here is simplified example of HTML and JavaScript I use:
<input id="filter" type="text" />
<input id="search" type="button" value="Search" />
<div id="customers">
<div class='customer-wrapper'>
<div class='customer-info'>
...
</div>
</div>
...
</div>
<script type="text/javascript">
$(document).ready(function() {
$("#search").on("click", function() {
var filter = $("#filter").val().trim().toLowerCase();
FilterCustomers(filter);
});
});
function FilterCustomers(filter) {
if (filter == "") {
$(".customer-wrapper").show();
return;
}
$(".customer-info").each(function() {
if ($(this).html().toLowerCase().indexOf(filter) >= 0) {
$(this).parent().show();
} else {
$(this).parent().hide();
}
});
}
</script>
The problem is that when I click on Search button, there is a quite long delay until I get list with matched results. Are there some better ways to filter list?
1) DOM manipulation is usually slow, especially when you're appending new elements. Put all your html into a variable and append it, that results in one DOM operation and is much faster than do it for each element
function LoadCustomers() {
var count = 10000;
var customerHtml = "";
for (var i = 0; i < count; i++) {
var name = GetRandomName() + " " + GetRandomName();
customerHtml += "<div class='customer-info'>" + name + "</div>";
}
$("#customers").append(customerHtml);
}
2) jQuery.each() is slow, use for loop instead
function FilterCustomers(filter) {
var customers = $('.customer-info').get();
var length = customers.length;
var customer = null;
var i = 0;
var applyFilter = false;
if (filter.length > 0) {
applyFilter = true;
}
for (i; i < length; i++) {
customer = customers[i];
if (applyFilter && customer.innerHTML.toLowerCase().indexOf(filter) < 0) {
$(customer).addClass('hidden');
} else {
$(customer).removeClass('hidden');
}
}
}
Example: http://jsfiddle.net/29ubpjgk/
Thanks to all your answers and comments, I've come at least to solution with satisfied results of performance. I've cleaned up redundant wrappers and made grouped showing/hiding of elements in a list instead of doing separately for each element. Here is how filtering looks now:
function FilterCustomers(filter) {
if (filter == "") {
$(".customer-info").show();
} else {
$(".customer-info").hide();
$(".customer-info").removeClass("visible");
$(".customer-info").each(function() {
if ($(this).html().toLowerCase().indexOf(filter) >= 0) {
$(this).addClass("visible");
}
});
$(".customer-info.visible").show();
}
}
And an test example http://jsfiddle.net/vtds899r/
The problem is that you are iterating the records, and having 10000 it can be very slow, so my suggestion is to change slightly the structure, so you won't have to iterate:
Define all the css features of the list on customer-wrapper
class and make it the parent div of all the list elements.
When your ajax request add an element, create a variable containing the name replacing spaces for underscores, let's call it underscore_name.
Add the name to the list as:
var customerHtml = "<div id='"+underscore_name+'>" + name + "</div>";
Each element of the list will have an unique id that will be "almost" the same as the name, and all the elements of the list will be on the same level under customer-wrapper class.
For the search you can take the user input replace spaces for underscores and put in in a variable, for example searchable_id, and using Jquery:
$('#'+searchable_id).siblings().hide();
siblings will hide the other elements on the same level as searchable_id.
The only problem that it could have is if there is a case of two or more repeated names, because it will try to create two or more divs with the same id.
You can check a simple implementation on http://jsfiddle.net/mqpsppxm/