Render a native HTML template with JavaScript - javascript

With HTML Templates it makes it easy to stamp out snippets of html.
What is a sane way of populating the stamped out templates? In the MDN link above they render the template as follows:
td = clone2.querySelectorAll("td");
td[0].textContent = "0384928528";
td[1].textContent = "Acme Kidney Beans 2";
This obviously only works if all the elements are the same tag, and ordered in the same way, which is very brittle.
What if I had a template like this:
<template>
<div>
<h2>__heading__</h2>
<label>__label1__</label><input type="text" value="__value1__">
<label>__label2__</label><input type="text" value="__value2__">
<div>__instruction__</div>
<label>__label3__</label><input type="text" value="__value3__">
</div>
</template>
And say one had this data to render it with:
{
__heading__: 'Lots of things',
__label1__: 'label 1',
__value1__: 'value 1',
__label2__: 'label 2',
__value2__: 'value 2',
__instruction__: 'Do the thing',
__label3__: 'label 3',
__value3__: 'value 3',
}
Then the rendered result would be:
<div>
<h2>Lots of things</h2>
<label>label 1</label><input type="text" value="value 1">
<label>label 2</label><input type="text" value="value 2">
<div>Do the thing</div>
<label>label 3</label><input type="text" value="value 3">
</div>
How would one render the template? PS if this is a XY question, you can use some other means to instead of the dunder fields.
I can only think of adding classes or attributes to each element which has a field to populate, and then perform lots of clonedNode.querySelector() ...seems very unelegant.

Note 9/9/2022: There is a proposal for DOM Parts on the table: https://github.com/WICG/webcomponents/blob/gh-pages/proposals/DOM-Parts.md
You could replace content with Template Literal String notation
string:
<h1>Run ${name}! Run!</h1>
Ain't <b>${tooling}</b> great!!
property Object:
{
name: this.getAttribute("name"),
tooling: "Web Components",
}
The parse(str,v) function
creates a new Function
with a String literal (note the back-ticks)
then executes that Function
passing all v Object values
and Returns the parsed String literal
<template id="MY-ELEMENT">
<h1 style="background:${color}"> Run ${name}! Run!</h1>
Ain't <b>${tooling}</b> great!!
</template>
<my-element name="Forrest"></my-element>
<script>
function parse(str, v = {}) {
try {
return new Function("v",
"return((" + Object.keys(v).join(",") + ")=>`" +
str +
"`)(...Object.values(v))")(v) || "";
} catch (e) {
console.error(e);
}
};
customElements.define("my-element", class extends HTMLElement {
color = "gold";
constructor() {
super().attachShadow({mode: 'open'}).innerHTML = parse(
document.getElementById(this.nodeName).innerHTML, // String
{ // properties
name: this.getAttribute("name"),
tooling: "Web Components",
...this, // to show you can add anything you want to the property bag
});
}
})
</script>

One of the possible solution is manipulation of the HTML string itself. There is not much stuff you can do with content inside DOM tree without some special markup (like you mentioned in your question).
So, you clone the template and modify inner HTML of every child inside the template:
// Gather the template from DOM and clone it
const template = document.querySelector('#target_template');
const clonedDocumentFragment = template.content.cloneNode(true);
// Looks like there is no better way to modify HTML of the whole
// DocumentFragment, so we modify HTML of each child node:
Array
.from(clonedDocumentFragment.children)
.forEach(childElement => renderInnerVariables(
childElement,
{
// Pass your values here
}
));
// And here example of the method replacing those values
function renderInnerVariables(targetElement, variables = {}) {
// Reminder: it can be really unsafe to put user data here
targetElement.innerHTML = targetElement.innerHTML.replace(
// Instead of looping through variables, we can use regexp
// to get all the variables in content
/__([\w_]+)__/,
(original, variableName) => {
// Check if variables passed and target variable exists
return variables && variables.hasOwnProperty(variableName)
// Pass the variable value
? variables[variableName]
// Or pass the original string
: original;
}
);
}
But since it's just a string content modification, you can pretty much just use string templates instead.

If I understood correctly you want to create a template based on object.. if yes you can use this as a start..
What I did was to create a function getTagName(key) which will return a specific string based on the object key (it can be made with a switch most probably but eh.. :( switch on regex ) For example __value2__ will return input
Then I created a function createHtmlElementwith with some conditions in order to set the text/value to my element then I iterate through all the properties and append the newly created element
const data = {
__heading__: 'Lots of things',
__label1__: 'label 1',
__value1__: 'value 1',
__label2__: 'label 2',
__value2__: 'value 2',
__instruction__: 'Do the thing',
__label3__: 'label 3',
__value3__: 'value 3',
}
let container = document.getElementById('container');
// this can be done with switch i think, but for demo i did if statements :(
function getTagName(key) {
key = key.replace('__', ''); //remove __ from front
if (/^heading/.test(key)) return "h2";
if (/^label/.test(key)) return "label";
if (/^value/.test(key)) return "input";
if (/^instruction/.test(key)) return "div";
}
function createHtmlElement(name, key, value) {
let element = document.createElement(name);
if (name === 'label' || name === "h2" || name === "div") {
element.innerHTML = value;
}
if (name === "input") {
element.type = "text";
//element.value = key; // this is not working for i don't know what reason :(
element.setAttribute('value', key);
}
// console.log(element) // decomment if you want to see the element as plain html
return element;
}
for (const [key, value] of Object.entries(data)) {
let tagName = getTagName(key);
let element = createHtmlElement(tagName, key, value);
let brTag = document.createElement("br");
container.appendChild(element);
container.appendChild(brTag);
}
<div id="container">
</div>
<!--
<h2>__heading__</h2>
<label>__label1__</label>
<input type="text" value="__value1__">
<label>__label2__</label>
<input type="text" value="__value2__">
<div>__instruction__</div>
<label>__label3__</label>
<input type="text" value="__value3__">
-->

Related

How to loop through HTML elements and populate a Json-object?

I'm looping through all the html tags in an html-file, checking if those tags match conditions, and trying to compose a JSON-object of a following schema:
[
{ title: 'abc', date: '10.10.10', body: ' P tags here', href: '' },
{ title: 'abc', date: '10.10.10', body: ' P tags here', href: '' },
{ title: 'abc', date: '10.10.10', body: ' P tags here', href: '' }
]
But I'd like to create the new entry only for elements, classed "header", all the other elements have to be added to earlier created entry. How do I achieve that?
Current code:
$('*').each((index, element) => {
if ( $(element).hasClass( "header" ) ) {
jsonObject.push({
title: $(element).text()
});
};
if( $(element).hasClass( "date" )) {
jsonObject.push({
date: $(element).text()
});
}
//links.push($(element))
});
console.log(jsonObject)
Result is:
{
title: 'TestA'
},
{ date: '10.10.10' },
{
title: 'TestB'
},
{ date: '10.10.11' }
I'd like it to be at this stage something like:
{
title: 'TestA'
,
date: '10.10.10' },
{
title: 'TestB'
,
date: '10.10.11' }
UPD:
Here's the example of HTML file:
<h1 class="header">H1_Header</h1>
<h2 class="date">Date</h2>
<p>A.</p>
<p>B.</p>
<p>С.</p>
<p>D.</p>
<a class="source">http://</a>
<h1 class="header">H1_Header2</h1>
<h2 class="date">Date2</h2>
<p>A2.</p>
<p>B2.</p>
<p>С2.</p>
<p>D2.</p>
<a class="source">http://2</a>
Thank you for your time!
Based on your example Html, it appears everything you are trying to collect is in a linear order, so you get a title, date, body and link then a new header with the associated items you want to collect, since this appears to not have the complication of having things being ordered in a non-linear fasion, you could do something like the following:
let jsonObject = null;
let newObject = false;
let appendParagraph = false;
let jObjects = [];
$('*').each((index, element) => {
if ($(element).hasClass("header")) {
//If newObject is true, push object into array
if(newObject)
jObjects.push(jsonObject);
//Reset the json object variable to an empty object
jsonObject = {};
//Reset the paragraph append boolean
appendParagraph = false;
//Set the header property
jsonObject.header = $(element).text();
//Set the boolean so on the next encounter of header tag the jsobObject is pushed into the array
newObject = true;
};
if( $(element).hasClass( "date" )) {
jsonObject.date = $(element).text();
}
if( $(element).prop("tagName") === "P") {
//If you are storing paragraph as one string value
//Otherwise switch the body var to an array and push instead of append
if(!appendParagraph){ //Use boolean to know if this is the first p element of object
jsonObject.body = $(element).text();
appendParagraph = true; //Set boolean to true to append on next p and subsequent p elements
} else {
jsonObject.body += (", " + $(element).text()); //append to the body
}
}
//Add the href property
if( $(element).hasClass("source")) {
//edit to do what you wanted here, based on your comment:
jsonObject.link = $(element).next().html();
//jsonObject.href= $(element).attr('href');
}
});
//Push final object into array
jObjects.push(jsonObject);
console.log(jObjects);
Here is a jsfiddle for this: https://jsfiddle.net/Lyojx85e/
I can't get the text of the anchor tags on the fiddle (I believe because nested anchor tags are not valid and will be parsed as seperate anchor tags by the browser), but the code provided should work in a real world example. If .text() doesn't work you can switch it to .html() on the link, I was confused on what you are trying to get on this one, so I updated the answer to get the href attribute of the link as it appears that is what you want. The thing is that the anchor with the class doesn't have an href attribute, so I'll leave it to you to fix that part for yourself, but this answer should give you what you need.
$('*').each((index, element) => {
var obj = {};
if ( $(element).hasClass( "header" ) ) {
obj.title = $(element).text();
};
if( $(element).hasClass( "date" )) {
obj.date = $(element).text()
}
jsonObject.push(obj);
});
I don't know about jQuery, but with JavaScript you can do with something like this.
const arr = [];
document.querySelectorAll("li").forEach((elem) => {
const obj = {};
const title = elem.querySelector("h2");
const date = elem.querySelector("date");
if (title) obj["title"] = title.textContent;
if (date) obj["date"] = date.textContent;
arr.push(obj);
});
console.log(arr);
<ul>
<li>
<h2>A</h2>
<date>1</date>
</li>
<li>
<h2>B</h2>
</li>
<li>
<date>3</date>
</li>
</ul>
Always use map for things like this. This should look something like:
let objects = $('.header').get().map(el => {
return {
date: $(el).attr('date'),
title: $(el).attr('title'),
}
})

Combine multiple elements and append result to a div

function setParagraph(paraList) {
$.each(paraList, function (i, field) {
var pElement = document.createElement('p');
$(pElement).text(field);
//a line that combines the p elements like so:
//<p>First para</p>
//<p>Second para</p>
});
return //all elements for append;
}
I am trying to write code with minimum number of lines to return a "collection" of p elements that needs to be appended to the following div:
$("#somediv").append(setParagraph(jsonValue));
To produce:
<div id="somediv">
<p>First para</p>
<p>Second para</p>
</div>
The method setParagraph is passed json string with collection of string items that are translated into p elements in the method.
I have tried pushing the elements into an array but I don't think that is the right way to go.
Also, I do not wish to use string concatenation in the loop to produce the desired results, unless of course that is the only best way to handle it.
EDIT:
The below works but as I said I am looking for some other solution besides array:
function setParagraph(paraList) {
var arrElements = [];
$.each(paraList, function (i, field) {
var $pElement = $("<p/>").text(field);
arrElements.push($pElement);
});
return arrElements;
}
var jsonValue = ["First para","Second para"];
$("#somediv").append(setParagraph(jsonValue));
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="somediv">
</div>
Shorter
const par = ["one", "two", "three"]
$("#somediv").html(par.map(p => $("<p>", { text: p })))
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="somediv"></div>
You can use $.map(..) for that, here is an example:
function setParagraph(paraList) {
return $.map(paraList, function(item) {
return $("<p>", {
text: item
});
});
}
$("#somediv").append(setParagraph(["one", "two", "three"]));
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="somediv"></div>
I've created a simple fiddle example that I think would suit your needs. It works in a different way, a little bit at least, but with a bit of changing you'd get your desired result.
Here's the fiddle: https://jsfiddle.net/fkoLn4v9/
Here's the code:
// index.js
const paragraphsCollection = [
{
tag: 'p',
content: 'This is the first parargraph',
},
{
tag: 'p',
content: 'This is the second paragraph',
},
];
const setParagraph = (parentElem, contentData) => {
contentData.forEach(({ tag, content }) => {
const domElem = document.createElement(tag);
domElem.innerText = content;
parentElem.appendChild(domElem);
});
};
const divElem = document.querySelector('.parentDiv');
setParagraph(divElem, paragraphsCollection);
HTML:
<div class="parentDiv"></div>

cheerio: Get normal + text nodes

I am using cheerio to parse HTML code in different nodes. I can easily do $("*"), but this gets me only normal HTML nodes and not the separate text nodes. Lets consider 3 user inputs:
One:
text only
I need: single text node.
Two:
<div>
text 1
<div>
inner text
</div>
text 2
</div>
I need: text node + div node + text node in same sequence.
Three:
<div>
<div>
inner text 1
<div>
inner text 2
</div>
</div>
<div>
inner text 3
</div>
</div>
I need: 2 div nodes
Possible?
In hope to help someone, filter function seems to return text nodes also.
I got help from this answer: https://stackoverflow.com/a/6520267/3800042
var $ = cheerio.load(tree);
var iterate = function(node, level) {
if (typeof level === "undefined") level = "--";
var list = $(node).contents().filter(function() { return true; });
for (var i=0; i<=list.length-1; i++) {
var item = list[i];
console.log(level, "(" + i + ")", item.type, $(item).text());
iterate(item, level + "--");
}
}
iterate($.root());
HTML input
<div>
text 1
<div>
inner text
</div>
text 2
</div>
Result
-- (0) tag
text 1
inner text
text 2
---- (0) text
text 1
---- (1) tag
inner text
------ (0) text
inner text
---- (2) text
text 2
I hope the following codes can help you.
const cheerio = require("cheerio");
const htmlText = `<ul id="fruits">
<!--This is a comment.-->
<li class="apple">Apple</li>
Peach
<li class="orange">Orange</li>
<li class="pear">Pear</li>
</ul>`;
const $ = cheerio.load(htmlText);
const contents = $('ul#fruits').contents();
console.log(contents.length);// 9, since nodes like '\n' are included
console.log(new RegExp('^\\s*$').test('\n '));
function isWhitespaceTextNode(node){
if(node.type !== 'text'){
return false;
}
if(new RegExp('^\\s*$').test(node.data)){
return true;
}
return false;
}
//Note here: filter is a function provided by cheerio, not Array.filter
const nonWhitespaceTextContents = contents.filter(nodeIndex=>{
const node = contents[nodeIndex];
if(isWhitespaceTextNode(node)){
return false;
}else{
return true;
}
});
console.log(nonWhitespaceTextContents.length);// 5, since nodes like '\n ' are excluded
nonWhitespaceTextContents.each((_, node)=>console.log(node));
//[comment node]
//[li node] apple
//[text node] peach
//[li node] orange
//[li node] pear
If you want all of the immediate children of a node, both text nodes and tag nodes, use .contents() and filter out whitespace-only text nodes.
Here's the code running on your examples:
const cheerio = require("cheerio"); // 1.0.0-rc.12
const tests = [
// added a div container to make the parent selector consistent
`<div>text only</div>`,
`<div>
text 1
<div>
inner text
</div>
text 2
</div>`,
`<div>
<div>
inner text 1
<div>
inner text 2
</div>
</div>
<div>
inner text 3
</div>
</div>`
];
tests.forEach(html => {
const $ = cheerio.load(html);
const result = [...$("div").first().contents()]
.filter(e => e.type !== "text" || $(e).text().trim())
// the following is purely for display purposes
.map(e => e.type === "text" ? $(e).text().trim() : e.tagName);
console.log(result);
});
Output:
[ 'text only' ]
[ 'text 1', 'div', 'text 2' ]
[ 'div', 'div' ]
If you only want the text nodes and not the tags, see How to get a text that's separated by different HTML tags in Cheerio.

Assert text value of list of webelements using nightwatch.js

I am new to using nightwatch.js.
I want to get a list of elements and verify text value of each and every element with a given string.
I have tried :
function iter(elems) {
elems.value.forEach(function(element) {
client.elementIdValue(element.ELEMENT)
})
};
client.elements('css selector', 'button.my-button.to-iterate', iter);
For another stackoverflow question
But what I am using right now is
waitForElementPresent('elementcss', 5000).assert.containsText('elementcss','Hello')
and it is returning me the output
Warn: WaitForElement found 5 elements for selector "elementcss". Only the first one will be checked.
So I want that it should verify text value of each and every element of list.
All the things can not be done by nightwatch js simple commands , so they have provided the custom command means selenium protocol. Here you can have all the selenium protocol. I have used following code to assert text value of each and every element with a given string "text". Hope it will help you
module.exports = {
'1. test if multiple elements have the same text' : function (browser) {
function iter(elems) {
elems.value.forEach(function(element) {
browser.elementIdText(element.ELEMENT, function(result){
browser.assert.equal(result.value,'text')
})
})
};
browser
.url('file:///home/user/test.html')
.elements('tag name', 'a', iter);
}
};
My HTML snippet
<div id="test">
<a href="google.com" class='red'> text </a>
<a href="#" class='red'> text </a>
<a href="#" class='red'> text 1</a>
</div>
I was able to do it as :
.elements('css selector', 'cssValue', function (elements) {
for(var i=0;i<elements.value.length;i++){
var elementCss = 'div.search-results-item:nth-child(' + (i+1) + ') span';
client.assert.containsText(elementCss,'textValue');
}
})
Put your function iter in a for loop and before that use
client.elements('css selector', '#CollectionClass', function (result) {
if(result.value.length > 1) {
var count;
for(count=1; count<result.value.length; count++) {
result.value.forEach(function(element) {
client.elementIdValue(element.ELEMENT);
client.elementIdText(selectedHighlight.ELEMENT, function(resuddlt) {
this.assert.equal(typeof resuddlt, "object");
this.assert.equal(resuddlt.status, 0);
this.assert.equal(resuddlt.value, "your value");
});
}
}
}
};
You have stated what you have tried (which is good) but you haven't presented us with sanitized HTML that demonstrates the problem (which reduces precision in possible answers).
There are many ways in HTML to contain information, and the built-in Nightwatch containsText will serialize any text it finds within a structure that contains substructures.
So for example, if you have as Juhi suggested,
<div id="test">
<a href="google.com" class='red'> text </a>
<a href="#" class='red'> text </a>
<a href="#" class='red'> text 1</a>
</div>
Then the assertions
.verify.containsText('#test', ' text ') // first one
.verify.containsText('#test', ' text ') // second one
.verify.containsText('#test', ' text 1') // third one
will pass, because they each verify the specific information without the need for writing a loop. Nightwatch will look at the test element and serialize the elements into the string text text text 1
Now if you need a loop for other reasons this is all academic, but your original question seemed to be targeted at how to get the text information out, not necessarily how to execute one possible solution to the problem (which is writing a loop).
I created custom assertions - custom-assertions/hasItems.js with content:
exports.assertion = function hasItems(selector, items) {
this.message = `Testing if element <${selector}> has items: ${items.join(", ")}`;
this.expected = items;
this.pass = selectedItems => {
if (selectedItems.length !== items.length) {
return false;
}
for (let i = 0; i < selectedItems.length; i++) {
if (selectedItems[i].trim() !== items[i].trim()) {
return false;
}
}
return true;
};
this.value = res => res.value;
function evaluator(_selector) {
return [...document.querySelectorAll(_selector)].map(
item => item.innerText
);
}
this.command = cb => this.api.execute(evaluator, [selector], cb);
};

Suggest any Good mustache document

Suggest me any good mustache doc. Also i want to know in a mushtach loop how do i get the count or the loop no. I mean how can i do a for loop in mustache.
In the below code i wish to change the id in every loop
<script src="http://github.com/janl/mustache.js/raw/master/mustache.js"></script>
<script>
var data, template, html;
data = {
name : "Some Tuts+ Sites",
big: ["Nettuts+", "Psdtuts+", "Mobiletuts+"],
url : function () {
return function (text, render) {
text = render(text);
var url = text.trim().toLowerCase().split('tuts+')[0] + '.tutsplus.com';
return '' + text + '';
}
}
};
template = '<h1> {{name}} </h1><ul> {{#big}}<li id="no"> {{#url}} {{.}} {{/url}} </li> {{/big}} </ul>';
html = Mustache.to_html(template, data);
document.write(html)
</script>
<body></body>
You can't get at the array index in Mustache, Mustache is deliberately simple and wants you to do all the work when you set up your data.
However, you can tweak your data to include the indices:
data = {
//...
big: [
{ i: 0, v: "Nettuts+" },
{ i: 1, v: "Psdtuts+" },
{ i: 2, v: "Mobiletuts+" }
],
//...
};
and then adjust your template to use {{i}} in the id attributes and {{v}} instead of {{.}} for the text:
template = '<h1> {{name}} </h1><ul> {{#big}}<li id="no-{{i}}"> {{#url}} {{v}} {{/url}} </li> {{/big}} </ul>';
And as an aside, you probably want to include a scheme in your url:
url : function () {
return function (text, render) {
text = render(text);
var url = text.trim().toLowerCase().split('tuts+')[0] + '.tutsplus.com';
return '' + text + '';
//---------------^^^^^^^
}
}
Demo: http://jsfiddle.net/ambiguous/SFXGG/
Expanding on #mu's answer, you could also keep an index in the data object and have the template refer to it and the function increment it. So you wouldn't need to add i to each item.
see demo : http://jsfiddle.net/5vsZ2/

Categories