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! 👋
Top comments (0)