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

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

By Dmitry Sheiko
books-svg-icon Book
eBook $39.99 $27.98
Print $48.99
Subscription $15.99 $10 p/m for three months
$10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
BUY NOW $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
eBook $39.99 $27.98
Print $48.99
Subscription $15.99 $10 p/m for three months
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
  1. Free Chapter
    Creating a File Explorer with NW.js-Planning, Designing, and Development
About 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.
Publication date:
July 2017
Publisher
Packt
Pages
300
ISBN
9781788295697

 

Creating a File Explorer with NW.js-Planning, Designing, and Development

Nowadays, when speaking of HTML5 desktop application development, one implies either NW.js or Electron. The first one has a shorter learning curve, which makes it a better choice for the beginning. Our first application will be a File Explorer. This sort of software is traditionally considered as a classical desktop application. I believe that you will find it exciting to build a File Explorer with HTML, CSS, and JavaScript. This chapter requires no skills in JavaScript frameworks, as we will use none. All you need is a basic knowledge of HTML, CSS, and plain JavaScript (including Node.js).

So, what are we up to? We will plan and sketch the project. We will set up the development environment and create a static prototype and run it with NW.js. We will implement the basic functionality, making it ready to be enhanced in Chapter 2, Creating a File Explorer with NW.js Enhancement and Delivery.

 

The application blueprint

By File Explorer, I mean a small program that allows navigating through the filesystem and performs basic operations on the files, which can be expressed with the following user stories:

  • As a user, I can see the content of the current directory
  • As a user, I can navigate through the filesystem
  • As a user, I can open a file in the default associated program
  • As a user, I can delete a file
  • As a user, I can copy a file in the clipboard and paste it later in a new location
  • As a user, I can open the folder containing the file with the system file manager
  • As a user, I can close the application window
  • As a user, I can minimize the application window
  • As a user, I can maximize and restore the application window
  • As a user, I can change the application language

It would be easier to perceive it in a visual form, wouldn't it? Wireframes come in handy here. Wireframe is a skeletal framework of the application that depicts the arrangement of the application's content, including UI elements and navigation system. Wireframe has no real graphics, typography, or even colors. It shows schematically, what the application does. As you know, drawing with a pencil on a paper is possible, but it is not the best way to create a wireframe; what we need is a prototyping tool. Today, there are plenty of solutions on the market. Here, I use an impressive, but affordable tool called WireframeSketcher (http://wireframesketcher.com/). It allows you to sketch web, desktop, and mobile applications (just what we need). It also has a rich mock-up gallery of stencils, widgets, icons, and templates that makes prototyping fast and easy. Besides, the wireframes look nice in a sketchy style:

What we can see on the wireframe is often called a Holy Grail Layout. In our case, the header serves as the window title bar. There, we keep our controls for window actions such as close, maximize, and minimize. Besides that, in the title bar, we display the path to the current directory. In the sidebar, we have our filesystem navigation. The main section contains a table that represents files of the current directory. It has columns--Name, Size, and Modified. A right-click on a file opens a context menu with available file operations. The footer comprises the application title and a language selector combo box.

 

Setting up an NW.js project

NW.js is an open source framework for building HTML, CSS, and JavaScript applications. You can also see it as a headless browser (based on Chromium https://www.chromium.org/) that includes Node.js runtime and provides desktop environment integration API. Actually, the framework is very easy to start with. What we need is just a start page HTML file and project manifest file (package.json).

To see it in action, we will create a project folder named file-explorer at an arbitrary location. The choice of the folder location is up to you, but I personally prefer to keep web projects in /<username>/Sites on Linux/macOS and %USERPROFILE%Sites on Windows.

As we enter the directory, we create placeholder folders for JavaScript and CSS sources (js and assets/css):

We also place a start page HTML (index.html) that consists of just a few lines:

./index.html
<!DOCTYPE html>
<html>
<body>
<h1>File Explorer</h1>
</body>
</html>

As you can guess, we shall see just this text--File Explorer-- when feeding this file to a browser.

Now, we need the Node.js manifest file (package.json). Node.js, embedded in the framework, will use it to resolve dependency package names when called with a require function or from an npm script. In addition, NW.js takes from it the project configuration data.

Why not create the manifest file and populate it with dependencies using the npm tool?

 

Node Package Manager

Nowadays, Node Package Manager (npm) is one of the most demanded gadgets in the web developer tool belt. It's a command-line utility connected with the corresponding online repository of packages and is capable of package installation, version management, and dependency management. So, when we need a package (library, framework, and module), we will check whether it's available in the npm repository and run npm to bring it into our project. It not only downloads the package, it also resolves its dependencies and does it pretty smartly. Furthermore, npm is pretty handy as an automation tool. We can set various command-line tasks to refer any of the locally installed packages by name. The npm tool will find the executable package among installed packages and run it.

The npm tool is distributed together with Node.js. So, you can find an installer for Windows or for macOS on the Node.js download page (https://nodejs.org/en/download). It is also available as an APT package, so you can install it for Linux with the apt-get tools:

sudo apt-get install npm

If you have already installed npm, ensure that it's up to date:

sudo npm install npm@latest -g

As I have already said, we can install packages with npm-- for example, NW.js. If we want to do it globally, we will run the following command:

sudo npm install nw --global

Alternatively, we can run the following command:

sudo npm i nw -g

This will download the latest build of NW.js in {prefix}/lib/node_modules/ and place the executable file in {prefix}/bin. It adds the binary to the PATH environment variable, so one can call nw in any location in the shell.

{prefix} In order to find out what {prefix} is one can run:
npm config get prefix. On Linux/macOS it will be /usr/local. On Windows %APPDATA%npm

This way, we will have a single instance of NW.js across the system, but what if an application requires a specific version of NW.js? Luckily, with npm, we can also install a package locally, and therefore, rely on a particular version that addresses our application. In addition, we can manage local dependencies in the package.json file. With a single command, npm can install/update all the dependencies enlisted there at once.

Let's take a look at how it works on our project. We go to the project root (the file-explorer folder) and run the following command:

npm init -y 

It produces a package.json file with the following content:

{ 
"name": "file-explorer",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Here, in the name field, we set our application name. Beware that NW.js will use the provided value to name the directory in a system-dependent path for the project persistent data (nw.App.dataPath). So, it shall be a unique, lowercase alpha-numeric, but may include a few special symbols, such as ., _, and -.

Field version expects the application version as a string, conforming to the Semantic Versioning standard (http://semver.org/). What it all boils down to is a composite product version out of three numbers separated with dots. The first number (MAJOR) increments when we make incompatible API changes, the second number (MINOR) increases when we introduce a new functionality, and the last one (PATCH) identifies bug fixes.

In the main field, we let NW.js know where to find our start page HTML. We have to edit the manifest to change its value with index.html:
./package.json

{ 
...
"main": "index.html",
...
}

The field scripts accepts a key value object with automation scripts for the project. By default, it has a placeholder for tests. Now, run the following command:

npm run test

The Shell responds with an error message saying no test specified, as we have no test yet. However, we will need a script to start the application. So, we edit package.json again and add to scripts field the following lines:

package.json

{ 
...
"scripts": {
"start": "nw .",
"test": "echo "Error: no test specified" && exit 1"
},

...
}

Now, we can type npm run start or npm start to run NW.js on the project root, but we do not have the framework installed, yet. We are just about to bring it in.

Manifest fields--such as description/keywords and author--help other people to discover the application as a package. The license field tells people how they are permitted to use the package. You can find more about these fields and other possible options at https://docs.npmjs.com/files/package.json.

Before telling npm to install the framework, we note that the standard version of NW.js doesn't include DevTools, which we definitely will need for development. So, we look for a specific version, the so-called SDK flavor. To find out the package versions that are available for the NW.JS package (nw), we run the following command:

npm view nw dist-tags

Alternatively, we can run the following command:

npm v nw dist-tags

This receives the following output:

{
latest: '0.20.3',

alphasdk: '0.13.0-alpha4sdk',
alpha5sdk: '0.13.0-alpha5sdk',
alpha6sdk: '0.13.0-alpha6sdk',
alpha7sdk: '0.13.0-alpha7sdk',
sdk: '0.20.3-sdk'
}

From this payload, we can assume that the latest version at the time of writing is 0.20.3 and that it is accompanied with 0.20.3-sdk. So, we can install the framework, as follows:

npm install nw@0.20.3-sdk --save-dev

Alternatively,we can install it, as follows:

npm i nw@0.20.3-sdk -D

Actually, since we know that the package has a dist-tag called sdk, we can also do it as follows:

npm i nw@sdk -D

Just after running any of these commands, we can find a new subdirectory named node_modules. There, npm installs local dependencies.

Have you noticed that we applied the --save-dev (-D) option? This way, we requested npm to save the package in our development dependency list. Observe that package.json is changed:

{ 
"name": "file-explorer",
"version": "1.0.0",
"description": "",
"main": "index.html",
"scripts": {
"start": "nw .",
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"nw": "^0.20.3-sdk"
}
}

We installed the package as a development dependency because this SDK version is meant only for development. In Chapter 2, Creating a File Explorer with NW.js–Enhancement and Delivery we will examine the distribution and packaging techniques. So, you will see how we can bundle the application with a platform-specific NW.js production build.

Since we have reflected our dependency in the manifest file, we can update this and any further packages any time by running the following command:

npm update

If we lose node_modules(for example after cloning the project from remote GIT repository given the dependency folder is usually in the ignore list), we can install all the dependencies through the following command:

npm i

Have you noticed? In the package.json, we assigned nw package with version in, so called, caret range ^0.20.3-sdk. That means during the install/update process, npm will accept new versions with patch and minor updates, but no major versions.

The following are some useful npm commands:
npm i pkg-name: Installs the latest available version of a package
npm i pkg-name@version: Installs a concrete version of the package
npm i pkg-name -S: Installs package as a dependency and saves it in package.json
npm i pkg-name -D: Installs package as a development dependency and save in package.json
npm i: Installs all the dependencies (including development ones) enlisted in package.json
npm i --production: Installs dependencies but not development ones
npm list: Shows all the installed dependencies
npm uninstall nw --save: uninstalls a package and removes it from
npm un nw -S: shorter syntax
package.json

At this point, we have the framework instance and package.json pointing to index.html. So, we can run the only script we have defined in the manifest file so far:

 npm start 

First, run it on NW.JS in Ubuntu:

Then, run it on NW.JS in windows:

Finally, we run it in macOS:

NW.js created a window and rendered index.html in it. It took the default Window parameters. If we want to customize them, we will need to edit package.json.

First, we will add the window field that accepts an object with the following properties:

  • window.icon: This specifies a relative path to the window icon.
  • window.show: This indicates whether the window is visible when the application starts or not. For instance, you can set it to false in the manifest and then change it programmatically with JavaScript (nw.Window.get().show( true )).
  • window.frame: This makes the window frameless when set to false.
  • window.width / window.height: This sets the window default size in pixels.
  • window.min_width / window.min_height: This sets a minimal acceptable size to the window.
  • window.position: This specifies where the window shall be placed. The value can be null, center, or mouse.
  • window.resizable: When set to true, this property makes the window resizable.

We will also use the chromium-args field to specify the command-line arguments that we want to pass to chromium. Here, we set it to --mixed-context to switch NW.js into the corresponding mode. So, we could access the browser and the NW.js API directly from Node.js modules. NW.js introduces Node.js context in addition to the browser context and keep them separate. After extending it with NWJS meta-data the manifest looks as follows:
./package.json

{ 
...
"chromium-args": "--mixed-context",
"window": {
"show": true,
"frame": true,
"width": 1000,
"height": 600,
"min_width": 800,
"min_height": 400,
"position": "center",
"resizable": true
}
}

These are just a few preferences set for our simple application. All the available options can be found at https://github.com/nwjs/nw.js/wiki/manifest-format.

 

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:

 

Fulfilling the functional requirements

We've described the semantic structure of our application with HTML. We have defined with CSS how our UI elements shall look. Now, we will teach our application to retrieve and update the content as well as to respond to user events. Actually, we will allocate the following tasks to several modules:

  • DirService: This provides control on directory navigation
  • FileService: This handles file operations
  • FileListView: This updates the file list with the data received from DirService, handles user events (open file, delete file, and so on) using FileService
  • DirListView: This updates the directory list with the data received from DirService and handles navigation events using DirService
  • TitleBarPath: This updates the current location with the path received from DirService
  • TitleBarActions: This handles user iteration with title bar buttons
  • LangSelector: This handles user iteration with language selector

However, before we start coding, let's see what we have in our arsenal.

NW.js gets distributed together with the latest stable version of Node.js, which has a great support for ES2015/ES2016 (http://node.green). It means that we can use any of the inherent new JavaScript features, but modules (http://bit.ly/2moblwB). Node.js has its own CommonJS-compliant module loading system. When we request a module by path, for example, require( "./foo" ), the runtime searches for a corresponding file (foo.js, foo.json, or foo.node) or a directory (./foo/index.js). Then, Node.js evaluates the module code and returns the exported type.

For example, we can create a module that exports a string:

./foo.js

console.log( "foo runs" ); 
exports.message = "foo's export";

and another one, which imports from the first module:

./bar.js

const foo = require( "./foo" ); 
console.log( foo.message );

If we run it, we get the following:

$node bar.js
foo runs
foo's export

One should note here that regardless of how many times we require a module, it gets executed just once, and every time, its exports are taken from the cache.

Starting with ES2015

As I have already mentioned, NW.js provides a complete support of JavaScript of ES2015 and ES2016 editions. To understand what it really means, we need a brief excursion into the history of the language. The standardized specification for JavaScript was first released in 1997 (ECMA-262 1st Edition).

Since then, the language has not really changed for 10 years. The 4th edition proposed in 2007 called for drastic changes. However, the working group (TC39) failed to agree on the feature set. Some proposals have been deemed unsound for the Web, but some were adopted in a new project code named Harmony. The project turned into the 6th edition of the language specification and was released in 2015 under the official name ES2015. Now, the committee is releasing a new specification every year.

New JavaScript is backward compatible with an earlier version. So, you can still write code with the syntax of the ECMAScript 5th edition or even 3rd one, but why should we lose the opportunity to work with the new advanced syntax and feature set? I think it would be helpful if we now go through some new language aspects that will be used in the application.

Scoping

In the old days, we used to always go with the var statement for variable declarations. ES2015 introduces two new declaration variables--let and const. The var statement declares a variable in a function scope:

(function(){ 
var foo = 1;
if ( true ) {
var foo = 2;
console.log( foo );
}
console.log( foo );
}()); $ node es6.js
2
2

A variable declared with var (foo) spans the entire function scope, meaning that every time we reference it by name, we target the same variable. Both let and const operate on block scopes (if statement, for/while loops, and so on) as shown:

 (function(){ 
let foo = 1;
if ( true ) {
let foo = 2;
console.log( foo );
}
console.log( foo );
}()); $ node es6.js
2
1

As you can see from the preceding example, we can declare a new variable in a block and it will exist only within that block. The statement const works the same, except it defines a constant that cannot be reassigned after it was declared.

Classes

JavaScript implies a prototype-based, object-oriented programming style. It differs from class-based OOP that is used in other popular programming languages, such as C++, C#, Objective-C, Java, and PHP. This used to confuse newcomer developers. ES2015 offers a syntactic sugar over the prototype, which looks pretty much like canonical classes:

 
class Machine {
constructor( name ){
this.name = name;
}
}
class Robot extends Machine {
constructor( name ){
super( name );
}
move( direction = "left" ){
console.log( this.name + " moving ", Robot.normalizeDirection( direction ) );
}
static normalizeDirection( direction ) {
return direction.toLowerCase();
}
}

const robot = new Robot( "R2D2" );
robot.move();
robot.move( "RIGHT" ); $ node es6.js
R2D2 moving left
R2D2 moving right

Here, we declare a Machine class that during instantiation assigns a value to a prototype property, name. A Robot class extends Machine and, therefore, inherits the prototype. In subtype, we can invoke the parent constructor with the super keyword.

We also define a prototype method--move--and a static method--normalizeDirection. The move method has a so-called default function parameter. So, if we omit the direction argument while calling move method, the parameter automatically sets to "left".

In ES2015, we can use a short syntax for the methods and do not need to repeat function keywords with every declaration. It's also available for object literals:

 
const R2D2 = {
name: "R2D2",
move(){
console.log( "moving" );
},
fly(){
console.log( "flying" );
}
};

The template literal

Another great addition to JavaScript is template literals. These are string literals that can be multiline and can include interpolated expressions (`${expression}`). For example, we can refactor our move method body, as follows:

  
console.log( `
${this.name} moving ${Robot.normalizeDirection( direction )}
` );

Getters and setters

Getters and setters were added back in ES5.1. In ES2015, it was extended for computed property names and goes hand in hand with a short method notation:

 
class Robot {
get nickname(){
return "But you have to prove first that you belong to the Rebel
Alliance!";
}
set nickname( nickname ){
throw new Error( "Seriously?!" );
}
};

const robot = new Robot();
console.log( robot.nickname );
robot.nickname = "trashcan"; $ node es6.js
But you have to prove first that you belong to the Rebel Alliance!
Error: Seriously?!

Arrow functions

A function declaration also obtained syntactic sugar. We write it now with a shorter syntax. It's remarkable that a function defined this way (fat arrow function) automatically picks up the surrounding context:

class Robot extends Machine { 
//...
isRebel(){
const ALLOWED_NAMES = [ "R2D2", "C3PO" ];
return ALLOWED_NAMES.find(( name ) => {
return name === this.name;
});
}
}

When using old function syntax, the callback function passed to an array's method, find, would lose the context of the Robot instance. Arrow functions, though, do not create their own context and, therefore, outer context (this) gets in the closure.

In this particular example, as it often goes with array extras, the callback body is extremely short. So, we can use an even shorter syntax:

return ALLOWED_NAMES.find( name => name === this.name ); 

Destructuring

In new JavaScript, we can extract specific data from arrays and objects. Let's say, we have an array that could be built by an external function, and we want its first and second elements. We can extract them as simple as this:

const robots =  [ "R2D2", "C3PO", "BB8" ]; 
const [ r2d2, c3po ] = robots;
console.log( r2d2, c3po );

So here, we declare two new constants--r2d2 and c3po--and assign the first and the second array elements to them, respectively.

We can do the same with objects:

const meta = { 
occupation: "Astromech droid",
homeworld: "Naboo"
};

const { occupation, homeworld } = meta;
console.log( occupation, homeworld );

What did we do? We declared two constants--occupation and homeworld--that receive values from correspondingly named object members.

What is more, we can even alias an object member while extracting:

const { occupation: affair, homeworld: home } = meta; 
console.log( affair, home );

In the last example, we delegated the values of object members--occupation and homeworld--to newly created constants--affair and home.

 

Handling windowing actions

Coming back to the file-explorer, we can start with the TitleBarActions module that listens to user click events on title bar buttons and performs the corresponding windowing action. First, we need to mark the action nodes in HTML. The ./index.html file contains the following code:

<header class="l-app__titlebar titlebar" data-bind="titlebar"> 
...
<a class="titlebar__btn" data-bind="close" > ;</a>
</header>

Here, we specify our bounding box (data-bind="titlebar") and the close window button (data-bind="close"). Let's begin with the only button. The ./js/View/TitleBarActions.js file contains the following code:

class TitleBarActionsView { 

constructor( boundingEl ){
this.closeEl = boundingEl.querySelector( "[data-bind=close]" );
this.bindUi();
}

bindUi(){
this.closeEl.addEventListener( "click", this.onClose.bind( this ), false );
}

onClose( e ) {
e.preventDefault();
nw.Window.get().close();
}
}

exports.TitleBarActionsView = TitleBarActionsView;

Here, we define a TitleBarActionView class that accepts an HTML element as a parameter. This element represents the view bounding box, meaning that the instance of this class will take care only of the passed in element and its descendants. During construction, the class will search for the first element in the scope of the bounding box that matches selector [data-bind=close]--the close window button of the title bar. In the bindUI method, we subscribe for clicks events on the Close button. When the button is clicked, the onClose method is called in the context of a TitleBarActionView instance, as we bound it in bindUi (this.onClose.bind( this )). The onClose method closes the window using the NW.js Window API (http://docs.nwjs.io/en/latest/References/Window/), namely it requests a current window object nw.Window.get() and calls its close method.

NW.js doesn't provide a module for the API, but exposes the nw variable in the global scope.

So, we have the first view module and can use it the main script:

./js/app.js

const { TitleBarActionsView } = require( "./js/View/TitleBarActions" ); 

new TitleBarActionsView( document.querySelector( "[data-bind=titlebar]" ) );

Here, we import the TileBarActionView class from the ./js/View/TitleBarActions module and make an instance of it. We pass the first document element matching selector [data-bind=titlebar] to the class constructor.

Have you noticed that we used destructuring while importing from the module? Particularly, we extracted the TitleBarActionsView class into a respectively called constant.

Now, we can launch the application and observe, as clicking on the close button really closes the window.

Going further, we take care of other title bar buttons. So, we adapt our index.html file to identify the buttons, nodes with unmaximize, maximize, and minimize values for the data-bind attribute. Then, we collect in the TileBarActionView constructor references to the corresponding HTML elements:

this.unmaximizeEl = boundingEl.querySelector( "[data-bind=unmaximize]" ); 
this.maximizeEl = boundingEl.querySelector( "[data-bind=maximize]" );
this.minimizeEl = boundingEl.querySelector( "[data-bind=minimize]" );

Of course, we have to add new listeners in the bindUi module, respectively:

this.minimizeEl.addEventListener( "click", this.onMinimize.bind( this ), false ); 
this.maximizeEl.addEventListener( "click", this.onMaximize.bind( this ), false );
this.unmaximizeEl.addEventListener( "click", this.onUnmaximize.bind( this ), false );

The handler for minimizing the window button looks pretty much the same as the one we have already examined previously. It just uses the corresponding method of the NW.js Window API:

onMinimize( e ) { 
e.preventDefault();
nw.Window.get().minimize();
}

With maximize and minimize (restore) window buttons, we need to take the fact that while one button is visible the second one shall be hidden into account. This we achieve with the toggleMaximize method:

toggleMaximize(){ 
this.maximizeEl.classList.toggle( "is-hidden" );
this.unmaximizeEl.classList.toggle( "is-hidden" );
}

Event handler for these buttons calls this method to the toggle buttons view:

  
onUnmaximize( e ) {
e.preventDefault();
nw.Window.get().unmaximize();
this.toggleMaximize();
}

onMaximize( e ) {
e.preventDefault();
nw.Window.get().maximize();
this.toggleMaximize();
}
 

Writing a service to navigate through directories

Other modules, such as FileListView, DirListView, and TitleBarPath, consume the data from the filesystem, such as directory list, file list, and the current path. So we need to create a service that will provide this data:

./js/Service/Dir.js

const fs = require( "fs" ), 
{ join, parse } = require( "path" );

class DirService {

constructor( dir = null ){
this.dir = dir || process.cwd();
}

static readDir( dir ) {
const fInfoArr = fs.readdirSync( dir, "utf-8" ).map(( fileName ) => {
const filePath = join( dir, fileName ),
stats = DirService.getStats( filePath );
if ( stats === false ) {
return false;
}
return {
fileName,
stats
};
});
return fInfoArr.filter( item => item !== false );
}

getDirList() {
const collection = DirService.readDir( this.dir ).filter(( fInfo )
=> fInfo.stats.isDirectory() );
if ( !this.isRoot() ) {
collection.unshift({ fileName: ".." });
}
return collection;
}

getFileList() {
return DirService.readDir( this.dir ).filter(( fInfo ) =>
fInfo.stats.isFile() );
}

isRoot(){
const { root } = parse( this.dir );
return ( root === this.dir );
}

static getStats( filePath ) {
try {
return fs.statSync( filePath );
} catch( e ) {
return false;
}
}

};

exports.DirService = DirService;

First of all, we import Node.js core module fs that provides us access to the filesystem. We also extract functions--join and parse--from the path module. We will need them for manipulations in the file/directory path.

Then, we declare the DirService class. On construction, it creates a dir property, which takes either a passed-in value or the current working directory (process.cwd()). We add a static method--readDir--to the class that reads the directory content on a given location. The fs.readdirSync method retrieves the content of a directory, but we extend the payload with file/directory stats (https://nodejs.org/api/fs.html#fs_class_fs_stats). In case the stats cannot be obtained, we replace its array element with false. To avoid such gaps in the output array, we will run the array filter method. Thus, on the exit point, we have a clean array of filenames and file stats.

The getFileList method requests readDir for the current directory content and filters the list to leave only files in there.

The getDirList method filters, evidently, the list for directories only. Besides, it prepends the list with a .. directory for upward navigation, but only if we are not in the system root.

So, we can get both lists from the modules consuming them. When the location changes and new directory and file lists get available, each of these modules have to update. To implement it, we will use the observe pattern:

./js/Service/Dir.js

//.... 
const EventEmitter = require( "events" );

class DirService extends EventEmitter {

constructor( dir = null ){
super();
this.dir = dir || process.cwd();
}
setDir( dir = "" ){
let newDir = path.join( this.dir, dir );
// Early exit
if ( DirService.getStats( newDir ) === false ) {
return;
}
this.dir = newDir;
this.notify();
}

notify(){
this.emit( "update" );
}
//...
}

We export from events, core module the EventEmitter class (https://nodejs.org/api/events.html). By extending it with DirService, we make the service an event emitter. It gives us the possibility to fire service events and to subscribe on them:

dirService.on( "customEvent", () => console.log( "fired customEvent" )); 
dirService.emit( "customEvent" );

So whenever the setDir method is called to change the current location, it fires an event of type "update". Given the consuming modules are subscribed, they respond to the event by updating their views.

Unit-testing a service

We've written a service and assume that it fulfills the functional requirements, but we do not know it for sure, yet. To check it, we will create a unit-test.

We do not have any test environment so far. I would suggest going with the Jasmine test framework (https://jasmine.github.io/). We will create in our tests/unit-tests subfolder a dedicated NW.js project, which will be used for the testing. This way, we get the runtime environment for tests, identical to what we have in the application.

So we create the test project manifest:

./tests/unit-tests/package.json

{ 
"name": "file-explorer",
"main": "specs.html",
"chromium-args": "--mixed-context"
}

It points at the Jasmine test runner page, the one we placed next to package.json:

./tests/unit-tests/specs.html

<!doctype html> 
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine-
html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/boot.js"></script>
</head>
<body>
<div id="sandbox" style="display: none"></div>
<script>
// Catch exception and report them to the console.
process.on( "uncaughtException", ( err ) => console.error( err ) );
const path = require( "path" ),
jetpack = require( "fs-jetpack" ),
matchingSpecs = jetpack.find( "../../js", {
matching: [
"*.spec.js",
"!node_modules/**"
]
}, "relativePath" );

matchingSpecs.forEach(( file ) => {
require( path.join( __dirname, file ) );
});
</script>
</body>
</html>

What does this runner do? It loads Jasmine, and with help of the fs-jetpack npm module (https://www.npmjs.com/package/fs-jetpack), it traverses the source directory recursively for all the files matching "*.spec.js" pattern. All these files get added to the test suite. Thus, it assumes that we keep our test specifications next to the target source modules.

fs-jetpack is an external module, and we need to install the package and add it to the development dependencies list:

npm i -D fs-jetpack

Jasmine implements a wide-spread, frontend development testing paradigm Behavior-driven Development (BDD) that can be described with the following pattern:

 
describe( "a context e.g. class or module", () => {
describe( "a context e.g. method or function", () => {
it( "does what expected", () => {
expect( returnValue ).toBe( expectedValue );
});
});
});

As it is generally accepted in unit testing, a suite may have setup and teardown:

beforeEach(() => { 
// something to run before to every test
});
afterEach(() => {
// something to run after to every test
});

When testing a service that touches the filesystem or communicates across the network or talks to databases, we have to be careful. A good unit test is independent from the environment. So, to test our DirService, we have to mock the filesystem. Let's test the getFileList method of the service class to see it in action:

./js/Service/Dir.spec.js

const { DirService } = require( "./Dir" ), 
CWD = process.cwd(),
mock = require( "mock-fs" ),
{ join } = require( "path" );

describe( "Service/Dir", () => {

beforeEach(() => {
mock({
foo: {
bar: {
baz: "baz", // file contains text baz
qux: "qux"
}
}
});
});
afterEach( mock.restore );

describe( "#getFileList", () => {
it( "receives intended file list", () => {
const service = new DirService( join( "foo", "bar" ) );
service.setDir( "bar" );
let files = service.getFileList();
expect( files.length ).toBe( 2 );
});
it( "every file has expected properties", () => {
const service = new DirService( join( "foo", "bar" ) );
const files = service.getFileList();
console.log( files );
const [ file ] = files;
expect( file.fileName ).toBe( "baz" );
expect( file.stats.size ).toBe( 3 );
expect( file.stats.isFile() ).toBe( true );
expect( file.stats.isDirectory() ).toBe( false );
expect( file.stats.mtime ).toBeTruthy();
});
});
});

Before running a test, we point the fs method to a virtual filesystem with the folder /foo/bar/ that contains the baz and qux files. After every test, we restore access to the original filesystem. In the first test, we instantiate the service on the foo/bar location and read the content with the getFileList() method. We assert the number of found files as 2 (as we defined in beforeEach). In the second test, we take the first element of the list and assert that it contains the intended filename and stats.

As we use an external npm package (https://www.npmjs.com/package/mock-fs) for mocking, we need to install it:

npm i -D mock-fs

As we came up with the first test suite, we can modify our project manifest file for a proper test runner script. The ./package.json file contains the following code:

{ 
...
"scripts": {
...
"test": "nw tests/unit-tests"
},
...
}

Now, we can run the tests:

npm test

NW.js will load and render the following screen:

Ideally, unit tests cover all the available functions/methods in the context. I believe that from the preceding code you will get an idea of how to write the tests. However, you may stumble over testing the EventEmitter interface; consider this example:

 
describe( "#setDir", () => {
it( "fires update event", ( done ) => {
const service = new DirService( "foo" );
service.on( "update", () => {
expect( true ).toBe( true );
done();
});
service.notify();
});
});

EventEmitter works asynchronously. When we have asynchronous calls in the test body, we shall explicitly inform Jasmin when the test is ready so that the framework could proceed to the next one. That happens when we invoke the callback passed to its function. In the preceding sample, we subscribe the "update" event on the service and call notify to make it fire the event. As soon as the event is captured, we invoke the done callback.

 

Writing view modules

Well, we have the service, so we can implement the view modules consuming it. However, first we have to mark the bounding boxes for the view in the HTML:

./index.html

<span class="titlebar__path" data-bind="path"></span> 
..
<aside class="l-main__dir-list dir-list">
<nav>
<ul data-bind="dirList"></ul>
</nav>
</aside>
<main class="l-main__file-list file-list">
<nav>
<ul data-bind="fileList"></ul>
</nav>
</main>

The DirList module

What are our requirements for the DirList view? It renders the list of directories in the current path. When a user selects a directory from the list, it changes the current path. Subsequently, it updates the list to match the content of the new location:

./js/View/DirList.js

class DirListView { 

constructor( boundingEl, dirService ){
this.el = boundingEl;
this.dir = dirService;
// Subscribe on DirService updates
dirService.on( "update", () => this.update( dirService.getDirList() ) );
}

onOpenDir( e ){
e.preventDefault();
this.dir.setDir( e.target.dataset.file );
}

update( collection ) {
this.el.innerHTML = "";
collection.forEach(( fInfo ) => {
this.el.insertAdjacentHTML( "beforeend",
`<li class="dir-list__li" data-file="${fInfo.fileName}">
<i class="icon">folder</i> ${fInfo.fileName}</li>` );
});
this.bindUi();
}

bindUi(){
const liArr = Array.from( this.el.querySelectorAll( "li[data-file]" ) );
liArr.forEach(( el ) => {
el.addEventListener( "click", e => this.onOpenDir( e ), false );
});
}
}

exports.DirListView = DirListView;

In the class constructor, we subscribe for the DirService "update" event. So, the view gets updated every time the event fired. Method update performs view update. It populates the bounding box with list items built of data received from DirService . As it is done, it calls the bindUi method to subscribe the openDir handler for click events on newly created items. As you may know, Element.querySelectorAll returns not an array, but a non-live NodeList collection. It can be iterated in a for..of loop, but I prefer the forEach array method. That is why I convert the NodeList collection into an array.

The onOpenDir handler method extracts target directory name from the data-file attribute and passes it to DirList in order to change the current path.

Now, we have new modules, so we need to initialize them in app.js:

./js/app.js

const { DirService } = require( "./js/Service/Dir" ), 
{ DirListView } = require( "./js/View/DirList" ),
dirService = new DirService();

new DirListView( document.querySelector( "[data-bind=dirList]" ), dirService );

dirService.notify();

Here, we require new acting classes, create an instance of service, and pass it to the DirListView constructor together with a view bounding box element. At the end of the script, we call dirService.notify() to make all available views update for the current path.

Now, we can run the application and observe as the directory list updates as we navigate through the filesystem:

npm start 

Unit-testing a view module

Seemingly, we are expected to write unit test, not just for services, but for other modules as well. When testing a view we have to check whether it renders correctly in response to specified events:

./js/View/DirList.spec.js

const { DirListView } = require( "./DirList" ), 
{ DirService } = require( "../Service/Dir" );

describe( "View/DirList", function(){

beforeEach(() => {
this.sandbox = document.getElementById( "sandbox" );
this.sandbox.innerHTML = `<ul data-bind="dirList"></ul>`;
});

afterEach(() => {
this.sandbox.innerHTML = ``;
});

describe( "#update", function(){
it( "updates from a given collection", () => {
const dirService = new DirService(),
view = new DirListView( this.sandbox.querySelector( "[data-bind=dirList]" ), dirService );
view.update([
{ fileName: "foo" }, { fileName: "bar" }
]);
expect( this.sandbox.querySelectorAll( ".dir-list__li" ).length ).toBe( 2 );
});
});
});

If you might remember in the test runner HTML, we had a hidden div element with sandbox for id. Before every test, we populate that element with the HTML fragment the view expects. So, we can point the view to the bounding box with the sandbox.

After creating a view instance, we can call its methods, supplying them with an arbitrary input data (here, a collection to update from). At the end of a test, we assert whether the method produced the intended elements within the sandbox.

In the preceding test for simplicity's sake, I injected a fixture array straight to the update method of the view. In general, it would be better to stub getDirList of DirService using the Sinon library (http://sinonjs.org/). So, we could also test the view behavior by calling the notify method of DirService--the same as it happens in the application.

The FileList module

The module handling the file list works pretty similar to the one we have just examined previously:

./js/View/FileList.js

const filesize = require( "filesize" ); 

class FileListView {

constructor( boundingEl, dirService ){
this.dir = dirService;
this.el = boundingEl;
// Subscribe on DirService updates
dirService.on( "update", () => this.update(
dirService.getFileList() ) );
}

static formatTime( timeString ){
const date = new Date( Date.parse( timeString ) );
return date.toDateString();
}

update( collection ) {
this.el.innerHTML = `<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>`;
collection.forEach(( fInfo ) => {
this.el.insertAdjacentHTML( "beforeend", `<li class="file-
list__li" data-file="${fInfo.fileName}">
<span class="file-list__li__name">${fInfo.fileName}</span>
<span class="file-list__li__size">${filesize(fInfo.stats.size)}</span>
<span class="file-list__li__time">${FileListView.formatTime(
fInfo.stats.mtime )}</span>
</li>` );
});
this.bindUi();
}

bindUi(){
Array.from( this.el.querySelectorAll( ".file-list__li" )
).forEach(( el ) => {
el.addEventListener( "click", ( e ) => {
e.preventDefault();
nw.Shell.openItem( this.dir.getFile( el.dataset.file ) );
}, false );
});
}

}

exports.FileListView = FileListView;

In the preceding code, in the constructor, we again subscribed the "update" event, and when it was captured, we run the update method on a collection received from the getFileList method of DirService. It renders the file table header first and then the rows with file information. The passed-in collection contains raw file sizes and modification times. So, we format these in a human-readable form. File size gets beautified with an external module--filesize (https://www.npmjs.com/package/filesize)--and the timestamp we shape up with the formatTime static method.

Certainly, we shall load and initialize the newly created module in the main script:

./js/app.js

const { FileListView } = require( "./js/View/FileList" ); 
new FileListView( document.querySelector( "[data-bind=fileList]" ), dirService );

The title bar path module

So we have a directory and file lists responding to the navigation event, but the current path in the title bar is still not affected. To fix it, we will make a small view class:

./js/View/TitleBarPathView.js

class TitleBarPathView { 

constructor( boundingEl, dirService ){
this.el = boundingEl;
dirService.on( "update", () => this.render( dirService.getDir() ) );
}

render( dir ) {
this.el.innerHTML = dir;
}
}

exports.TitleBarPathView = TitleBarPathView;

You can note that the class simply subscribes for an update event and modifies the current path accordingly to DirService.

To get it live, we will add the following lines to the main script:

./js/app.js

const { TitleBarPathView } = require( "./js/View/TitleBarPath" ); 
new TitleBarPathView( document.querySelector( "[data-bind=path]" ), dirService );
 

Summary

So we've made it to the milestone and have now a working version of the File Explorer providing basic functionality. What have we achieved so far?

We went together though the traditional development routine: we planned, sketched, set up, templated, styled, and programmed. On the way, we discussed the best practices of writing maintainable and conflict-free CSS. We have discovered that NW.js enables the features of the latest CSS and JavaScript specifications. So while refactoring our CSS code, we exploited new aspects, such as custom properties and position sticky. We also had a tour of the basics of ES2015, which helped us to build our JavaScript modules in a cleaner syntax using classes, arrow functions, destructuring, and block scope declarations.

What is more, we explored a few of the goodies normally unavailable in the browser, such as Node.js core and external modules, and the desktop environment integration API. Thus, we were able to access the filesystem and implement windowing actions (close, minimize, maximize, and restore). We made a service extending Node.js EventEmitter and incorporated the event-based architecture to serve our needs.

We didn't forget about unit-testing. We set up Jasmine testing runner and discussed the essentials of BDD specifications. While writing the application unit tests, we examined an approach to mock the filesystem and one to test Document Object Model (DOM) manipulations.

Evidently, there's still much left for the second chapter, where we will augment the existing functionality, dive deeper into NW.js API, and go through the preproduction steps. Yet, I hope that you have already accrued a grasp on NW.js and HTML5 desktop development basics. See? It doesn't differ much from traditional web development after all, just unlocks new exciting possibilities.

About the Author
  • Dmitry Sheiko

    Dmitry Sheiko is a web developer, blogger, and open source contributor, living and working in the lovely city of Frankfurt am Main, Germany. Dmitry got hooked on computer programming in late 80s. Since 1998, he has been in web-development. Over the last few years, Dmitry has been creating desktop applications with NW.js/Electron. The very first solutions were made with JavaScript/Backbone, but then he switched to TypeScript/React/Redux. Dmitry has authored dozens of projects at GitHub, including: nw-autoupdater, Pragmatic CSS, and a CommonJS compiler.

    Browse publications by this author
Latest Reviews (2 reviews total)
Some of the technology is out of date.
Very good. I like it. Great.
Cross-platform Desktop Application Development: Electron, Node, NW.js, and React
Unlock this book and the full library FREE for 7 days
Start now