Do we even need UI frameworks any more?!

Hi all, just wanting to share a bit about some experiments I've been doing.

Here are some buzzwords for you :grinning:

  • 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. :exploding_head:

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.

5 Likes

It's interesting, but it doesn't appear easier than just using the dashboard. I use the rpi/node-red combo for some non-critical automation and controls scenarios. The integration of sensors, circuits and devices as well as the publishing of data to other systems are my main objectives.

Pardon me for being dense, but even this example screams to me, "I think I'll just put a ui node on the dashboard to display that thing or value that I was after." It seems beyond my pay grade. (I'm self-employed... don't really have a pay grade per se. :slight_smile: )

All of that said, I'm going to follow your series of examples to see if some of what your are proposing is easier than what I'm already doing. Thanks for taking the time to share it with us and explain it.

Thanks for the reply and of course, looked at in detail, this in no way comes close to the simplicity of Dashboard. But the same is true when you look at the detail of the Dashboard - in fact, I'd say (unless you are an Angular framework specialist), it is worse.

What I'm looking at at the moment is what can be done with pure HTML, CSS and JavaScript, totally standards-based with no framework dependencies. In fact, the approach allows the use of a framework as well if you want.

What this is doing is building up the components that might enable something that is simple to use and simple to develop components for. All without tying you into a framework that might have a limited lifespan.

Or it might just be some fun :rofl: :rofl: We'll have to see.

But I still found it fascinating that I could write a few lines of code that allows dynamic loading of another page into the current one. It shows the level of flexibility you get from modern HMTL and JavaScript.

2 Likes

I agree this (No frameworks) has its place. I spent a day getting windowsXP installed in a virtual box so I could install Microsoft explorer 6 so I could load a UI for a outdated network device.

It can be an issue when things are built upon stuff the becomes abandon ware.