A file-system browser component for Electron/Vue.js applications

; Date: Mon Jul 30 2018

Tags: Node.JS »»»» Electron »»»» Vue.js

Many kinds of applications need to browse files in the file system. For example most programmers text editors (I'm writing this in Microsoft Visual Studio Code) include a sidebar showing the file-system. Therefore it's important to have such a component to use in Electron/Vue.js applications. In this article we'll look at one way to implement such a component.

To follow along with this article, clone the following repositories:

Let's initialize the application:

$ vue init simulatedgreg/electron-vue electron-vue-file-browse

? Application Name electron-vue-file-browse
? Project description Electron/Vue.js file browser
? Select which Vue plugins to install vue-electron, vue-router
? Use linting with ESLint? No
? Set up unit testing with Karma + Mocha? No
? Set up end-to-end testing with Spectron + Mocha? No
? What build tool would you like to use? builder
? author David Herron <david@davidherron.com>

   vue-cli · Generated "electron-vue-file-browse".


All set. Welcome to your new electron-vue project!

Make sure to check out the documentation for this boilerplate at

Next Steps:

  $ cd electron-vue-file-browse
  $ yarn (or `npm install`)
  $ yarn run dev (or `npm run dev`)

The workspace at simulatedgreg/electron-vue includes a very useful framework for developing Electron applications using Vue.js. This downloads the skeleton anc creates the named directory. I've configured it to use vue-electron and vue-router but not the other components, and to not include any ESLint or unit testing features. You may find it useful to answer these questions differently. The framework includes support for both electron-builder and electron-packager and I've chosen the former.

Next we set up dependencies:

$ cd electron-vue-file-browse
$ npm install buefy walkdir path-directories vue-awesome sl-vue-tree --save
$ npm install
$ npm run dev

The last step is somewhat not-useful at the moment because we've done no configuration.

The chosen dependencies include:

  • Buefy is the Vue.js package that uses Bulma for components
  • Walkdir supports tracking changes in the files under a given directory
  • path-directories helps with splitting directory names that will be useful in building the file browser object tree
  • vue-awesome encapsulates the Font Awesome icons as Vue.js components
  • sl-vue-tree is a nice tree view component. We'll use this to store the file system contents

Let's start the surgery on the skeleton application.

src/main/main.js - the Main modeule in the Main process

We'll make a few select changes in main.js to tweak how the application launches.

mport { 
  app, BrowserWindow, ipcMain, dialog 
} from 'electron';
import path from 'path';
import walkdir from 'walkdir';

Change the imports as shown.

In the end of the supplied createWindow function add this line of code:


Then add the implementation of this function:

const rootdir = '/Volumes/Extra/akasha-tools/electron-vue-file-browse';

function scanDirectory() {
  walkdir('src', {})
  .on('file', (fn, stat) => {
    mainWindow.webContents.send('file', fn.slice(rootdir.length + 1), stat);
  .on('directory', (fn, stat) => {
    mainWindow.webContents.send('directory', fn.slice(rootdir.length + 1), stat);
  .on('error', (fn, err) => {
    console.error(`!!!! ${fn} ${err}`);

The idea is to "walk" a selected directory tree, sending out information about the files in that tree. Since this is a test application there are some hard-coded values. For a real application we'd have a file browser dialog to select a directory.

The walkdir module ( (www.npmjs.com) https://www.npmjs.com/package/walkdir) "Walks a directory tree emitting events based on what it finds." As it traverses the directory tree it emits events concerning what it finds. We simply forward those events to the Renderer process.

The fn argument is the complete pathname that is found. On studying this application it seems more useful to send not the complete pathname, but the segment following the root directory being scanned. Therefore we use the slice function to trim the string appropriately.

ipcMain.on('rescan-directory', () => {

This message can be sent from the Renderer process to cause the Main process to rescan the directory structure.

process.on('unhandledRejection', (reason, p) => {
  console.error(`Unhandled Rejection at: ${util.inspect(p)} reason: ${reason}`);

This catches any unhandle promise rejection errors. See: Handling unhandled Promise rejections, avoiding application crash

src/renderer/main.js - the Main module in the Renderer process

Change main.js to the following

import Vue from 'vue'

import { ipcRenderer } from 'electron';

import App from './App';
import router from './router';
import Buefy from 'buefy';
import 'buefy/lib/buefy.css';


if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.config.productionTip = false

export const messageBus = new Vue({

/* eslint-disable no-new */
new Vue({
  components: { App },
  template: '<App/>'

ipcRenderer.on('file', (event, fn, stat) => {
  messageBus.$emit('file', fn, stat);

ipcRenderer.on('directory', (event, fn, stat) => {
  messageBus.$emit('directory', fn, stat);

Some of this carries over from the skeleton, and the rest initializes support for some of the dependencies we loaded, as well as useful objects.

We initialize Buefy (the UI toolkit), sl-tree-vue (the tree viewer), and vue-awesome (Font Awesome).

The messageBus object is an extra Vue instance we'll use for sending messages around the application. Vue.js has great support for a child component to send messages to its parent component. But out of the box it does not directly support sending messages between any two components. We will use messageBus to do so - since all Vue instances contain capabilities to emit and listen to messages.

We see two such messages - the file and directory events are received from the Main process, and then are emitted on the messageBus.

src/renderer/router/index.js -- The Router

For this application the Router is not really required, but I said "yes" to it above so the code is here. This module is where you would handle multiple application pages. As it stands there is a single route for the / URL, and it refers to a LandingPage component. Change it to this:

export default new Router({
  routes: [
      path: '/',
      name: 'file-browser-page',
      component: require('@/components/FileBrowserMain').default
      path: '*',
      redirect: '/'


What we've done so far is position this component as the main window of the application. We'll implement it as a single-file-template that will in turn demonstrate using a file-system oriented tree browser.

In src/renderer/components delete all the supplied files. Those files comprise the demo application, and we need none of that code.

Create FileBrowserTree.vue to contain this:

  <div id="wrapper" class="columns is-gapless is-mobile">

    <template slot="context-menu">
        <div @click="doDashboard">Dashboard</div>
        <div @click="doCustomers">Customers</div>


    <div id="file-info-view" class="column">
        <span v-html="fileInfo"></span>
        <button class="button is-primary" @click="rescan">Rescan</button>


const path = require('path');
const util = require('util');
import { messageBus } from '../main.js';
import { ipcRenderer } from 'electron';

import FileBrowserTree from 'vue-file-tree';

export default {
  name: 'file-browser-main',
  components: {
    'file-browser-tree': FileBrowserTree,
  data() {
    return {
      fileInfo: "",
      nodes: []
  methods: {
    nodeClick(event, node) {
      this.fileInfo = `
    nodeDoubleClick(node) {
      this.fileInfo = `
    nodeDrop(node) {
      this.fileInfo = `
    doCustomers() {
        this.$refs.filetree.contextMenuIsVisible = false;
    doDashboard() {
        this.$refs.filetree.contextMenuIsVisible = false;
    rescan() {
      this.nodes = [];
  created: function() {
      messageBus.$on('file', (fn, stat) => { 
        this.$refs.filetree.addPathToTree(fn, stat, false);
      messageBus.$on('directory', (fn, stat) => { 
        this.$refs.filetree.addPathToTree(fn, stat, true);

  @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro');

  * {
    box-sizing: border-box;
    margin: 0;
    padding: 0;

  body { font-family: 'Source Sans Pro', sans-serif; }

  #wrapper {
    height: 100vh;
    width: 100vw;

  #file-tree, #file-info-view {
    height: 100%;
    overflow: scroll;


In the <template> we have a simple Buefy columns component. In the left-side column is the file browser, and the right side is an area to display information. The right side also includes a button to cause rescanning of the directory content.

The Rescan button causes a message to be sent to the Main process. If you refer back to the Main process you see this message causes the chosen directory to be scanned.

When the chosen directory is scanned a series of messages are sent here from the Main process. Those messages are either file or directory events, and contain both a pathname and the stat object corresponding to the file. Those messages are handled in event handlers, and turn into addPathToTree calls which add the pathname to the tree object. We'll get to that a little later.

Note: <file-browser-tree ... ref="filetree" ...></file-browser-tree>

The ref= attribute allows us to reference the component's object as so: this.$refs.filetree.addPathToTree(fn, stat, false);

Vue.js makes sure the $refs object has fields corresponding to every component with a ref= attribute. This component is documented a little further into the article.

We have three handlers for events emitted by that component:

  • nodeClick Handles clicks on items
  • nodeDoubleClick Handles double-clicks on items
  • nodeDrop Handles drag-and-drop operations on items

In this demo application each prints information in the information pane.

The file tree component also supports a context menu, activated by right-click. We use <template slot-name="context-menu"> to inject menu choices into the menu. It automatically pops up at the location of the mouse pointer. The context menu items in the demo application are simply examples, each of which invokes a handler method. An important step of these handler methods is this:

this.$refs.filetree.contextMenuIsVisible = false;

This line of code instructs the file tree component to dismiss the menu.

vue-file-tree component

We've mentioned this component several times, and now it is time to investigate it.

The repository for the component has a whole build configuration and other scripts. See (github.com) https://github.com/robogeek/vue-file-tree for details. In this article we'll only investigate the .vue file.

To use this component in your application add the following to the dependencies section of the package.json in your application: "vue-file-tree": "github:robogeek/vue-file-tree",. Then follow the further directions in the README, or work it out from the code presented earlier.

The template for this component:


        <template slot="toggle" slot-scope="{ node }">
            <span v-if="!node.isLeaf">

        <template slot="title" slot-scope="{ node }">
                :icon="[ 'fab', 'js' ]" 
                v-if='node.data.type === "application/javascript"'></font-awesome-icon>
                v-else-if='node.data.type === "application/json"'></font-awesome-icon>
                v-else-if='node.data.type === "IMAGE"'></font-awesome-icon>
                v-else-if='node.data.type === "EJS"'></font-awesome-icon>
                :icon="[ 'fab', 'vuejs' ]" 
                v-else-if='node.data.type === "VUEJS"'></font-awesome-icon>
            {{ node.title }} </template>

        <template slot="sidebar" slot-scope="{ node }">

    <aside class="menu vue-file-tree-contextmenu" 
        <slot name="context-menu"></slot>


What we're doing is to implement a wrapper around yet another component, whose repository is: (github.com) https://github.com/holiber/sl-vue-tree

There are a number of event handlers and other configuration we've implemented to configure default behavior that makes sense as a tree component displaying files in the file system.

The sl-vue-tree component supports a number of slot's into which we can inject HTML. We've added these:

  • toggle This determines which arrow button to show for directory nodes which are expanded, versus ones which are closed.
  • title This describes one row of the tree component. We want to show an icon based on the file type. We have a means to select these codes to select the correct icon.
  • sidebar This describes an icon that appears at the far-right of the tree display.

We also have a slot of our own, context-menu, which is where the context menu HTML arrives. In this case we are using a Buefy menu component. The display of this component is controlled by the contextMenuIsVisible data item.

Obviously we are using the Font Awesome icon set.

Next, we add the <script> tag:


import path from 'path';
import util from 'util';
import splitter from './path-splitdirs';

import mime from 'mime';

import slVueTree from 'sl-vue-tree';
// import 'sl-vue-tree/dist/sl-vue-tree-dark.css';

import { library } from '@fortawesome/fontawesome-svg-core';
import {
  faCaretRight, faCaretDown, faTable, faImage, faFile, faCircle, faCode
} from '@fortawesome/free-solid-svg-icons';
import { faJs, faVuejs } from '@fortawesome/free-brands-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';

library.add(faJs, faVuejs, faCaretRight, faCaretDown, faTable, faImage, faFile, faCircle, faCode);

/* var nodes = [
    {title: 'Item1', isLeaf: true},
    {title: 'Item2', isLeaf: true, data: { visible: false }},
    {title: 'Folder1'},
      title: 'Folder2', isExpanded: true, children: [
        {title: 'Item3', isLeaf: true},
        {title: 'Item4', isLeaf: true, data: { isModified: true }}
]; */

export default {

    data() {
        return {
            nodes: [],
            contextMenuIsVisible: false
    components: {
        'sl-vue-tree': slVueTree,
        'font-awesome-icon': FontAwesomeIcon
    created() {
         * Derived from Buefy's b-dropdown
         * https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
        if (typeof window !== 'undefined') {
            document.addEventListener('click', this.clickedOutside)
    methods: {
        nodeClick(node, event) {
            this.$emit('nodeClick', event, node);
        nodeDoubleClick(node, event) {
            console.log(`nodeDoubleClick ${node.title} ${node.data.type} isLeaf ${node.isLeaf} ${util.inspect(node)}`);
            if (!node.isLeaf) {
                this.$refs.slvuetree.onToggleHandler(event, node);
            this.$emit('nodeDoubleClick', node);
        nodeSelect(node) {
            console.log(`nodeSelect ${util.inspect(node)}`);
        nodeToggle(node) {
            console.log(`nodeToggle ${util.inspect(node)}`);
        nodeDrop(node) {
            console.log(`nodeDrop ${util.inspect(node)}`);
            this.$emit('nodeDrop', node);
        nodeContextMenu(node, event) {
            console.log(`nodeContextMenu ${util.inspect(node)}`);
            this.contextMenuIsVisible = true;
            const $contextMenu = this.$refs.contextmenu;
            $contextMenu.style.left = event.clientX + 'px';
            $contextMenu.style.top = event.clientY + 'px';
         * Close dropdown if clicked outside.
         * Derived from Buefy's b-dropdown
         * https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
        clickedOutside(event) {
            if (!this.isInWhiteList(event.target)) this.contextMenuIsVisible = false;
        // If the "clickOutside" is on a target where we should ignore the click
        // then we should ignore this.  
        // See: https://github.com/buefy/buefy/blob/dev/src/components/dropdown/Dropdown.vue
        isInWhiteList(el) { return false; },
        onExternalDropHandler(cursorPosition, event) {
            console.log('external drop', cursorPosition, util.inspect(event));
        addPathToTree(fn, stat, isDir) {
            console.log(`addPathToTree ${fn} ${util.inspect(stat)} ${isDir}`);
            console.log(`addPathToTree ${util.inspect(process)}`);
            fn = path.normalize(fn);
            console.log(`addPathToTree NORMALIZED ${fn}`);
            const basenm = path.basename(fn);
            console.log(`addPathToTree BASENAME ${basenm}`);

            const split = splitter(fn);

            console.log(`addPathToTree dirs ${util.inspect(split)}`);
            let curnodes = this.nodes;
            for (let dir of split.dirs) {
                if (dir === '.') continue;
                let found = undefined;
                for (let cur of curnodes) {
                    if (cur.isLeaf === false && cur.title === dir) {
                        found = cur;
                if (!found) {
                    let newnode = {
                        title: dir, 
                        isLeaf: false, 
                        children: [], 
                        data: { 
                            type: "DIRECTORY",
                            pathname: fn, 
                    console.log(`addPathToTree !found push newnode ${util.inspect(newnode)}`);
                    curnodes = newnode.children;
                } else {
                    curnodes = found.children;
            let newnode = {
                title: basenm, 
                isLeaf: !isDir, 
                data: { 
                    type: mime.getType(fn),
                    pathname: fn,
            if (!newnode.data.type) newnode.data.type = "text/plain";
            if (newnode.data.type.startsWith('image/')) newnode.data.type = "IMAGE";
            if (fn.endsWith('.ejs')) newnode.data.type = "EJS";
            if (fn.endsWith('.vue')) newnode.data.type = "VUEJS";
            if (!newnode.isLeaf) newnode.children = [];
            console.log(`addPathToTree FINAL push newnode ${util.inspect(newnode)}`);


At the top of this we import various required modules. This includes the sl-vue-tree component, and some of the Font Awesome icons. Both of those become Vue.js components available to this component.

The sl-vue-tree component expects data items in a particular data structure. When in Bucharest do as the Romanians, so therefore we must give a correct data structure to that component. An example of the data structure is in a comment shown here.

The addPathToTree function receives the file and directory messages described earlier. It receives a simple pathname string plus a Stat object. The task here is to convert that information into the structure required by sl-vue-tree.

I ran into a problem developing this component. The requirement to create that directory structure is to split the pathname string into segments. To do this in a operating system independent manner requires careful considerations.

At first the path-directories module (https://github.com/jy95/path-directories) was used. It takes a pathname and reliably splits it into segments accounting for Windows or POSIX file path conventions. Unfortunately as written it runs on Node.js just fine, but this vue-file-tree component is packaged assuming it is running in a web browser. Where path-directories assumes a path module compatible with Node.js, the packaged Vue.js component receives a path module which presents a subset of Node.js functionality.

Therefore a replacement was coded:

const _path = require('path');
const normalize = require('normalize-path');
const parsePath = require('parse-filepath');

let isWin32 = false;
let isPosix = true;

module.exports = function(path2split) {
    path2split = normalize(path2split);
    if (path2split.match(/^[a-zA-Z]\:/)) { isWin32 = true; isPosix = false; }
    if (path2split.indexOf('\\') >= 0) { isWin32 = true; isPosix = false; }
    let path = isWin32 ? _path.win32 : _path.posix;
    if (!path) path = _path;
    let parsed = path.parse ? path.parse(path2split) : parsePath(path2split);
    if (parsed.root === '') parsed.root = '.';
    let dir = parsed.dir;
    let dirz = [];
    do {
        dir = path.dirname(dir);
    } while (dir !== parsed.root);
    parsed.dirs = dirz;
    return parsed;

This module handles pathname normalization and robustly splitting the path irregardless of file system conventions. It returns the object supplied by the path.parse function, augmented with a dirs attribute containing the directory portion of the pathname but split into an array.

What addPathToTree does with this is to add each segment of the file pathname into the data structure required by sl-vue-tree. Then Vue.js arranges for this structure to be handed to sl-vue-tree which causes the file system tree to diaplay.

The other methods handle various events going back and forth.

Finally we get to the <style> section:

.vue-file-tree-contextmenu {
    position: absolute;
    background-color: white;
    color: black;
    border-radius: 2px;
    cursor: pointer;

.vue-file-tree-contextmenu > div {
    padding: 10px;

.vue-file-tree-contextmenu > div:hover {
    background-color: rgba(100, 100, 255, 0.5);

#vue-file-tree {
    height: 100%;

.sl-vue-tree {
    position: relative;
    cursor: default;
    user-select: none;

.sl-vue-tree.sl-vue-tree-root {
    border: 1px solid rgb(9, 22, 29);
    background-color: rgb(9, 22, 29);
    color: rgba(255, 255, 255, 0.5);
    border-radius: 3px;

.sl-vue-tree-root > .sl-vue-tree-nodes-list {
    overflow: hidden;
    position: relative;
    padding-bottom: 4px;

.sl-vue-tree-selected > .sl-vue-tree-node-item {
    background-color: #13242d;
    color: white;

.sl-vue-tree-node-item.sl-vue-tree-cursor-hover {
    color: white;

.sl-vue-tree-node-item {
    position: relative;
    display: flex;
    flex-direction: row;

    padding-left: 10px;
    padding-right: 10px;
    line-height: 28px;
    border: 1px solid transparent;

.sl-vue-tree-node-item.sl-vue-tree-cursor-inside {
    border: 1px solid rgba(255, 255, 255, 0.5);

.sl-vue-tree-gap {
    width: 25px;
    min-height: 1px;


.sl-vue-tree-toggle {
    display: inline-block;
    text-align: left;
    width: 20px;

.sl-vue-tree-sidebar {
    margin-left: auto;

.sl-vue-tree-cursor {
    position: absolute;
    border: 1px solid rgba(255, 255, 255, 0.5);
    height: 1px;
    width: 100%;

.sl-vue-tree-drag-info {
    position: absolute;
    background-color: rgba(0,0,0,0.5);
    opacity: 0.5;
    margin-left: 20px;
    padding: 5px 10px;

These CSS declarations handle two tasks: a) the context menu, b) styling of the file tree component

Running the application

We now have everything lined up and ready to go. The electron-vue framework makes this easy:

$ npm run dev

> electron-vue-file-browse@0.0.0 dev /Volumes/Extra/akasha-tools/electron-vue-file-browse
> node .electron-vue/dev-runner.js

         ___                      __                                                                
   __   /\_ \       __     ___   /\ \__    _ __    ___     ___              __  __  __  __     __   
 / ,.`\ \//\ \    / ,.`\  /'___\ \ \ ,_\  /\` __\ / __`\ /' _ `\   _______ /\ \/\ \/\ \/\ \  / ,.`\ 
/\  __/   \_\ \_ /\  __/ /\ \__/  \ \ \/  \ \ \/ /\ \_\ \/\ \/\ \ /\______\\ \ \/ |\ \ \_\ \/\  __/ 
\ \____\  /\____\\ \____\\ \____\  \ \ \_  \ \_\ \ \____/\ \_\ \_\\/______/ \ \___/ \ \____/\ \____\
 \/____/  \/____/ \/____/ \/____/   \ \__\  \/_/  \/___/  \/_/\/_/           \/__/   \/___/  \/____/
  getting ready...

┏ Main Process ---------------


┗ ----------------------------

┏ Renderer Process -----------

  Hash: b470657ee609c4cee744
  Version: webpack 3.12.0
  Time: 8084ms
        Asset       Size  Chunks                    Chunk Names
  renderer.js    1.13 MB       0  [emitted]  [big]  renderer
   styles.css     304 kB       0  [emitted]  [big]  renderer
   index.html  508 bytes          [emitted]         
     [0] ./node_modules/vue/dist/vue.esm.js 292 kB {0} [built]
     [1] ./node_modules/vue-hot-reload-api/dist/index.js 6.24 kB {0} [built]
     [5] ./src/renderer/main.js 623 bytes {0} [built]
     [6] external "electron" 42 bytes {0} [not cacheable]
    [12] multi ./.electron-vue/dev-client ./src/renderer/main.js 40 bytes {0} [built]
    [13] ./.electron-vue/dev-client.js 731 bytes {0} [built]
    [14] (webpack)-hot-middleware/client.js?noInfo=true&reload=true 7.77 kB {0} [built]
    [16] external "querystring" 42 bytes {0} [not cacheable]
    [19] (webpack)-hot-middleware/client-overlay.js 2.21 kB {0} [built]
    [25] ./src/renderer/App.vue 1.84 kB {0} [built]
    [29] ./src/renderer/router/index.js 273 bytes {0} [built]
    [37] external "buefy" 42 bytes {0} [not cacheable]
    [38] ./node_modules/buefy/lib/buefy.css 41 bytes {0} [built]
    [39] external "vue-electron" 42 bytes {0} [not cacheable]
    [41] ./node_modules/style-loader/lib/addStyles.js 8.7 kB [built]
      + 28 hidden modules
  Child html-webpack-plugin for "index.html":
           Asset     Size  Chunks  Chunk Names
      index.html  1.45 MB       0  
         [0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.ejs 1.3 kB {0} [built]
         [1] ./node_modules/lodash/lodash.js 540 kB {0} [built]
         [2] (webpack)/buildin/module.js 517 bytes {0} [built]
  Child extract-text-webpack-plugin node_modules/extract-text-webpack-plugin/dist node_modules/css-loader/index.js!node_modules/buefy/lib/buefy.css:
         [0] ./node_modules/css-loader!./node_modules/buefy/lib/buefy.css 318 kB {0} [built]
         [1] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]

┗ ----------------------------

The framework supports hot reload while the app is running in dev mode.

While scanning the directory tree these messages are printed:

┏ Electron -------------------

  walkdir sending FILE /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/index.ejs
  walkdir sending DIR /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/main
  walkdir sending DIR /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/renderer
┗ ----------------------------

┏ Electron -------------------

  walkdir sending FILE /Volumes/Extra/akasha-tools/electron-vue-file-browse/src/main/index.dev.js
┗ ----------------------------

This is what the application looks like during execution.

About the Author(s)

(davidherron.com) David Herron : David Herron is a writer and software engineer focusing on the wise use of technology. He is especially interested in clean energy technologies like solar power, wind power, and electric cars. David worked for nearly 30 years in Silicon Valley on software ranging from electronic mail systems, to video streaming, to the Java programming language, and has published several books on Node.js programming and electric vehicles.

Books by David Herron