XPath evaluation on document created dynamically not working - javascript

Updated/Simplified based on Mathias's comment:
I'm trying to dynamically create an HTML Document and then find elements within the DOM via XPath.
What's odd is that the created Document looks to be properly constructed and querying it with document.querySelector('<some el>') for example works as expected.
However, document.evaluate is always returning null for every XPath.
Update #2: This is true for Chrome + Safari. Everything works as expected in Firefox.
function createDocumentFromHTMLContent(htmlContent) {
const htmlEl = document.createElement('HTML');
htmlEl.innerHTML = htmlContent;
const doctype = document.implementation.createDocumentType('html', '', '');
const doc = document.implementation.createDocument('', 'html', doctype);
doc.replaceChild(htmlEl, doc.firstElementChild);
return doc;
}
function getElementByXpath(path, doc) {
doc = doc || document;
return doc.evaluate(path, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
const pageContent = `
<!DOCTYPE html>
<html>
<head>
<title>Yup</title>
</head>
<body>
<h1>Title</h1>
</body>
</html>
`;
const doc = createDocumentFromHTMLContent(pageContent);
const xpath = '/html[1]/body[1]/h1';
const onDoc = {
viaXPath: getElementByXpath(xpath, doc),
viaSelector: doc.querySelector('h1'),
};
const onDocument = {
viaXPath: getElementByXpath(xpath, document),
viaSelector: document.querySelector('h1'),
};
const summarize = (obj) => `XPath El: ${!!obj.viaXPath}, Selector El: ${!!obj.viaSelector}`;
const summaryEl = document.createElement('p');
summaryEl.innerHTML = `Via Document: ${summarize(onDocument)}<br />Via Doc: ${summarize(onDoc)}`;
document.body.appendChild(summaryEl);
Here's the above in a JSFiddle: https://jsfiddle.net/two2hg0z/
I can't figure out why XPath selection works on one document object, but not the other.
Any help is appreciated! Very stumped.

I'm not entirely sure what happens here in webkit browsers, probably they don't like to Document.replaceChild the documentElement, or maybe it's because you are setting some markup that is actually invalid inside an <html> element (for instance the Doctype should actually be set outside, it can't contain an <html> node etc. but anyway, the correct way to parse a string as a Document is through the use of a DOMParser:
function createDocumentFromHTMLContent(htmlContent) {
return new DOMParser().parseFromString(htmlContent, 'text/html');
}
function getElementByXpath(path, doc) {
doc = doc || document;
return doc.evaluate(path, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}
const pageContent = `
<!DOCTYPE html>
<html>
<head>
<title>Yup</title>
</head>
<body>
<h1>Title</h1>
</body>
</html>
`;
const doc = createDocumentFromHTMLContent(pageContent);
const xpath = '/html[1]/body[1]/h1';
const onDoc = {
viaXPath: getElementByXpath(xpath, doc),
viaSelector: doc.querySelector('h1'),
};
const onDocument = {
viaXPath: getElementByXpath(xpath, document),
viaSelector: document.querySelector('h1'),
};
const summarize = (obj) => `XPath El: ${!!obj.viaXPath}, Selector El: ${!!obj.viaSelector}`;
const summaryEl = document.createElement('p');
summaryEl.innerHTML = `Via Document: ${summarize(onDocument)}<br />Via Doc: ${summarize(onDoc)}`;
document.body.appendChild(summaryEl);
<h1>Title</h1>
Note that if instead of replacing the documentElement, you did set its innerHTML to the one of your generated HTMLElement, it would also have worked in Chrome, but not in Firefox anymore ;-)

Related

'getElementById' on HTMLelement

I have a simple site. It does a fetch to another html page. That's working and I have some response. Now I want a part of the page and select the id
'test' from the HTML. Now I created a HTMLelement, but how can I select a part of it?
How can I do this without adding all the HTML to the dom.
<html>
<body>
<script>
// in real it's a fetch with response text:
const HTML = `<div id="">I dont need this</div>
<div id="test"> I want this</div>`;
const EL = document.createElement('html')
EL.innerHTML = HTML;
console.log(EL);
// This is not working, but I want to do this
console.log(EL.getElementById('test'));
</script>
</body>
</html>
Use a new DOMParser():
// in real it's a fetch with response text:
const HTML = `<div id="">I dont need this</div>
<div id="test"> I want this</div>`;
const EL = new DOMParser()
.parseFromString(HTML, "text/html")
console.log(EL.getElementById('test'));
The trick here is to create a new DOM that you can add the content to instead of creating a second HTML element belonging to your existing document.
var parser = new DOMParser();
var htmlDoc = parser.parseFromString('<div id="parent"></div>', 'text/html');
const html = `<div id="">I dont need this</div>
<div id="test"> I want this</div>`;
const el = htmlDoc.getElementById('parent');
el.innerHTML = html;
console.log(el);
// This is not working, but I want to do this
console.log(htmlDoc.getElementById('test'));
Alternatively, you can create an element belonging to the existing document and search it with querySelector (which can be called on an element) instead of document.
const html = `<div id="">I dont need this</div>
<div id="test"> I want this</div>`;
const el = document.createElement('div');
el.innerHTML = html;
console.log(el);
// This is not working, but I want to do this
console.log(el.querySelector('#test'));

Can someone explain why document.write does not write the body tag in this example?

Given the following HTML with a small JS snippet:
<html>
<head>
<title></title>
</head>
<body>
<script>
const iframeEl = document.createElement("iframe");
document.body.appendChild(iframeEl);
const htmlToWrite = "<html><head><title><\/title><script src=\"javascript:void(0)\" defer=\"\"><\/script><\/head><body><\/body><\/html>";
const iframeDocument = iframeEl.contentWindow.document;
iframeDocument.open();
iframeDocument.write(htmlToWrite);
iframeDocument.close();
const bodyEl = iframeDocument.querySelector("body");
console.log("Has body element", bodyEl != null);
if (bodyEl == null) {
console.log("Got", iframeDocument.documentElement.outerHTML);
}
</script>
</body>
</html>
if I remove the defer attribute from the script tag, document.write no longer writes the body tag. Can someone explain why this is happening?
I would expect that it writes out the rest of the document, even if the script src isn't loadable.

Output html element content in another innerHTML js

I want to create a dynamic URL. The domain part should remain the same. For example: In this URL "https://www.google.com/search?", I want to append content fetched from another website.
I use this js to fetch and store result to Html element "KK1"
<script>
'use strict';
(async () => {
let response = await fetch('https://api.com/values/1');
let text = await response.text(); // read response body as text
document.getElementById("KK1").innerHTML = (text.slice(0, 80) );
})()
</script>
Assuming the response received from fetch is amoeba1, I want to generate a URL like "https://www.google.com/search?amoeba1"
My code
<!DOCTYPE html>
<html>
<head><script>
'use strict';
(async () => {
let response = await fetch('https://api.exchangeratesapi.io/latest?symbols=USD,GBP');
let text = await response.text(); // read response body as text
document.getElementById("KK1").innerHTML = (text.slice(0, 80) );
})()
</script>
<title>My title Dowmloder.rar</title>
</head>
<body>
<h2>My First Web Page</h2>
<p>My First Paragraph.</p>
Generate clickable URL from parameters.
<p id="demo"></p>
<script>
document.getElementById("demo").innerHTML = "https://www.google.com/search?"+ "I ADD WHAT HERE TO??";
</script>
BELOW IS THE STRING I WANT TO APPEND.
<p id="KK1"></p>
</body>
</html>
How do I append the fetched string? The string is already in
<p id="KK1"></p>
Looking Forward to getting help.
You are looking for anchor tag & you want to add dynamic href:
<!DOCTYPE html>
<html>
<head><script>
'use strict';
var res = "";
(async () => {
let response = await fetch('https://api.exchangeratesapi.io/latest?symbols=USD,GBP');
let text = await response.text(); // read response body as text
document.getElementById("KK1").innerHTML = (text.slice(0, 80) );
document.getElementById("demo").innerHTML = "https://www.google.com/search?"+ text;
document.getElementById("demo").href = "https://www.google.com/search?"+ encodeURI(text);
})()
</script>
<title>My title Dowmloder.rar</title>
</head>
<body>
<h2>My First Web Page</h2>
<p>My First Paragraph.</p>
Generate clickable URL from parameters.
<a id="demo"></a><br>
BELOW IS THE STRING I WANT TO APPEND.
<p id="KK1"></p>
</body>
</html>
document.getElementById("demo").innerHTML = "https://www.google.com/search?" + document.getElementById("KK1").innerHTML
Maybe you can modify your script to store in a variable the text result from your API call then with template literals, using ${} you can build your new string for demo element.
Like the following
const resultText = text.slice(0, 80);
document.getElementById("KK1").innerHTML = resultText;
document.getElementById("demo").innerHTML = `https://www.google.com/search?${resultText}`;
Or there are other ways to concatenate a string like:
'https://www.google.com/search?' + resultText
Read further here: Template literals (Template strings)
Update:
So you need to merge those two <script> tags what you have.
I guess this will do that job for you:
'use strict';
(async () => {
let response = await fetch('https://api.exchangeratesapi.io/latest?symbols=USD,GBP');
let text = await response.text(); // read response body as text
// essential modified part:
const resultText = text.slice(0, 80);
document.getElementById("KK1").innerHTML = resultText;
document.getElementById("demo").innerHTML = `https://www.google.com/search?${resultText}`;
})();
And don't forget to remove this one:
<script>
document.getElementById("demo").innerHTML = "https://www.google.com/search?"+ "I ADD WHAT HERE TO??";
</script>
I hope that helps!

Is that possible to put Template7 code in a separate file rather than in html

I am using a framework called Framework7.
In my index.html, I have some Template7 code, like this format
<script type="text/template7" id="commentsTemplate">
{{#each this}}
<div> test this template 7 code </div>
</script>
However, I want to have this part of code into an another separated file (Just like I can have many other *.js files in, say, a static folder and refer to the file by "static/*.js).
I have tried to use a typical way to import js
<script type="text/template7" id="storiesTemplate" src="js/template.js"></script>
But it doesn't work, there is also no demo/sample code in the documentation.
Any help is appreciated!
You can do it. The idea behind is to include a HTML file in a HTML file. I can tell at least 3 ways that this can happen, but personally I fully validated only the third.
First there is a jQuery next sample is taken from this thread
a.html:
<html>
<head>
<script src="jquery.js"></script>
<script>
$(function(){
$("#includedContent").load("b.html");
});
</script>
</head>
<body>
<div id="includedContent"></div>
</body>
</html>
b.html:
<p> This is my include file </p>
Another solution, I found here and doesn't require jQuery but still it's not tested: there is a small function
My solution is a pure HTML5 and is probably not supported in the old browsers, but I don't care for them.
Add in the head of your html, link to your html with template
<link rel="import" href="html/templates/Hello.html">
Add your template code in Hello.html. Than use this utility function:
loadTemplate: function(templateName)
{
var link = document.querySelector('link[rel="import"][href="html/templates/' + templateName + '.html"]');
var content = link.import;
var script = content.querySelector('script').innerHTML || content.querySelector('script').innerText;
return script;
}
Finally, call the function where you need it:
var tpl = mobileUtils.loadTemplate('hello');
this.templates.compiledTpl = Template7.compile(tpl);
Now you have compiled template ready to be used.
=======UPDATE
After building my project for ios I found out that link import is not supported from all browsers yet and I failed to make it work on iphone. So I tried method number 2. It works but as you might see it makes get requests, which I didn't like. jquery load seems to have the same deficiency.
So I came out with method number 4.
<iframe id="iFrameId" src="html/templates/template1.html" style="display:none"></iframe>
and now my loadTemplate function is
loadTemplate: function(iframeId, id)
{
var iFrame = document.getElementById(iframeId);
if ( !iFrame || !iFrame.contentDocument ) {
console.log('missing iframe or iframe can not be retrieved ' + iframeId);
return "";
}
var el = iFrame.contentDocument.getElementById(id);
if ( !el ) {
console.log('iframe element can not be located ' + id );
return "";
}
return el.innerText || el.innerHTML;
}
How about lazy loading and inserting through the prescriptions?
(function (Template7) {
"use strict";
window.templater = new function(){
var cache = {};
var self = this;
this.load = function(url)
{
return new Promise(function(resolve,reject)
{
if(cache[url]){
resolve(cache[url]);
return true;
}
if(url in Template7.templates){
resolve(Template7.templates[url]);
return true;
}
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if(this.status == 200 && this.response.search('<!DOCTYPE html>') == -1){
cache[url] = Template7.compile(this.response);
resolve(cache[url]);
}else{
reject(`Template ${url} not found`);
}
};
xhr.send();
})
}
this.render = function(url, data)
{
return self.load(url)
.then(function(tpl){
return tpl(data) ;
});
}
this.getCache = function()
{
return cache;
}
}
})(Template7);
Using :
templater.render('tpl.html').then((res)=>{ //res string })
Or :
templater.load('tpl.html').then( tpl => { Dom7('.selector').html( tpl(data) ) } )
It is possible to define your templates in .js-files. The template just needs to be a string.
Refer to this [JSFiddle] (https://jsfiddle.net/timverwaal/hxetm9rc/) and note the difference between 'template1' and 'template2'
var template1 = $$('#template').html();
var template2 = '<p>Hello, my name is still {{firstName}} {{lastName}}</p>'
template1 just extracts the content of the <script> and puts it in a string.
template2 directly defines the string

Create a DOM document from string, without JQuery

We're looking for ways to create a DOM document in javascript from a string, but without using Jquery.
Is there a way to do so? [I would assume so, since Jquery can do it!]
For those curious, we can't use Jquery, becase we're doing this in the context of a Chrome application's content script, and using Jquery would just make our content script too heavy.
https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
var parser = new DOMParser();
var doc = parser.parseFromString("<html_string>", "text/html");
(the resulting doc variable is a documentFragment Object).
In case you're still looking for an anwer, and for anyone else coming accross it, I just have been trying to do the same thing myself. It seems you want to be looking at javascript's DOMImplementation:
http://reference.sitepoint.com/javascript/DOMImplementation
There are few references to compatibility as well here, but it's fairly well supported.
In essence, to create a new document to manipulate, you want to create a new Doctype object (if you're going to output some standards based stuff) and then create the new Document using the newly created Doctype variable.
There are multiple options to be put into both the doctype and the document, but if you're creating an HTML5 document, it seems you want to leave most of them as blank strings.
Example (New HTML5 DOM Document):
var doctype = document.implementation.createDocumentType( 'html', '', '');
var dom = document.implementation.createDocument('', 'html', doctype);
The new Document now looks like this:
<!DOCTYPE html>
<html>
</html>
Example (New XHTML DOM Document):
var doctype = document.implementation.createDocumentType(
'html',
'-//W3C//DTD XHTML 1.0 Strict//EN',
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
);
var dom = document.implementation.createDocument(
'http://www.w3.org/1999/xhtml',
'html',
doctype
);
So it's up to you to populate the rest of it. You could do this as simply as changing
dom.documentElement.innerHTML = '<head></head><body></body>';
Or go with the more rigorous:
var head = dom.createElement( 'head' );
var body = dom.createElement( 'body' );
dom.documentElement.appendChild(head);
dom.documentElement.appendChild(body);
All yours.
createDocumentFragment may help you.
https://developer.mozilla.org/En/DOM/DocumentFragment
Browsers always create document by themselves with empty page (about:blank).
Maybe, in Chrome application there're some functions available (like XUL in FF), but there's no such function in ordinary javascript.
Solution - works with all browsers since IE 4.0
var doc = (new DOMParser).parseFromString(htmlString, "text/html");
Or
var doc = document.implementation.createHTMLDocument();
1)Example: new DOMParser()
var htmlString = `<body><header class="text-1">Hello World</header><div id="table"><!--TABLE HERE--></div></body>`;
var insertTableString = `<table class="table"><thead><tr><th>th cell</th></tr></thead><tbody><tr><td>td cell</td></tr></tbody></table>`;
var doc = (new DOMParser).parseFromString(htmlString, "text/html");
doc.getElementById('table').insertAdjacentHTML('beforeend', tableString);
console.log(doc);
2)Example: createHTMLDocument()
var htmlString = `<body><header class="text-1">Hello World</header><div id="table"><!--TABLE HERE--></div></body>`;
var insertTableString = `<table class="table"><thead><tr><th>th cell</th></tr></thead><tbody><tr><td>td cell</td></tr></tbody></table>`;
var doc = document.implementation.createHTMLDocument();
doc.open();
doc.write(htmlString);
doc.getElementById('table').insertAdjacentHTML('beforeend', tableString);
doc.close();
console.log(doc);
I tried some of the other ways here but there where issues when creating script elements such as Chrome refusing to load the actual .js file pointed to by the src attribute. Below is what works best for me.
It's up to 3x faster than jQuery and 3.5x faster than using DOMParser, but 2x slower than programmatically creating the element.
https://www.measurethat.net/Benchmarks/Show/2149/0
Object.defineProperty(HTMLElement, 'From', {
enumerable: false,
value: (function (document) {
//https://www.measurethat.net/Benchmarks/Show/2149/0/element-creation-speed
var rgx = /(\S+)=(["'])(.*?)(?:\2)|(\w+)/g;
return function CreateElementFromHTML(html) {
html = html.trim();
var bodystart = html.indexOf('>') + 1, bodyend = html.lastIndexOf('<');
var elemStart = html.substr(0, bodystart);
var innerHTML = html.substr(bodystart, bodyend - bodystart);
rgx.lastIndex = 0;
var elem = document.createElement(rgx.exec(elemStart)[4]);
var match; while ((match = rgx.exec(elemStart))) {
if (match[1] === undefined) {
elem.setAttribute(match[4], "");
} else {
elem.setAttribute(match[1], match[3]);
}
}
elem.innerHTML = innerHTML;
return elem;
};
}(window.document))
});
Usage Examples:
HTMLElement.From(`<div id='elem with quotes' title='Here is "double quotes" in single quotes' data-alt="Here is 'single quotes' in double quotes"><span /></div>`);
HTMLElement.From(`<link id="reddit_css" type="text/css" rel="stylesheet" async href="https://localhost/.js/sites/reddit/zinject.reddit.css">`);
HTMLElement.From(`<script id="reddit_js" type="text/javascript" async defer src="https://localhost/.js/sites/reddit/zinject.reddit.js"></script>`);
HTMLElement.From(`<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">`);
HTMLElement.From(`<div id='Sidebar' class='sidebar' display=""><div class='sb-handle'></div><div class='sb-track'></div></div>`);
I was able to do this by writing the html string on an iframe
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
</body>
</html>`
const iframe = document.createElement('iframe')
iframe.contentDocument.open()
iframe.contentDocument.write(html)
iframe.contentDocument.close()
iframe.addEventListener('load', () => {
myDocumentObject = iframe.contentDocument
})
fetch("index.html", { // or any valid URL
method: "get"
}).then(function(e) {
return e.text().then(e => {
var t = document.implementation.createHTMLDocument("");
t.open();
t.write(e);
t.close();
return t;
});
}).then(e => {
// e will contain the document fetched and parsed.
console.log(e);
});
The DOM element has the property innerHTML that allows to change completely its contents. So you can create a container and fill it with a new HTML content.
function createElementFromStr(htmlContent) {
var wrapperElm = document.createElement("div");
wrapperElm.innerHTML = htmlContent; // Ex: "<p id='example'>HTML string</p>"
console.assert(wrapperElm.children.length == 1); //Only one child at first level.
return wrapperElm.children[0];
}
*
I know it is an old question, but i hope to help someone else.
var dom = '<html><head>....</head><body>...</body></html>';
document.write(dom);
document.close();
HTML would like this:
<html>
<head></head>
<body>
<div id="toolbar_wrapper"></div>
</body>
</html>
JS would look like this:
var data = '<div class="toolbar">'+
'<button type="button" class="new">New</button>'+
'<button type="button" class="upload">Upload</button>'+
'<button type="button" class="undo disabled">Undo</button>'+
'<button type="button" class="redo disabled">Redo</button>'+
'<button type="button" class="save disabled">Save</button>'+
'</div>';
document.getElementById("toolbar_wrapper").innerHTML = data;

Categories

Resources