Building a file explorer with Angular 2 and Electron

After having a deeper look into the project environment and the basic anatomy of an Angular-CLI-Electron project in the last article we now want to build a simple application using the Node.js as well as the Electron API to get a deeper insight into the possibilities of such an app and to gain more familiarity with the environment. Our example will be a basic file explorer which can browse through the directories of our file system and open files in their associated default program. It should also provide a keyboard shortcut to move up in the file tree.

You can find the sample project of this article in the electron-explorer GitHub Repo. If you want to have a look at a bigger sample project, you could use Electron-Microscope.

Setting up the project

Commit: 7ecab6c

To set up the project you can either follow the steps in the last article or you use the angular-2-electron-seed project. For the purpose of this article I will do the latter. So after cloning the seed with

git clone git@github.com:DevWurm/angular-2-electron-seed.git ./electron-explorer

we change into the project directory (electron-explorer) and open the package.json where we adapt the name field to match our project name electron-explorer. After this we have to run

npm install

to install the required dependencies.

Building the application

After setting up the project we can run npm run start to have a look at the current state.

Screenshot Angular 2 – Electron seed default app
Figure 1: Angular 2 – Electron seed default app

We can see the default app of the Angular-CLI inside of Electron.

Creating the file list

Commit: 7675758

To build a simple file list view we just need a component which displays a list of all files in the current directory. All directory entries are displayed as links, which change the currently displayed directory or open the selected file. For the purpose of simplicity we store the currently active directory in the component itself. We fetch all available files in the active directory via the Node.js core module fs and display the entries via NgFor.

So let’s start with creating the component. To do so, change into the directory src/app and create a new component via the scaffolding command of the Angular-CLI by running

ng generate component files

This creates a new component called FilesComponent which is bound to the HTML tag <app-files></app-files>. The generated component already contains a basic component class in the file src/app/files/files.component.ts, decorated with the correctly configured @Component decorator. To implement our component we have to include the necessary file system functions from the Node.js core module fs and some other functions from the Node core via

    import { readdir, stat } from 'fs';
    import { resolve } from 'path';
    [...]

After this we implement our properties for storing the current directory, as well as all file system elements in this directory. These properties are always updated in sync via the changeDir method when the user clicks on a directory entry. When a regular file is clicked we want to open it in the systems default application, rather than changing the directory. This can be implemented the following way:

    [...]
    @Component({
      selector: 'app-files',
      templateUrl: './files.component.html',
      styleUrls: ['./files.component.css']
    })
    export class FilesComponent implements OnInit {
      private currentPath: string = process.cwd();
      private entries: Array<string>;
      constructor() {
        this.updateEntries();
      }
      ngOnInit() {}
      private updateEntries() {
        readdir(this.currentPath, (err: Error, files: [string]) => {
          if (err) {
            console.error(err);
          }
          this.entries = ['../'].concat(files);
        });
      }
      private changeDir(newDir: string) {
        const targetPath = resolve(this.currentPath, newDir);
        stat(targetPath, (err, stats) => {
          if (err) {
            console.error(err);
          }
          if (stats.isFile()) {
            this.openFile(targetPath);
          } else if (stats.isDirectory()) {
            this.currentPath = targetPath;
            this.updateEntries();
          } else {
            console.error(new Error(`Unknown file system object: ${targetPath}`));
          }
        });
      }
      private openFile(path: string) {
        // TODO: Implement file opening
        return
      }
    }

To represent these information in our view we list all the file system entries via NgFor and handle the user interaction via a click binding by inserting the following into src/app/files/files.component.html:

    <ul *ngFor="let entry of entries">
      <li><a (click)="changeDir(entry)">{{entry}}</a></li>
    </ul>

During development you can live-test and debug your app by running npm run start and you can create unit tests by adding test cases in the <component>.spec.ts file of your component and run them via npm run test.

After adding the directive to the main view (it is added to the module automatically by the CLI), by inserting

    <app-files></app-files>

at the end of src/app.component.html and setting the correct application title in src/app.component.ts we can have a first look at our file list.

Screenshot Electron-Explorer file list
Figure 2: Electron-Explorer file list

Opening files

Commit: b93f98f

If the user clicks on a file, we want to open the file in the default program associated with the file type on the users system. Fortunately Electron provides this feature out of the box with the function shell.openItem.
Lets include the shell object by adding

    import { shell } from 'electron';

to the top of src/app/files/files.component.ts and implement our openFile method by using the provided helper function:

    [...]
      private openFile(path: string) {
        shell.openItem(path);
      }
    [...]

When clicking on a file entry in our file list our app now opens it in the associated default application or prompts for action.

Screenshot Electron-Explorer when opening a file
Figure 3: Electron-Explorer when opening a file

Using keyboard shortcuts

Commit: 61b0735

Implementing keyboard shortcuts is a bit harder because of Electrons architecture. Electron allows the registration of keyboard shortcuts in the MainProcess only, while our application gets executed in a RendererProcess. Because we want to handle the keyboard shortcuts in our application we have to use Inter-process communication (IPC) to communicate between the MainProcess and a RendererProcess. Fortunately Electron provides an easy event-based IPC mechanism to achieve this. In the Electron IPC mechanism the MainProcess doesn’t know how to send information to a RendererProcess until it gets a reference to corresponding IPC endpoint through an incoming message. Therefor our application has to request the keyboard shortcut in the RendererProcess using ipcRenderer.send:

    import { [...], ipcRenderer } from 'electron';
    [...]
    class FilesComponent implements OnInit {
      [...]
      ngOnInit() {
        ipcRenderer.send("request-keyboard-shortcut", "Backspace");
      }
      [...]
    }

Our files component now sends a message to our MainProcess containing the desired keyboard shortcut. Our MainProcess now listens for the request-keyboard-shortcut message, subscribes to the provided shortcut via globalShortcut.register and notifies our component via event.sender.send. To implement this behavior, we edit src/entry.js:

    const {[...], ipcMain, globalShortcut} = require('electron')
    [...]
    ipcMain.on('request-keyboard-shortcut', (event, shortcut) => {
      globalShortcut.register(shortcut, () => {
        event.sender.send(`keyboard-shortcut-${shortcut}`);
      });
    })
    [...]

Make sure to make the corresponding changes in src/entry.live.js to enable the feature when running your app with npm run start, too. Now we can listen for the corresponding keyboard-shortcut-Backspace event in our component and move upwards in the file tree:

    [...]
    class FilesComponent implements OnInit {
      [...]
      ngOnInit() {
        [...]
        ipcRenderer.on('keyboard-shortcut-Backspace', () => {
            this.changeDir('..');
        })
      }
      [...]
    }

Now our app reacts to key presses on Backspace and moves to the parent directory. Great!

With only little code we created our own small file explorer which already integrates itself into the surrounding system and there are a lot more APIs we could use to achieve more system integration and features!

As we have seen Electron can be a great tool to develop full-featured desktop applications especially when combined with a modern GUI application framework like Angular 2.

This post was written by: