This article is a continuation of the article
"Desktop applications in JavaScript. Part 1 " . In the previous section, we looked at the following:
- install NW.js
- building and running applications on NW.js
- basics of working with native controllers (for example, creating a menu)
As part of this article, we will look at creating a password storage application. The application is relatively simple and is mostly a prototype for real. However, if you want and have time, you can modify it and use it for everyday work.
The basis of the application for storing passwords
As you know, the development can be conducted both in pure JavaScript, and using a variety of frameworks, of which there is such a huge amount that
sometimes you lose yourself in their diversity and for a long time you cannot decide what to choose . For application development, patterns that start with MV (
MVC ,
MVVM ,
MVP ) are especially popular. One of the frameworks using a similar pattern is
Angular JS , which is what we will use when developing our application. If you are not familiar with it, I advise you to read the documentation (
tutorial ,
API ), you can also learn basic information in the
manual in Russian.
What will be the application? All data is displayed in the form of a table, with the login should be visible, and instead of the password should be asterisks. The user can add a new login / password, as well as delete entries that have become unnecessary. In addition, it is necessary to provide for the possibility of editing.
')
We implement the basic functionality of the application. To do this, you need to create a folder in which we will locate the source code, and also place
package.json in it (on how to do this, see Part 1).
Create a basic folder structure consisting of the following directories:
- CSS - we will place styles in this folder (Add the index.css file here , which will contain the main styles)
- Controller - there will be controllers
- View - folder for views
- Directive - folder with directives
- Lib libraries (in this folder you need to copy angular.min.js , about how to add angularJS )
In addition, we will add the
index.html file to the project root, which will be the entry point to the application. Create the basic markup:
Basic markup<html ng-app="main"> <head> <meta charset="utf-8"> <title>Password keeper</title> <link rel="stylesheet" type="text/css" href="css/index.css"> </head> <body> <table> <thead> <tr> <td></td> <td>Login</td> <td>Password</td> <td></td> </tr> </thead> <tbody> <tr> <td></td> <td></td> <td></td> <td><a></a></td> </tr> </tbody> <tfoot> <tr> <td></td> <td><a></a></td> <td></td> <td></td> </tr> </tfoot> </table> <script type="text/javascript" src="lib/angular.min.js"></script> </body> </html>
Since the application is fairly simple, we will create a controller and within it we will place all the main application logic (as the logic grows, you need to add the
Service folder and place
services in it, where all the complex logic should be placed, the controllers should be left "Thin"). Let's call the
main controller, and the
main.ctrl.js controller
file . So, the blank for the controller:
(function () { 'use strict'; angular .module('main', []) .controller('MainCtrl', [MainCtrl]); function MainCtrl() { this.data = []; return this; } })();
Data containing logins / passwords for our prototype will be placed in the
data array. To simplify the implementation of editing, create your own
EditableText element and arrange it as a directive. This element will work as follows: the element is displayed as text, when you click on an element, the text turns into a textual input field, and when the focus is lost, the element is again displayed as text. To do this, we will create a markup file for the
editableText.html directive inside the
View folder:
<input ng-model="value"> <span ng-click="edit()">{{value}}</span>
And inside the
directive folder, create an
editableText.js file:
Editable-text directive code (function () { 'use strict'; angular .module('main') .directive('editableText', [editableText]); function editableText() { var directive = { restrict: 'E', scope: { value: "=" }, templateUrl: 'view/editableText.html', link: function ( $scope, element, attrs ) {
The directive also requires some styles that can be placed inside
index.css :
.editable-text span { cursor: pointer; } .editable-text input { display: none; } .editable-text.active span { display: none; } .editable-text.active input { display: inline-block; }
The use of the directive is as follows:
<editable-text value="variable"></editable-text>
For the login, everything is in order - we display either the text or the text field, but what about the password, because we should not show it. Add a crypto field to the
scope of our directive as follows:
scope: { value: "=", crypto: "=" }
And also change the markup directive:
<input ng-model="value"> <span ng-click="edit()">{{crypto?'***':value}}</span>
In addition, you must not forget to add scripts to index.html:
<script type="text/javascript" src="lib/angular.min.js"></script> <script type="text/javascript" src="controller/main.ctrl.js"></script> <script type="text/javascript" src="directive/editableText.js"></script>
It's time to add functionality. Change the controller as follows:
Controller code function MainCtrl() { var self = this; this.data = []; this.remove = remove; this.copy = copy; this.add = add; return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){
In addition, changes to the markup are needed:
Summary markup <body ng-controller="MainCtrl as ctrl"> <table> <thead> <tr> <td></td> <td>Login</td> <td>Password</td> <td></td> </tr> </thead> <tbody> <tr ng-repeat="record in ctrl.data track by $index"> <td><a ng-click="ctrl.copy($index)">{{$index}}</a></td> <td><editable-text value="record.login"></editable-text></td> <td><editable-text value="record.password" crypto="true"></editable-text></td> <td><a ng-click="ctrl.remove($index)"></a></td> </tr> </tbody> <tfoot> <tr> <td></td> <td><a ng-click="ctrl.add()"></a></td> <td></td> <td></td> </tr> </tfoot> </table> <script type="text/javascript" src="lib/angular.min.js"></script> <script type="text/javascript" src="controller/main.ctrl.js"></script> <script type="text/javascript" src="directive/editableText.js"></script> </body>
At this stage, you can do styling. An example of simple styling (I remind you that we add styles to
index.css , however, if there are quite a lot of styles, you can split the styles into files or even use a preprocessor, for example
LESS ):
An example of styling an application table { border-collapse: collapse; margin: auto; width: calc(100% - 40px); } table, table thead, table tfoot, table tbody tr td:first-child, table tbody tr td:nth-child(2), table tbody tr td:nth-child(3), table thead tr td:nth-child(2), table thead tr td:nth-child(3) { border: 1px solid #000; } table td { padding: 5px; } table thead { background: #EEE; } table tbody tr td:first-child { background: #CCC; } table tbody tr td:nth-child(2) { background: #777; color: #FFF; } table tbody tr td:nth-child(3) { background: #555; color: #FFF; } table thead tr td:nth-child(2),table thead tr td:nth-child(3) { text-align: center; } table a { font-size: smaller; cursor: pointer; }
The application is as follows:

Work with clipboard
So, the basis of the application is ready, but it does not yet realize the main purpose; we cannot copy passwords (or rather, we can, but rather inconveniently). To begin, consider the work with the clipboard in NW.js
There is a special object -
Clipboard , which is used as an
abstraction for the Windows
clipboard and GTK, as well as for pasteboard (Mac). At the time of this writing, support is provided for writing and reading only text.
To work with the object, we need the familiar module
nw.gui :
var gui = require('nw.gui'); var clipboard = gui.Clipboard.get();
Please note that we can not create your object, we can only get the system. Three methods are supported:
- get ([type]) - get an object from the clipboard indicating the type of this object, the default text , but so far this is the only supported type
- set (data, [type]) - send the object to the clipboard (only type - “text” is also supported)
- clear - clear the clipboard
Now you can finish the application functionality, and the controller will look like this:
Controller code function MainCtrl() { var self = this; var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); this.data = []; this.remove = remove; this.copy = copy; this.add = add; return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){ clipboard.set(self.data[ind].password); }; function add(){ self.data.push({login: "login", password: "password"}); }; }
Password storage
After the application was launched, the user stored several passwords, closed the application. The next day it turns out that the passwords are gone. The problem is that we kept them in a normal local variable, which was deleted when closed.
In the third part, we will look at how NW.js works with databases, but for now we will store passwords in
localStorage . Before proceeding to the creation of functionality, (although the application we have so far only a prototype) you need to take care of security. To do this, we do not need to store passwords in the clear.
For encryption / decryption there are various libraries in
javascript . One such library is
crypto-js . Install it as a module for
node.js. The library supports a large number of standards, a complete list of which can be found in the documentation. At the same time, you can connect both all modules and a separate module:
To encrypt a message, use the encrypt method:
var ciphertext = CryptoJS.AES.encrypt('', ' ');
Decryption is a bit more complicated:
var bytes = CryptoJS.AES.decrypt(ciphertext.toString(), ' '); var plaintext = bytes.toString(CryptoJS.enc.Utf8);
Let's modify our application so that we can save passwords when closing the application and load them at startup.
Let's create the
crypto.svc service and place it in the
service folder (if you have not created this folder yet, create it in the application root):
Service code (function () { 'use strict'; angular .module('main') .factory('CryptoService', [CryptoService]); function CryptoService() { var CryptoJS = require("crypto-js"); var secretKey = "secretKey"; var service = { encrypt: encrypt, decrypt: decrypt }; return service; function encrypt(data) { return CryptoJS.AES.encrypt(JSON.stringify(data), secretKey); } function decrypt(text) { var bytes = CryptoJS.AES.decrypt(text.toString(), secretKey); var decryptedData = JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); return decryptedData; } } })();
To use our service, we upgrade the controller:
Controller code (function () { 'use strict'; angular .module('main', []) .controller('MainCtrl', ['$scope', 'CryptoService', MainCtrl]); function MainCtrl($scope, CryptoService) { var self = this; var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); var localStorageKey = "loginPasswordData" this.data = []; this.remove = remove; this.copy = copy; this.add = add; load(); $scope.$watch("ctrl.data", save, true); return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){ clipboard.set(self.data[ind].password); }; function add(){ self.data.push({login: "login", password: "password"}); }; function load(){ var text = localStorage.getItem(localStorageKey); if(text) { self.data = CryptoService.decrypt(text); } } function save(){ if(self.data) { localStorage.setItem(localStorageKey, CryptoService.encrypt(self.data)); } } } })();
In addition to connecting the service, we also needed the
$ scope service that already exists in
AngularJS . We use the
$ watch method to track the moment of data change in order to save it in time (note that we pass
true as the third argument so that changes not only in the array, i.e. insert / delete, but also changes) are tracked elements of the array, ie, changing the login or password of a separate element of the array). Data loading occurs when opening the view.
Minimize to tray
The basis of the application is ready, but as you know, such programs are often minimized to the system tray in order not to overload the user with an abundance of open windows.
Another abstraction introduced in NW.js is tray:
System Tray Icon for Windows,
Status Icon for GTK,
Status Item for OSX. This object is created using the
gui module known to us:
var gui = require("nw.gui"); var tray = new gui.Tray({ title: 'Tray', icon: 'img/icon.png' });
When working with this object, you need to take care of the scope of the variable; if created inside a function, it will soon be removed by the
GC . When creating an object, you can immediately create properties, as we did in the example, or you can take care of this a little later. The following properties can be defined for this object:
- Title - will be displayed only in Mac OSX
- Tooltip - tooltip available on all platforms
- Icon - the icon displayed in the tray is also available on all platforms.
- Menu - a menu that in Mac OS X will appear on the click, for Windows and Linux - will respond to a single click and right click (for how to create a menu, see the first part of a series of articles)
In order to use the tray in our application, you need to create any markup element, the most obvious option is the
button . Next, you need to subscribe to the
click event, and use the methods of the
window object, which we will now become familiar with.
We work with a window
To work with a window, you must either get an existing window or create a new one. So, in order to get the current window, in which our application is displayed, you need to run the command:
var win = gui.Window.get();
And to create a new window, you must specify the address where the page is located to open in this window, as well as the parameters to open (these parameters correspond to what we specify when creating the manifest, see the first part of the series of articles):
var win = gui.Window.open ('https://myurl', { position: 'center', width: 901, height: 127 });
Also in the parameters you can pass a special property
focus: true , if you specify which, the newly created window will immediately receive focus, otherwise the focus will remain on our current window.
If we create a new window and want to do something with it after it is created, you need to subscribe to the appropriate event:
win.on ('loaded', function(){
As you can see from the example, one of the window properties is the window object, from which we can get the rest of the elements, including the document. In addition to this property, the window also supports many others:
- x, y - coordinates of the window
- width, height - window size
- title - window title
- menu - the main menu of the application, which will be located at the top of the window
These properties can not only read, but also change. In addition to them, there are also read-only properties (they are all logical and can be true or false)
- isTransparent - whether the window is transparent
- isFullscreen - is the window open full screen
- isKioskMode - is the application open in the kiosk mod
In addition to properties, the object supports a large number of methods. The main methods are given below and are grouped for convenience.
Methods for changing the position and size of the window:
- moveTo - move the window to the position passed in the parameters in the form of x coordinate and y coordinate
- moveBy - move the window by a certain number of pixels to the right and down (in case of setting negative arguments to the left and up)
- resizeTo - resize the window: the first argument indicates the width, the second - the height of the window
- resizeBy - resize the window by a certain number of pixels to the right and down (in case of setting negative arguments to the left and up)
- setPosition - set a specific window position, passed as an argument (currently only 'center' is supported)
Methods for working with focus and visibility:
- focus - method without parameters to transfer focus to the window
- blur - method without parameters to make the window inactive
- hide - hide the window
- show - show the window, but if you pass false as an argument, the method will work as hide
Methods for managing the coagulation / deployment, closing the window:
- close - closes the window, and a close event occurs; however, if you pass true as an argument, the event will not occur
- reload - reload window
- reloadDev - reload the window, but with developer elements
- maximize - open the window full screen
- unmaximize - return the window to its original size after the window has been opened
- minimize - minimize the window
- restore - maximize window, opposite
- setShowInTaskbar - whether to show the window on the taskbar
- setAlwaysOnTop - whether to show the window on top of others
Methods for state management:
- enterFullscreen, leaveFullscreen, toggleFullscreen - control fullscreen mode
- enterKioskMode, leaveKioskMode, toggleKioskMode - control kiosk mode
- setTransparent - set / reset window transparency, depending on the argument passed
- showDevTools - show developer tools
- closeDevTools - hide developer tools
- isDevToolsOpen - check: whether developer tools are open
Methods for controlling the ability to resize a window
- setResizable - set / reset the ability to change the screen size
- setMaximumSize - set limits on the maximum screen size (the first argument is width, the second is height)
- setMinimumSize - set limits on the minimum screen size (the first argument is the width, the second is the height)
So, having got acquainted with the objects
tray and
window , we write the functionality of minimizing to tray. To do this, it is necessary (as mentioned above) to add an element to the markup, for example, a button or a link:
<a ng-click="ctrl.toTray()"> </a>
And change the controller as follows:
Controller code (function () { 'use strict'; angular .module('main', []) .controller('MainCtrl', ['$scope', 'CryptoService', MainCtrl]); function MainCtrl($scope, CryptoService) { var self = this; var localStorageKey = "loginPasswordData" this.data = []; var gui = require('nw.gui'); var clipboard = gui.Clipboard.get(); var win = gui.Window.get(); var tray = new gui.Tray({ title: 'Tray', icon: 'img/test.png' }); tray.on("click", restoreFromTray); this.remove = remove; this.copy = copy; this.add = add; this.toTray = toTray; load(); $scope.$watch("ctrl.data", save, true); return this; function remove(ind){ self.data.splice(ind, 1); }; function copy(ind){ clipboard.set(self.data[ind].password); }; function add(){ self.data.push({login: "login", password: "password"}); }; function load(){ var text = localStorage.getItem(localStorageKey); if(text) { self.data = CryptoService.decrypt(text); } } function save(){ if(self.data) { localStorage.setItem(localStorageKey, CryptoService.encrypt(self.data)); } } function toTray(){ win.minimize(); win.setShowInTaskbar(false); } function restoreFromTray(){ win.restore(); win.setShowInTaskbar(true); } } })();
Also, for this example to work, you must create an
img folder and place the tray icon there (in this example,
img / test.png ).
Conclusion
Within the framework of this article, we wrote a prototype of the application, which you can improve in various ways: starting from styles and ending with improvements in functionality. For example:
- You can subscribe to the keydown event for the first 10 passwords, by pressing the button from 0 to 9, copy the password to the clipboard, this will simplify and speed up the work with the program
- add a way to copy not only the password, but also login
Successes in programming!