The Electron Saga 3️⃣: All beginings have an end.
Jace
Posted on March 23, 2023
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>
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>
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);
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>
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>
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");
}
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];
};
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);
});
});
}
}
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! 👋
Posted on March 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.