The Electron Saga 3️⃣: All beginings have an end.

scriptjayt

Jace

Posted on March 23, 2023

The Electron Saga 3️⃣: All beginings have an end.

And we are back for our latest part of the Electron Sage. As this series is more about the setup I will only go so far, as building the app itself is not really linked to the workings of, and working with Electron.

Frameworks?

When I first started with Electron, I did not think about using a JavaScript framework; most of the time I use PHP for my web development, so it did not come to mind that such a framework could be useful.

The app I wanted to build would be able to do a bit more than just add items to a list I could edit. It needed to be expandable; so I build my first page with a nice sidebar and a main... and came to the realisation that I would need to copy the sidebar each time I would add a new page.
When using PHP, you would build a page index.php, which would contain the sidebar and an empty main that would be populated with a require- or an include function. What file you would include would depend on the current location (the URI).

<html lang="en">

<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">

   <title>My Site</title>
</head>

<body>
    <header>
        <h1>Hello World</h1>
    </header>

    <aside>
        <nav>
            <ul>
                <li>navitem</li>
                <li>navitem</li>
                <li>navitem</li>
                <li>navitem</li>
            </ul>
        </nav>
    </aside>

    <main>
        <?include 'path_of_file'?>
    </main>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

But no PHP, so no HTML-include.
I shrugged and instead of looking for a NPM package that could help me, or to look into a framework like Svelte; I decided to try importing with vanilla JavaScript. It worked, kind of.

Layout

First of; creating a new folder classes in /front/logic and a new file app.js.
Then linking the app.js file as a module.

/front/index.html

<head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>My App</title>
    <link rel="stylesheet" href="styles/reset.css">
    <link rel="stylesheet" href="styles/main.css">

    <!-- right here  -->
    <script type="module" src="logic/app.js"></script>
</head>
Enter fullscreen mode Exit fullscreen mode

Great, check if the connection is working by logging the values from last time:

/front/logic/app.js

console.log(E_system.mode);
console.log(E_system.platform);
Enter fullscreen mode Exit fullscreen mode

No breakage? Great, let's see, how do we want it to work?
Well, when I click a button in the nav; the content should be switched out.
I would like to be able to include content in a header, main and footer separately as to not break my main layout; so let's build up a nav and include some slots in our body

/front/index.html

<body>
    <nav id="feature-nav" class="c-wrapper">
        <ul>
            <li>
                <button sheet="home">
                    <span>Home</span>
                </button>
            </li>
            <li>
                <button sheet="todo">
                    <span>Todo</span>
                </button>
            </li>
            <li>
                <button sheet="tracker">
                    <span>Schema</span>
                </button>
            </li>
        </ul>
    </nav>
    <div id="app-interface">
        <header>
            <content-slot></content-slot>
        </header>
        <main>
            <content-slot></content-slot>
        </main>
        <footer>
            <content-slot></content-slot>
        </footer>
    </div>
</body>
Enter fullscreen mode Exit fullscreen mode

You can see I added an attribute I called sheet to all buttons. I also created a folder /front/sheets and added some HTML-files with names that correspond with those attribute values.

It is certainly possible to add aria-labels like aria-current and aria-controls to the buttons; or add a skip-to-content button above the ul, but those features are a bit out of scope for this checking-out series.

I want the sheet-files to be plain old HTML-file; I want three containers that I can target with the JS dom-api and just write content as I would normally.

/front/sheets/home.html

<header-content>
    <h1>Home</h1>
</header-content>

<main-content>
    my content
</main-content>

<footer-content>
    my footer
</footer-content>
Enter fullscreen mode Exit fullscreen mode

JS

The class is called page_loader, and it just gets imported and called. The is_ready method checks to see if our index.html has all the required elements to work; and the activate() method enables the button click.

import { D$ } from "./fn/dev.js";
import { page_loader } from "./classes/loader.js";

console.log(E_system.mode);
console.log(E_system.platform);

//? Load Pager
{
    const page = new page_loader();
    if (page.is_ready()) {
        D$(console.log, "Page Loader Ready");
        page.activate();
    } else D$(console.warn, "Page Loader not Ready");
}
Enter fullscreen mode Exit fullscreen mode

It works by using the fetch-api and a DOMParser object.
When a button is clicked, the class checks which sheet it links to and fetches the file with, well, Fetch.
A plain text-string is returned; which is passed on to a DOMParser. This JS-object is able to transform our plain string back to HTML.
Then we use the good old DOM-api to query our content-blocks
(<header-content> <main-content> <footer-content>)
At last it's just a matter of removing the old content from our index.html and pasting in the new content to have our final result.

Helper functions;

I had made some helper functions and saved them in the files /front/logic/fn/dev.js and /front/logic/fn/dom.js.

$D()
: Is a function that fires the passed through function only when the app is in development. It uses the is_dev variable we bridged over before.

$S()
: An abbreviation for querySelector

$SA()
: An abbreviation for querySelectorAll

page_loader class

First; some more helper functions; these do most of the work; they fetch and convert data.
And we import the previously mentioned helpers as well.

/front/logic/classes/loader.js

import { D$ } from "../fn/dev.js";
import { S$, SA$ } from "../fn/dom.js";

/**
 * @description get the sheet-attribute off of buttons
 * @param {HTMLButtonElement} _btn
 * @returns {string} - Sheet
 */
const get_sheet = (_btn) => {
    return _btn.getAttribute("sheet") || "";
};

/**
 * @description fetch sheet and return its data
 * @param {string} _sheet
 * @returns {response} - string
 */
async function find_sheet(_sheet) {
    const path = `./sheets/${_sheet}.html`;
    const F = await fetch(path, {
        method: "GET",
    })
        .then((response) => {
            // D$(console.log, response);
            return response.text();
        })
        .then((data) => {
            return data;
        })
        .catch((error) => {
            D$(console.log, error);
            return "";
        });
    return F;
}

/**
 * @description convert string to html of header-main-footer
 * @param {*} _html
 * @returns {array} - object of header_data, main_data, footer_data
 */
const data_to_html = (_html) => {
    var parser = new DOMParser();
    var doc = parser.parseFromString(_html, "text/html");
    const header_data = S$("header-content", doc)?.innerHTML.trim() || "";
    const main_data = S$("main-content", doc)?.innerHTML.trim() || "";
    const footer_data = S$("footer-content", doc)?.innerHTML.trim() || "";
    return [header_data, main_data, footer_data];
};
Enter fullscreen mode Exit fullscreen mode

And the main class; it does little more than querying all nav-buttons with the sheets attribute and the <content-slot> tags and attaches a click-event to those nav-buttons.
There are more functions you could add to the class; for example, automatically load the home.html sheet on startup. Or the sheet last used.
And some navigation functionality like showing what the current sheet is.

export class page_loader {
    #header_slot;
    #main_slot;
    #footer_slot;

    #all_nav_btns;

    constructor() {
        this.#get_targets();
        this.#get_nav();
    }

    // query native elements
    #get_targets() {
        this.#header_slot = S$("#app-header content-slot");
        this.#main_slot = S$("#app-content content-slot");
        this.#footer_slot = S$("#app-footer content-slot");
    }
    #get_nav() {
        this.#all_nav_btns = SA$("#feature-nav [sheet]");
    }

    /**
     * Fetch given sheet and render the data in the app
     * @param {string} _sheet - sheet of data to fetch
     * @param {HTMLButtonElement} _btn - button to turn on in the nav
     * @returns {void}
     */
    async #render(_sheet, _btn) {
        const data = await find_sheet(_sheet);
        if (!data) return;
        const [header_data, main_data, footer_data] = data_to_html(data);

        this.#header_slot.innerHTML = header_data;
        this.#main_slot.innerHTML = main_data;
        this.#footer_slot.innerHTML = footer_data;
    }

    /**
     * @description checks to see if the loader has succesfully retrieved all nav btns and the content slots
     * @returns {boolean}
     */
    is_ready() {
        if (
            this.#all_nav_btns &&
            this.#header_slot &&
            this.#main_slot &&
            this.#footer_slot
        )
            return true;
        return false;
    }

    /**
     * @description function to activate click events on nav-btns
     */
    activate() {
        this.#all_nav_btns.forEach((btn) => {
            btn.addEventListener("click", (e) => {
                const sheet = get_sheet(btn);
                this.#render(sheet, btn);
            });
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

It works but...

What I noticed when using this class is, that when I write JavaScript in a <script> tags in a sheet.html; it does not get triggered in the index.html; which does not mean it is safe to allow any scripts to infiltrate those files though. My guess is that the script gets run when the fetch-request reads through the file and not when the content gets 'pasted' into our index.html.

But what if you want to import some JS anyway? Well; I noticed that custom-elements get rendered properly, (if they are defined in our app.js file); so you could use a custom-element to autoload JS code.

Notice: I can't really speak about the security of this method; I would say not too bad, but if those HTML files got corrupted anyhow, the class would have no way of knowing, never mind handeling.

Conclusion: use a framework 😉

Series Conclusion

In these 4 articles, I set to lay out my first steps into starting with Electron and desktop-app making. I must say I was surprised how little extra setup it is compared to web development, for this example at least. I assume that if I where to dive deeper into this world, it would require some more setup.
I found that it is certainly possible to work with Electron without a framework (I fully build my app before realizing I could have used one); but it would have improved my dev-experience a bit.
I like how close it is to web-development I'm used to, and working with it feels quite nice, but I can't really compare how it holds up against other app-dev frameworks.
However, the unsolvable errors at the start of my journey should be mentioned (The Electron Saga 0). They did cause a bit of confusion and worry.

Overall, it was fun mocking around this new environment, away from the projects lurking over my shoulder.
Speaking of which, I should return to those. I hope you got something out of this series.
See ya! 👋

💖 💪 🙅 🚩
scriptjayt
Jace

Posted on March 23, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related