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 navigationFileService
: This handles file operationsFileListView
: This updates the file list with the data received from DirService, handles user events (open file, delete file, and so on) using FileServiceDirListView
: This updates the directory list with the data received from DirService and handles navigation events using DirServiceTitleBarPath
: This updates the current location with the path received from DirServiceTitleBarActions
: This handles user iteration with title bar buttonsLangSelector
: 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
.