Book Image

Cross-platform Desktop Application Development: Electron, Node, NW.js, and React

By : Dmitry Sheiko
Book Image

Cross-platform Desktop Application Development: Electron, Node, NW.js, and React

By: Dmitry Sheiko

Overview of this book

Building and maintaining cross-platform desktop applications with native languages isn’t a trivial task. Since it’s hard to simulate on a foreign platform, packaging and distribution can be quite platform-specific and testing cross-platform apps is pretty complicated.In such scenarios, web technologies such as HTML5 and JavaScript can be your lifesaver. HTML5 desktop applications can be distributed across different platforms (Window, MacOS, and Linux) without any modifications to the code. The book starts with a walk-through on building a simple file explorer from scratch powered by NW.JS. So you will practice the most exciting features of bleeding edge CSS and JavaScript. In addition you will learn to use the desktop environment integration API, source code protection, packaging, and auto-updating with NW.JS. As the second application you will build a chat-system example implemented with Electron and React. While developing the chat app, you will get Photonkit. Next, you will create a screen capturer with NW.JS, React, and Redux. Finally, you will examine an RSS-reader built with TypeScript, React, Redux, and Electron. Generic UI components will be reused from the React MDL library. By the end of the book, you will have built four desktop apps. You will have covered everything from planning, designing, and development to the enhancement, testing, and delivery of these apps.
Table of Contents (9 chapters)

An HTML prototype

We've just reached the point where we can start templating our application. Using HTML and CSS, we will achieve the intended look and feel. Later, we will bind JavaScript modules to the acting elements.

We start by replacing the content of index.html with the following code:

./index.html

<!DOCTYPE html> 
<html>
<head>
<title>File Explorer</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="./assets/css/app.css" rel="stylesheet" type="text/css">
</head>
<body class="l-app">
<header class="l-app__titlebar titlebar">
</header>
<div class="l-app__main l-main">
<aside class="l-main__dir-list dir-list">
</aside>
<main class="l-main__file-list file-list">
</main>
</div>
<footer class="l-app_footer footer">
</footer>
</body>
</html>

Here, we just defined the page layout with semantically meaningful HTML tags. As you can see, we refer to ./assets/css/app.css that we are about to create.

Maintainable CSS

Before we start styling, I would like to talk briefly about the importance of maintainability in CSS. Despite the fact that CSS is a declarative language, it requires no less diligence than any other code in general. When browsing a public repository, such as GitHub, you can still find plenty of projects where all the styles are put in a single file that is full of code smells (https://csswizardry.com/2012/11/code-smells-in-css/) and has no consistency in class naming.

Well, it will not be much of a problem at the beginning, but CSS as any other code tends to grow. Eventually, you will end up with thousands of lines of rotting code often written by different people.

Then, you have to fix the UI element appearance, but you realize that dozens of existing CSS declarations across the cascade impact this element. You change one, and styles break unpredictably on other elements. So, you will likely decide to add your own rules overriding existing styles. After that, you may find out that some of the existing rules have a higher specificity, and you will have to use brute force through the cascade; every time it is going to be worse.

To avoid this maintainability problem, we have to break the entire application UI into components and design the CSS code so as to keep them reusable, portable, and conflict free; the following heuristics may come in handy:

  • Split the whole CSS code into modules that represent components, layouts, and states
  • Always use classes for styling (not IDs or attributes)
  • Avoid qualified selectors (selectors with tags such as nav, ul, li, and h2)
  • Avoid location dependency (long selectors such as .foo, .bar, .baz, and article)
  • Keep selectors short
  • Do not use !important reactively

There are different methodologies that help to improve CSS maintainability. Probably, the most popular approach is Blocks Elements Modifiers (BEM). It introduces a surprisingly simple, but powerful concept (https://en.bem.info/methodology/key-concepts/). It describes a pattern for class names that encourages readability and portability. I believe that the best way to explain it is by an example. Let's say we have a component representing a blog post:

<article class="post"> 
<h2 class="post__title">Star Wars: The Last Jedi's red font is a
cause for concern/h2>
<time datetime="2017-01-23 06:00" class="post__time">Jan 23, 2017</time>
</article>

In BEM terminology, this markup represents a block that we can define with a class name post. The block has two elements--post__title and post_time. Elements are integral parts of a block; you cannot use them out of the parent block context.

Now imagine that we have to highlight one post of the list. So, we add a post--sponsored modifier to the block's classes:

<article class="post post--sponsored"> 
....
</article>

At first, class names containing double dashes and underscores may make you dizzy, but after a while you will get used to it. The BEM naming convention helps developers remarkably by showing indention. So when reading your own or somebody else's code, you can quickly figure out by its name what the purpose of a class is.

In addition to the BEM naming convention, we will use a few ideas from the Pragmatic CSS styleguide (https://github.com/dsheiko/pcss). We will give names prefixed with is- and has- to the classes representing global states (for example, is-hidden and has-error); we will prefix layout-related classes with l- (for example, l-app). Finally, we will amalgamate all CSS files in two folders (Component and Base).

Defining base rules

Firstly, we will create a Base directory and place the reset styles in there:

./assets/css/Base/base.css

html { 
-webkit-font-smoothing: antialiased;
}

* {
box-sizing: border-box;
}

nav > ul {
list-style: none;
padding: 0;
margin: 0;
}

body {
min-height: 100vh;
margin: 0;
font-family: Arial;
}

.is-hidden {
display: none !important;
}

For HTML scope, we will enable font smoothing for better font rendering.

Then, we will set box sizing of every element (*) in border-box. The default CSS box model is content-box, where width and height set to an element do not include padding and border. However, if we are setting, let's say, a sidebar width 250px, I would expect it to cover this length. With border-box, the box's size is always exactly what we set it, regardless of padding or border, but if you ask me, the border-box mode feels more natural.

We will reset indents and markers--for an unordered list--that are used for navigation (nav > ul). We make body element span the height of the entire viewport (min-height: 100vh), remove the default margin, and define the font family.

We will also introduce a global state is-hidden that can be applied on any element to remove it from the page flow. By the way, that is a good example of proactive and, therefore, permissible use of !important. By adding an is-hidden class (with JavaScript), we state that we want the element to hide, with no exceptions. Thus, we will never run into a specificity problem.

Defining layouts

That's enough for base styles; now, we will start on the layout. First, we will arrange the title bar, main section, and footer:

To achieve this design, we should preferably use Flexbox. If you are not familiar with this layout mode, I will recommend the article, Understanding Flexbox: Everything you need to know (http://bit.ly/2m3zmc1). It provides probably the most clear and easy-to-catch-up way of explaining what a Flexbox is, what options are available, and how to use them efficiently.

So, we can define the application layout like that:

./assets/css/Component/l-app.css

.l-app { 
display: flex;
flex-flow: column nowrap;
align-items: stretch;
}

.l-app__titlebar {
flex: 0 0 40px;
}

.l-app__main {
flex: 1 1 auto;
}

.l-app__footer {
flex: 0 0 40px;
}

We make .l-app a flex container that arranges inner items along a cross axis, vertically (flex-flow: column nowrap). In addition, we request the flex items to fill in the full height of the container (align-items: stretch). We set the title bar and footer to a fixed height always (flex: 0 0 40px). However, the main section may shrink and grow depending on the viewport size (flex: 1 1 auto).

Since we have an application layout, let's define the inner layout for the main section:

What we need to do is to make items--dir-list and file-list--to arrange horizontally one after another:

./assets/css/Component/l-main.css

.l-main { 
display: flex;
flex-flow: row nowrap;
align-items: stretch;
}

.l-main__dir-list {
flex: 0 0 250px;
}

.l-main__file-list {
flex: 1 1 auto;
}

In the preceding code, we set the flex items to line up along an main axis horizontally using flex-flow: row nowrap. The l-main__dir-list item has a fixed width and its width depends on the viewport.

Actually, it's hard to see any results of our work until we give the components some colors:

./assets/css/Component/titlebar.css

.titlebar { 
background-color: #2d2d2d;
color: #dcdcdc;
padding: 0.8em 0.6em;
}

We also colorise the footer component:
./assets/css/Component/footer.css

.footer { 
border-top: 1px solid #2d2d2d;
background-color: #dedede;
padding: 0.4em 0.6em;
}

and the file-list component:

./assets/css/Component/file-list.css

.file-list { 
background-color: #f9f9f9;
color: #333341;
}

and eventually the dir-list component:

./assets/css/Component/dir-list.css

.dir-list { 
background-color: #dedede;
color: #ffffff;
border-right: 1px solid #2d2d2d;
}

Now, we only need to include all the modules in the index file:

./assets/css/app.css:

@import url("./Base/base.css"); 
@import url("./Component/l-app.css");
@import url("./Component/titlebar.css");
@import url("./Component/footer.css");
@import url("./Component/dir-list.css");
@import url("./Component/file-list.css");

As it's done, we launch the application using the following command:

npm start

It launches the application and shows the layout:

For font sizes and related parameters such as padding, we use relative units (em). It means that we set these values relative to the parent font size:

.component { font-size: 10px; } .component__part { font-size: 1.6em; /* computed font-size is 10*1.6=16px */ }

This trick allows us to efficiently scale components. For example, when using the Responsive Web Design (RWD) approach, we may need to reduce the font sizes and spacing proportionally for a smaller viewport width. When using ems, we just change font size for a target component, and values of subordinated rules will adapt.

Defining CSS variables

NW.js releases quite frequently, basically updating with every new version of Chromium. That means we can safely use the latest CSS features. The one I'm most excited about is called Custom Properties (https://www.w3.org/TR/css-variables), which were formerly known as CSS variables.

Actually, variables are one of the main reasons CSS preprocessors exist. With NW.js, we can set variables natively in CSS, as follows:

--color-text: #8da3c5; 
--color-primary: #189ac4;

After that, we can use the variable instead of real values across all the modules in the document scope:

.post__title { 
color: var(--color-primary);
}
.post__content {
color: var(--color-text);
}

So if we decide now to change one of defined colors, we need to do it once, and any rules relying on the variable receives the new value. Let's adopt this technology for our application.

First, we need to create definitions for the module:

./assets/css/Base/defenitions.css

:root { 
--titlebar-bg-color: #2d2d2d;
--titlebar-fg-color: #dcdcdc;
--dirlist-bg-color: #dedede;
--dirlist-fg-color: #636363;
--filelist-bg-color: #f9f9f9;
--filelist-fg-color: #333341;
--dirlist-w: 250px;
--titlebar-h: 40px;
--footer-h: 40px;
--footer-bg-color: #dedede;
--separator-color: #2d2d2d;
}

Here, we define variables representing colors and fixed sizes in the root scope. This new file gets included to the CSS index file:

./assets/css/app.css:

@import url("./Base/defenitions.css"); 
...

Then, we have to modify our components. First we take care of the top level application layout:

./assets/css/Component/l-app.css

.l-app { 
display: flex;
flex-flow: column nowrap;
align-items: stretch;
}

.l-app__titlebar {
flex: 0 0 var(--titlebar-h);
}

.l-app__main {
flex: 1 1 auto;
}

.l-app_footer {
flex: 0 0 var(--footer-h);
}

Then we layout the main section that consists of two columns with dir and file lists:

./assets/css/Component/l-main.css

.l-main { 
display: flex;
flex-flow: row nowrap;
align-items: stretch;
}

.l-main__dir-list {
flex: 0 0 var(--dirlist-w);
}

.l-main__file-list {
flex: 1 1 auto;
}

We style the header:

./assets/css/Component/titlebar.css

.titlebar { 
background-color: var(--titlebar-bg-color);
color: var(--titlebar-fg-color);
padding: 0.8em 0.6em;
}

And the footer:

./assets/css/Component/footer.css

.footer { 
border-top: 1px solid var(--separator-color);
background-color: var(--footer-bg-color);
padding: 0.4em 0.6em;
}

We also need to set colors for the child components of the main section. So style the file list component:

./assets/css/Component/file-list.css

.file-list { 
background-color: var(--filelist-bg-color);
color: var(--filelist-fg-color);
}

and directory list component:

./assets/css/Component/dir-list.css

.dir-list { 
background-color: var(--dirlist-bg-color);
color: var(--dirlist-fg-color);
border-right: 1px solid var(--separator-color);
}

We can run the application to observe that it looks the same. All the colors and sizes are successfully extrapolated from the variables.

Sticking the title bar and header

The layout looks fine without any content, but what happens to the layout if it receives content that is too long?

In fact, we will have a header and footer shifting out of the view when scrolling. It doesn't look user-friendly. Fortunately, we can change it easily using another fresh addition to CSS called Sticky positioning (https://www.w3.org/TR/css-position-3/#sticky-pos).

All we need to do is to modify slightly the title bar component:

./assets/css/Component/titlebar.css

.titlebar { 
...
position: sticky;
top: 0;
}

and the footer:

./assets/css/Component/footer.css

.footer { 
...
position: sticky;
bottom: 0;
}

In the preceding code, we declared that the title bar will stick to the top and footer to the bottom. Run the application now, and you will note that both boxes are always visible, regardless of scrolling:

Styling the title bar

Speaking of the view content, we are ready to populate the layout slots. We will start with the title bar:

./index.html

<header class="l-app__titlebar titlebar"> 
<span class="titlebar__path">/home/sheiko/Sites/file-explorer</span>
<a class="titlebar__btn" >_</a>
<a class="titlebar__btn is-hidden" > </a>
<a class="titlebar__btn" ></a>
<a class="titlebar__btn" ></a>
</header>

Basically, we want the current path to be displayed on the left and window controls on the right. It can be achieved with Flexbox. It's a tiny layout that won't be reused, so it won't hurt if we mix it in the component module:

./assets/css/Component/titlebar.css

.titlebar { 
...
display: flex;
flex-flow: row nowrap;
align-items: stretch;
}
.titlebar__path {
flex: 1 1 auto;
}
.titlebar__btn {
flex: 0 0 25px;
cursor: pointer;
}

Styling the directory list

The directory list will be used for navigation through the file system, so we will wrap it with the nav > ul structure:

./index.html

<aside class="l-main__dir-list dir-list"> 
<nav>
<ul>
<li class="dir-list__li">..</li>
<li class="dir-list__li">assets</li>
<li class="dir-list__li">js</li>
<li class="dir-list__li">node_modules</li>
<li class="dir-list__li">tests</li></ul>
</nav>
</aside>

To support it with styles, we go with the following code:

./assets/css/Component/dir-list.css

.dir-list__li { 
padding: 0.8em 0.6em;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.dir-list__li:hover {
background-color: var(--dirlist-bg-hover-color);
color: var(--dirlist-fg-hover-color);
}

Note that we've just introduced a couple of variables. Let's add them in the definitions module:

./assets/css/Base/definitions.css

  --dirlist-bg-hover-color: #d64937; 
--dirlist-fg-hover-color: #ffffff;

As we ruin the application we can observe the new contents in the directory list:

Styling a file list

The file list will be represented as a table, but we will build it out of an unordered list. The./index.html file contains the following code:

<main class="l-main__file-list file-list"> 
<nav>
<ul>
<li class="file-list__li file-list__head">
<span class="file-list__li__name">Name</span>
<span class="file-list__li__size">Size</span>
<span class="file-list__li__time">Modified</span>
</li>
<li class="file-list__li">
<span class="file-list__li__name">index.html</span>
<span class="file-list__li__size">1.71 KB</span>
<span class="file-list__li__time">3/3/2017, 15:44:19</span>
</li>
<li class="file-list__li">
<span class="file-list__li__name">package.json</span>
<span class="file-list__li__size">539 B</span>
<span class="file-list__li__time">3/3/2017, 17:53:19</span>
</li>
</ul>
</nav>
</main>

In fact, here Grid Layout (https://www.w3.org/TR/css3-grid-layout/) would probably suit better; however, at the time of writing, this CSS module was not yet available in NW.js. So, we go on again with Flexbox:

./assets/css/Component/file-list.css

.file-list { 
background-color: var(--filelist-bg-color);
color: var(--filelist-fg-color);
cursor: pointer;
}

.file-list__li {
display: flex;
flex-flow: row nowrap;
}

.file-list__li:not(.file-list__head){
cursor: pointer;
}
.file-list__li:not(.file-list__head):hover {
color: var(--filelist-fg-hover-color);
}
.file-list__li > * {
flex: 1 1 auto;
padding: 0.8em 0.8em;
overflow: hidden;
}

.file-list__li__name {
white-space: nowrap;
text-overflow: ellipsis;
width: 50%;
}
.file-list__li__time {
width: 35%;
}
.file-list__li__size {
width: 15%;
}

I believe that everything is clear with the preceding code, except that you might not be familiar with the pseudo-class :not(). I want to change the color and mouse cursor on hover for all the file list items, except the table header. So, I achieve it with a selector that can be read like any .file-list__li that is not .file-list__head.

The following assignment goes to the definitions file:

./assets/css/Base/definitions.css

--filelist-fg-hover-color: #d64937; 

As we run the application we can see the table with the file list:

Styling the footer

Eventually, we now reached the footer:

./index.html

... 
<footer class="l-app__footer footer">
<h2 class="footer__header">File Explorer</h2>
<select class="footer__select">
<option value="en-US">English</option>
<option value="de-DE">Deutsch</option>
</select>
</footer>

We arrange the application title to the left and language selector to the right. What do we use to lay this out? Obviously, Flexbox:

./assets/css/Component/footer.css

.footer { 
...
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
}

.footer__header {
margin: 0.2em auto 0 0;
font-size: 1em;
}

It's a special case. We set items to align right in general, but have reset it for the .footer__header item that snuggles against the left border driven by margin-right: auto:

While looking at the result, I think it would be nice to emphasize the functional meaning of some UI elements with icons. I personally prefer the icon font of Material Design system (https://material.io/icons/). So, as described in the Developer Guide (http://google.github.io/material-design-icons/), we include the corresponding Google Web Font to index.html:
./index.html

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" 
rel="stylesheet">

I would suggest that you dedicate a component that will represent an icon and fill it with the rule set suggested by Material Design:

./assets/css/Component/icon.css

.icon { 
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 16px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}

Now, we can add an icon anywhere in HTML, as simple as that:

<i class="material-icons">thumb_up</i> 

Why not then make a folder icon accompanying items in the directory list?:

<li class="dir-list__li"><i class="icon">folder</i>assets</li> 

I believe that a globe icon will get along nicely with the language selector. So we modify the HTML:

./index.html

... 
<footer class="l-app__footer footer">
<h2 class="footer__header">File Explorer</h2>
<label class="icon footer__label">language</label>
....

and we add a class in the CSS:

./assets/css/Component/footer.css

... 
.footer__label {
margin-right: 0.2em;
font-size: 1.4em;
margin-top: 0.1em;
}

As we run the application we can see an icon rendered next to the language selector control:

If something went wrong after running the application, you can always call for Developer Tools--just press F12: