How can I create a syntax like vue js in vanilla JavaScript? - javascript

<div id="">
<span>{{msg}}</span>
</div>
Let's think msg is variable of JavaScript and now I want to get the parent tag of {{msg}} and push a new value by innerHTML, here {{msg}} working as an identity.
demo JavaScript example:
<script>
var msg = "This is update data";
{{msg}}.parentElement.innerHTML=msg;
</scritp>
This is not actual JavaScript code, only for better understanding.

You can use jquery easily to find that element and then replace the text
var msg = "This is update data";
$(`span:contains(${msg})`).html("Its New");
In javascript:
var spanTags = document.getElementsByTagName("span");
var msg = "This is update data";
var found;
for (var i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent == msg) {
found = spanTags[i];
break;
}
}
Now, you have found that element in found and you can now change its text
if (found) {
found.innerHTML = "New text";
}

The simplest approach is to treat the entire document as a string and then re-parse it when you're done.
The .innerHTML property is both an HTML decompiler and compiler depending on weather you're reading or writing to it. So for example if you have a list of variables that you want to replace in your document you can do:
let vars = {
msg: msg, // pass value as variable
test_number: 10, // pass value as number
test_str: 'hello' // pass value as string
};
let htmlText = document.body.innerHTML;
// find each var (assuming the syntax is {{var_name}})
// and replace with its value:
for (let var in vars) {
let pattern = '\\{\\{\\s*' + var + '\\s*\\}\\}';
let regexp = new RegExp(pattern, 'g'); // 'g' to replace all
htmlText = htmlText.replace(regexp, vars[var]);
}
// Now re-parse the html text and redraw the entire page
document.body.innerHTML = htmlText;
This is a quick, simple but brutal way to implement the {{var}} syntax. As long as you've correctly specified/designed the syntax to make it impossible to appear in the middle of html tags (for example <span {{ msg > hello </ }} span>) then this should be OK.
There may be performance penalties redrawing the entire page but if you're not doing this all the time (animation) then you would generally not notice it. In any case, if you are worried about performance always benchmark your code.
A more subtle way to do this is to only operate on text nodes so we don't accidentally mess up real html tags. The key to doing this is to write your own recursive descent parser. All nodes have a .childNodes attribute and the DOM is strictly a tree (non-cyclic) so we can scan the entire DOM and search for the syntax.
I'm not going to write complete code for this because it can get quite involved but the basic idea is as follows:
const TEXT_NODE = 3;
let vars = {
msg: msg, // pass value as variable
test_number: 10, // pass value as number
test_str: 'hello' // pass value as string
};
function walkAndReplace (node) {
if (node.nodeType === TEXT_NODE) {
let text = node.nodeValue;
// Do what you need to do with text here.
// You can copy the RegExp logic from the example above
// for simple text replacement. If you need to generate
// new DOM elements such as a <span> or <a> then remove
// this node from its .parentNode, generate the necessary
// objects then add them back to the .parentNode
}
else {
if (node.childNodes.length) {
for (let i=0; i<node.childNodes.length; i++) {
walkAndReplace(node.childNodes[i]); // recurse
}
}
}
}
walkAndReplace(document.body);

Related

Onclick event in literal string in Preload for electron

Well this is a thing i need help of some one who know how to get the element from a foreach that innerHTML into a table data that comes from a data base, i did it by this way but it is not the most optimize way, so i need help to do it by other way, i add an onclick event with javascript and the only thing i make by doing this is that the only button that works is the one on top of the table or the bottom button of the table.
This is the way i make it work:
//Preload
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld(
"electron", {
printmovieonpreload: (results) => ipcRenderer.on("send-movie", (event, results) => {
mylist.innerHTML = " "
results.forEach(elements => {
mylist.innerHTML += `<tr><td> ${elements.movie-name} </td>
<td> ${elements.movie-duration} min </td><td><button id="btn" value="${elements.id-movie}" "/*this get all the onclick events of the page*/${onclick = deletefromtable}" type="button" class="fas cli fa-trash-alt"></button></td>
</tr>`;
});
})
});
async function deletefromtable(e) {
/* this is were i filter from id of the onclick event and how i reduce the value of the button to delete it*/
if (e.srcElement.id == "btn") {
const obj = {
id: e.srcElement.value
}
await ipcRenderer.invoke('delete_movie', obj);
}
}
It's better to not do this by string interpolation, but by letting the browser (in Electron's terms, Chromium) create elements of the wanted type and then setting their contents and appending them to the parent. This can (and should) be done like so:
// inside your forEach function
var tr = document.createElement ("tr");
var td1 = document.createElement ("td");
var td2 = document.createElement ("td");
var td3 = document.createElement ("td");
var btn = document.createElement ("button");
td1.textContent = elements ["movie-name"];
td2.textContent = elements ["movie-duration"] + " min";
btn.id = "btn";
btn.classList.add ("fas", "cli", "fa-trash-alt");
btn.type = "button";
btn.value = elements ["id-movie"];
btn.onclick = delete_from_table;
td3.appendChild (btn);
tr.appendChild (td1);
tr.appendChild (td2);
tr.appendChild (td3);
myList.appendChild (tr);
For this to work, however, you must meet the following conditions:
The index strings for the elements array must be correct. I'm pretty sure that your code using string interpolation would throw errors, because - is an operator and cannot be used when referencing indices using the dot operator.
delete_from_table must be a function, not a string. Since it is not included in the code snippet you have posted, I cannot verify if my solution would work based off of that. However, it seems that you would rather like to bind updatestatuscero to the onclick event of the button. If so, simply replace delete_from_table with this function's name.
Just some more tips regarding general JavaScript:
Use braces wherever possible to make the code more intelligible. For example, you could have written your code like so:
contextBridge.exposeInMainWorld(
"electron", {
printmovieonpreload: (results) => {
ipcRenderer.on("send-movie", (event, results) => {
mylist.innerHTML = " "
results.forEach(elements => {
mylist.innerHTML += `<tr><td> ${elements.movie-name} </td>
<td> ${elements.movie-duration} min </td><td><button id="btn" value="${elements.id-movie}" "/*this get all the onclick events of the page*/${onclick = delete_from_table}" type="button" class="fas cli fa-trash-alt"></button></td>
</tr>`;
});
});
}
}
});
This way, the scope of the functions can be understood at first glance. Both you and any other person having to work with this code in the future will be very pleased if you keep it organised.
When using string interpolation, you can execute operations. This is why I stated that - is not valid in an index above. The reason for this is that any expression inside ${} in an interpolated string will be evaluated and its return value will be included in the string. Thus,
var testObject = {
"test-prop": "def"
};
var stringA = `abc ${testObject.test-prop} ghi`; // Throws: ReferenceError, prop is not defined
var stringB = `abc ${testObject["test-prop"]} ghi`; // yields "abc def ghi"
What the expression inside stringA tries to do is to actually subtract prop from testObject.test, neither of which are defined.
Also, with string interpolation, you have executed an assignment (${ onclick = delete_from_table}) which caused the new variable onclick to be defined, the method to be (correctly) written to the HTML, but not bound to the buttons' onclick event! Instead, you could have done onclick="${delete_from_table}" just like you did with the value property.
Don't ever insert HTML by using element.innerHTML! Take a look at MDN -- this method has some pretty unexpected implications. Better sanitise your HTML first, use element.insertAdjacentHTML() or simply create elements and add them to a parent element by using element.appendChild() as I showed you above. This way is also, in my opinion, but tastes differ, the cleanest way possible.

Updating variables inside js template literals

I have 1 string named a which stores large html element with js template literals by calling objects property . I am keeping those element in my div id hello. Now I want to update data inside js template literal when I change objects property without keeping elements of a in div again.
my code:-
var object = {name:'alisha', age:18 , count:1000};
var a = `<div class="nav">
<span>name:</span><span>${object.name}<span><br>
<span>age:</span><span>${object.age}<span><br>
<span>count:</span><span>${object.count}<span><br>
<input type="text">
</div>`;
var el = document.getElementById('hello');
el.innerHTML = a;
var replace_count = 0;
setInterval(function(){
replace_count = replace_count + 1;
var object.count = replace_count;
},2000)
Yes I have alternative idea of doing this but I can't follow those idea
My Ideas:-
I can give id or class to each span containing js template literals.
I can keep all the elements of variable a by updating objects property in div of id hello.
Please give me idea how can I do this.
You need to place the object and the string within a function to get this to work.
function doReplace(count) {
var object = {name:'alisha', age:18 , count};
var a = `<div class="nav">
<span>name:</span><span>${object.name}<span><br>
<span>age:</span><span>${object.age}<span><br>
<span>count:</span><span>${object.count}<span><br>
<input type="text">
</div>`;
var el = document.getElementById('hello');
el.innerHTML = a;
}
var replace_count = 0;
doReplace(replace_count);
setInterval(function(){
replace_count = replace_count + 1;
doReplace(replace_count);
},200)
<div id="hello"></div>
I change the time to 200ms instead of 2s so it is easier to see.
But the string literals must be recreated each time. It is a literal, not a template.
If you don't want to replace the entire innerHTML every time, then you need to set the innerHTML once and then change the textContent of the correct span during each step of the interval.
But, as I said above, these are not really templates and the ES6 name of template literal is misleading. There is no binding in vanilla JavaScript and Template Literals are a one time generation. So you need to either regenerate the literal after the data changes or just change the innerHTML or textContent of a specific element.
UPDATE
You must understand that vanilla JavaScript does not have built in data binding. Template Literals are really just a simplification of string concatenation. But they are not bindable templates.
You can always look into any of the multiple of template tools out there. If you don't want something as heavy as React, Vue or Angular then you can look into things like Rivets, Simulacra, knockout or any other of the many small data binding libraries out there.
var object = {name:'alisha', age:18 , count:0};
function render(obj) {
var binders = document.querySelectorAll('[bind]');
binders.forEach(
(el) => {
var field = el.getAttribute('bind');
if (field && (obj[field] !== undefined)) {
el.innerHTML = obj[field];
}
}
);
}
var replace_count = 0;
render(object);
setInterval(function(){
object.count++;
render(object);
},200)
<div class="nav">
<div>name: <span bind="name"></div>
<div>age: <span bind="age"></div>
<div>count: <span bind="count"></div>
<input type="text">
</div>
This is a simple binding library I just wrote. It is under 300 bytes. It probably needs some enhancements, but it shows that you can create a very simple binding library.
If these answers don't help then please describe why they don't help. I can help if you don't describe why.
Here this simple function do everything.
const getTemplateByInterpolatingParams = (templateHTML, placeholders) => {
try {
const names = Object.keys(placeholders);
const values = Object.values(placeholders);
return new Function(...names, `return \`${templateHTML}\`;`)(...values);
} catch (error) {
console.log('Error in template interpolation', error);
return error;
}
};
Pass your template with template literals and object with same keys
If you will use jQuery, it is easy:
HTML
<span>count:</span><span class="count"></span>
JS
var replace_count = 0;
setInterval(function(){
$('.nav span.count','#hello').text(replace_count++);
}, 2000);
This will change the data in all span.count in div#hello every 2 sec.
If you render ${object.count}, than it is a normal Text, or you use view.js or angular with special template-engine.
test it on fiddle:
https://jsfiddle.net/chrobaks/r2a1anw6/

HTML and JS: surrounding every word in the document with a <span> tag

I am trying to use Javascript to modify an existing HTML document so that I can surround every word of text in the web page with a span tag that would have a counter. This is a very specific problem so I am going to provide an example case:
<body><p>hello, <br>
change this</p>
<img src="lorempixel.com/200/200> <br></body></html>
This should change to:
<body><p><span id="1">hello,</span>
<br> <span id="2"> change</span><span id="3"> this</span> </p>
<br> <img src="lorempixel.com/200/200> <br></body></html>
I am thinking or regex solutions but they get truly complicated and I am not sure of how to ignore tags and change text without completely breaking the page.
Any thoughts appreciated!
Don't use regex on raw HTML. Use it only on text. This is because regex is a context free parser but HTML is a recursive language. You need a recursive descent parser to properly handle HTML.
First a few useful features of the DOM:
document.body is the root of the DOM
Every node of the DOM has a childNodes array (even comments, text, and attributes)
Element nodes such as <span> or <h> don't contain text, instead they contain text nodes that contain text.
All nodes have a nodeType property and text node is type 3.
All nodes have a nodeValue property that holds different things depending on what kind of node it is. For text nodes nodeValue contains the actual text.
So, using the information above we can surround all words with a span.
First a simple utility function that allows us to process the DOM:
// First a simple implementation of recursive descent,
// visit all nodes in the DOM and process it with a callback:
function walkDOM (node,callback) {
if (node.nodeName != 'SCRIPT') { // ignore javascript
callback(node);
for (var i=0; i<node.childNodes.length; i++) {
walkDOM(node.childNodes[i],callback);
}
}
}
Now we can walk the DOM and find text nodes:
var textNodes = [];
walkDOM(document.body,function(n){
if (n.nodeType == 3) {
textNodes.push(n);
}
});
Note that I'm doing this in two steps to avoid wrapping words twice.
Now we can process the text nodes:
// simple utility functions to avoid a lot of typing:
function insertBefore (new_element, element) {
element.parentNode.insertBefore(new_element,element);
}
function removeElement (element) {
element.parentNode.removeChild(element);
}
function makeSpan (txt, attrs) {
var s = document.createElement('span');
for (var i in attrs) {
if (attrs.hasOwnProperty(i)) s[i] = attrs[i];
}
s.appendChild(makeText(txt));
return s;
}
function makeText (txt) {return document.createTextNode(txt)}
var id_count = 1;
for (var i=0; i<textNodes.length; i++) {
var n = textNodes[i];
var txt = n.nodeValue;
var words = txt.split(' ');
// Insert span surrounded words:
insertBefore(makeSpan(words[0],{id:id_count++}),n);
for (var j=1; j<words.length; j++) {
insertBefore(makeText(' '),n); // join the words with spaces
insertBefore(makeSpan(words[j],{id:id_count++}),n);
}
// Now remove the original text node:
removeElement(n);
}
There you have it. It's cumbersome but is 100% safe - it will never corrupt other tags of javascript in your page. A lot of the utility functions I have above can be replaced with the library of your choice. But don't take the shortcut of treating the entire document as a giant innerHTML string. Not unless you're willing to write an HTML parser in pure javascript.
This sort of processing is always a lot more complex than you think. The following will wrap sequences of characters that match \S+ (sequence of non–whitespace) and not wrap sequences that match \s+ (whitespace).
It also allows the content of certain elements to be skipped, such as script, input, button, select and so on. Note that the live collection returned by childNodes must be converted to a static array, otherwise it is affected by the new nodes being added. An alternative is to use element.querySelectorAll() but childNodes has wider support.
// Copy numeric properties of Obj from 0 to length
// to an array
function toArray(obj) {
var arr = [];
for (var i=0, iLen=obj.length; i<iLen; i++) {
arr.push(obj[i]);
}
return arr;
}
// Wrap the words of an element and child elements in a span
// Recurs over child elements, add an ID and class to the wrapping span
// Does not affect elements with no content, or those to be excluded
var wrapContent = (function() {
var count = 0;
return function(el) {
// If element provided, start there, otherwise use the body
el = el && el.parentNode? el : document.body;
// Get all child nodes as a static array
var node, nodes = toArray(el.childNodes);
var frag, parent, text;
var re = /\S+/;
var sp, span = document.createElement('span');
// Tag names of elements to skip, there are more to add
var skip = {'script':'', 'button':'', 'input':'', 'select':'',
'textarea':'', 'option':''};
// For each child node...
for (var i=0, iLen=nodes.length; i<iLen; i++) {
node = nodes[i];
// If it's an element, call wrapContent
if (node.nodeType == 1 && !(node.tagName.toLowerCase() in skip)) {
wrapContent(node);
// If it's a text node, wrap words
} else if (node.nodeType == 3) {
// Match sequences of whitespace and non-whitespace
text = node.data.match(/\s+|\S+/g);
if (text) {
// Create a fragment, handy suckers these
frag = document.createDocumentFragment();
for (var j=0, jLen=text.length; j<jLen; j++) {
// If not whitespace, wrap it and append to the fragment
if (re.test(text[j])) {
sp = span.cloneNode(false);
sp.id = count++;
sp.className = 'foo';
sp.appendChild(document.createTextNode(text[j]));
frag.appendChild(sp);
// Otherwise, just append it to the fragment
} else {
frag.appendChild(document.createTextNode(text[j]));
}
}
}
// Replace the original node with the fragment
node.parentNode.replaceChild(frag, node);
}
}
}
}());
window.onload = wrapContent;
The above addresses only the most common cases, it will need more work and thorough testing.

Adding a javaScript array to a HTML page?

Im trying to add an array to a webpage. I have tried a few different pieces of code show below but none of them work. I would like the output to be similar to a list like:
text1
text2
text3
...
The code I have used so far is:
var i;
var test = new Array();
test[0] = "text1";
test[1] = "text2";
test[2] = "text3";
// first attempt
$('#here').html(test.join(' '));
// second attempt
$(document).ready(function() {
var testList="";
for (i=0;i<test.length; i++) {
testList+= test[i] + '<br />';
}
$('#here').html('testList');
songList="";
});
I am quite new to javaScript so I am not sure if I have just made a small mistake or if Im doing this in the wrong way. Also, above is a copy of all the code in my javaScript file and some places online are saying I need to import something? Im not sure!
Thanks
Try without quotes:
$('#here').html(testList);
-or-
$('#here').html(test.join('<br />'));
Another approach:
var html = ''; // string
$.each(test,function(i,val){ // loop through array
var newDiv = $('<div/>').html(val); // build a div around each value
html += $('<div>').append(newDiv.clone()).remove().html();
// get the html by
// 1. cloning the object
// 2. wrapping it
// 3. getting that html
// 4. then deleting the wrap
// courtesy of (http://jquery-howto.blogspot.com/2009/02/how-to-get-full-html-string-including.html)
});
$('#here').html(html);
There might be more code in the latter, but it'll be cleaner in the long run if you want to add IDs, classes, or other attributes. Just stick it in a function and amend the jQuery.
Try changing the line
$('#here').html('testList')
to
$('#here').html(testList)
What you have works if you remove the single quotes from testList. However, if you would like an actual unordered list you can do this. (here's a jsFiddle)
var test = new Array();
test[0] = "text1";
test[1] = "text2";
test[2] = "text3";
// first attempt
$('#here').html(test.join(' '));
// second attempt
$(document).ready(function() {
var testList=$("<ul></ul>");
for (var i=0;i<test.length; i++) {
$(testList).append($("<li></li>").text(test[i]));
}
$('#here').html(testList);
songList="";
}); ​
This line:
$('#here').html('testList');
shouldn't have single quotes around testList - you want to use the content of the variable, not the string literal "testList".
Don't pass the variable as a string : $('#here').html('testList'); Pass it without quotes : $('#here').html(testList);
Here's the simplest version:
$(document).ready(function() {
var test = ["text1", "text2", "text3"];
$('#here').html(test.join("<br>"));
});

trouble creating nested dom nodes in javascript

I've a function that takes an object as a parameter, and uses the structure of the object to create nested DOM nodes, but I receive the following error:
http://new.app/:75NOT_FOUND_ERR: DOM Exception 8: An attempt was made to reference a Node in a context where it does not exist.
What I would like my function to do, is, when supplied with a suitable object as a parameter, example:
var nodes = {
tweet: {
children: {
screen_name: {
tag: "h2"
},
text: {
tag: "p"
}
},
tag: "article"
}
};
It would create the following DOM nodes:
<article>
<h2></h2>
<p></p>
</article>
Here is my attempt so far:
function create(obj) {
for(i in obj){
var tmp = document.createElement(obj[i].tag);
if(obj[i].children) {
tmp.appendChild(create(obj[i].children)); /* error */
};
document.getElementById("tweets").appendChild(tmp);
};
};
I'm already struggling!
Ideally I'd like to eventually add more child key's to each object, not just tag, but also id, innerHTML, class etc.
Any hel would be much appreciated, though please: I'm sure a framework or library could do this for me in just a few lines of code, or something similar, but I'd prefer not to use one for this particular project.
If you could briefly explain your answers too it'd really help me learn how this all works, and where I went wrong!
Thank you!
NB: I've changed and marked the line in my function that the error message is talking about.
I changed it from:
mp.appendChild(obj[i].children);
to:
mp.appendChild(create(obj[i].children));
This is because I want any nested keys in the children object to also be created, so screen_name had a children key, they too would be created. Sorry, I hope you can understand this!
I'm looking at http://jsperf.com/create-nested-dom-structure for some pointers, this may help you too!
Your "create" function is going to have to be written recursively.
To create a node from your data (in general), you need to:
Find the "tag" property and create a new element
Give the element the "id" value of the element (taken from the data)
For each element in "children", make a node and append it
Thus:
function create(elementDescription) {
var nodes = [];
for (var n in elementDescription) {
if (!elementDescription.hasOwnProperty(n)) continue;
var elem = elementDescription[n];
var node = document.createElement(elem.tag);
node.id = n; // optional step
var cnodes = create(elem.children);
for (var c = 0; c < cnodes.length; ++c)
node.appendChild(cnodes[c]);
nodes.push(node);
}
return nodes;
}
That will return an array of document elements created from the original "specification" object. Thus from your example, you'd call:
var createdNodes = create(nodes);
and "createdNodes" would be an array of one element, an <article> tag with id "tweets". That element would have two children, an <h2> tag with id "screen_name" and a <p> tag with id "text". (Now that I think of it, you might want to skip the "id" assignment unless the node description has an explicit "id" entry, or something.)
Thus if you have a <div> in your page called "tweets" (to use your example, though if so you'd definitely want to cut out the "id" setting part of my function), you'd add the results like this:
var createdNodes = create(nodes), tweets = document.getElementById('tweets');
for (var eindex = 0; eindex < createdNodes.length; ++eindex)
tweets.appendChild(createdNodes[eindex]);
I added a function appendList that accepts a list of elements, and the container to append to. I removed the append to "tweets" part out of the create function to more effectively separate your code.
function create(obj) {
var els = [];
for(i in obj){
var tmp = document.createElement(obj[i].tag);
var children;
if(children = obj[i].children) {
var childEls = create(children);
appendList(childEls, tmp);
}
els.push(tmp);
};
return els;
};
function appendList(list, container){
for(var i = 0, el; el = list[i]; i++){
container.appendChild(el);
}
};
// gets an array of root elements populated with children
var els = create(nodes);
// appends the array to "tweets"
appendList(els, document.getElementById("tweets"));
Building on the previous answer:
I think you still need to create the element you're trying to append:
tmp.appendChild(children[prop].tag);
should be
tmp.appendChild(document.createElement(children[prop].tag));
function create(obj) {
for(i in obj){
var tmp = document.createElement(obj[i].tag);
var children;
if(children = obj[i].children) {
for(var prop in children)
tmp.appendChild(document.createElement(children[prop].tag));
}
document.getElementById("tweets").appendChild(tmp);
};
};

Categories