Hi all, just wanting to share a bit about some experiments I've been doing.
Here are some buzzwords for you
- Web Components
- JavaScript Modules
- Dynamic Import
import(...)
- DOM shadowRoot
Enough to make anyone break down in tears! Or maybe not.
Below is some code - around 100 lines or so (and only 1 line of HTML and a few of your own js, the rest is the component file that I've done for you) - that creates a new web component capable of dynamically inserting another html or json file content into your web page!
So this is part of a series of experiments where I'm trying to learn the skills to be able to guide people on how to create dynamic, data-driven, configuration-first web UI's - without ANY external dependencies. Hopefully you can imagine how this fits in with the principles behind uibuilder.
Enjoy and please provide feedback.
If you have got uibuilder installed, create a new uibuilder node using the "blank" template.
Add this to your index.html: <html-include id="inc1" src="./test1.html">This is in the slot</html-include>
And this to your index.js:
// This dynamically loads the file from the Node-RED ExpressJS web server
const HtmlInclude = import('./html-include.js')
// Get a reference to a specific html-include tag
const inc1 = document.getElementById('inc1')
uibuilder.start()
uibuilder.onChange('msg', function (msg) {
if ( msg.topic === 'html-include') {
// Here we change the html file that is loaded - watch the web page change!
inc1.setAttribute('src', msg.payload)
}
})
Now create a new file in the same folder as your index.html file - html-include.js
/** Web component to load HTML dynamically
* See https://github.com/justinfagnani/html-include-element for inspiration
*/
const template = document.createElement('template')
template.innerHTML = `
<slot></slot>
<inner-load></inner-load>
`
export class HtmlInclude extends HTMLElement {
constructor() {
super()
this.type = 'text'
this.text = ''
this.json = {}
this.attachShadow({ mode: 'open', delegatesFocus: true })
.appendChild(template.content.cloneNode(true))
}
// component attributes
static get observedAttributes() {
return ['src']
}
/** The URL to fetch an HTML document from.
* Setting this property causes a fetch the HTML from the URL.
* We are reflecting the src attrib and the src prop.
*/
get src() {
return this.getAttribute('src');
}
set src(value) {
this.setAttribute('src', value);
}
// attribute change
async attributeChangedCallback(property, oldValue, newValue) {
if (oldValue === newValue) return
if (property === 'src') {
const response = await fetch(newValue)
if (!response.ok) {
throw new Error(`html-include fetch failed: ${response.statusText}`);
}
const contentType = response.headers.get('content-type')
if (contentType) {
if (contentType.includes('text/html')) {
this.type = 'html'
} else if (contentType.includes('application/json')) {
this.type = 'json'
} else if (contentType.includes('multipart/form-data')) {
this.type = 'form'
}
}
// Could add other binary types here. https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#body
switch (this.type) {
case 'html': {
this.text = await response.text()
const parser = new DOMParser()
const newDoc = parser.parseFromString( this.text, 'text/html' )
this.shadowRoot.removeChild(this.shadowRoot.lastElementChild)
this.shadowRoot.appendChild(newDoc.body)
break;
}
case 'json': {
this.json = await response.json()
this.text = JSON.stringify(this.json, null, 4)
this.shadowRoot.removeChild(this.shadowRoot.lastElementChild)
const myHtml = document.createElement('pre')
myHtml.textContent = this.text
this.shadowRoot.appendChild( myHtml )
break;
}
case 'form': {
this.json = await response.formData()
this.text = JSON.stringify(this.json)
this.shadowRoot.removeChild(this.shadowRoot.lastElementChild)
this.shadowRoot.append(this.text)
break;
}
default: {
this.text = await response.text()
this.shadowRoot.append(this.text)
break;
}
}
}
}
}
customElements.define('html-include', HtmlInclude)
Finally, all you need is a very simple couple of html files test1.html
and test2.html
. You will see from the code you are putting into index.html
that the first of those is manually specified and the content of the file will be inserted to your web page on load.
So now create a simple flow using uibuilder and send a message: {"topic": "html-include", "payload": "./test2.html"}
.
The content from the test1.html file will be replaced by the new file dynamically.
Oh, and then try creating a test.json
file and inserting that to see what the difference is.
Also try adding console.log('json> ', inc1.json)
after inc1.setAttribute('src', msg.payload)
in your index.js file and see what happens when you load the json file. Hint: you can access the internal property that has parsed the json back into an object.