How to create an Electron app using Angular and SQLite3.
I was recently experimenting with converting one of my Angular web apps into a desktop application using Electron. I encountered a few hurdles along the way, and decided to put my experience in writing so that it may help others. If you have similar plans for your project, I hope this may be of use. The source code for this guide can be found here.
Part I: Angular
Create the Boilerplate.
For the sake of this guide, we will be creating a new Angular app from scratch. I will be using Electron-Forge to create the boilerplate. Electron-Forge offers several templates for creating boilerplate code, including one for Angular 2. First install the Electron-Forge CLI.
$ npm i -g electron-forge
Now use the CLI to create the Angular app boilerplate.
$ electron-forge init electron-angular-sqlite3 --template=angular2
$ cd electron-angular-sqlite3
The forge CLI will add the bare essentials we need to run our app. Let’s add a few additional directories to house our database files. Add an assets directory under src, and put data and model directories under it.
$ mkdir ./src/assets/data ./src/assets/model
The directory tree should now look like this:
.
+-node_modules
+-src
| |
| +-assets
| | |
| | +-data
| | +-model
| |
| +-app.component.ts
| +-bootstrap.ts
| +-index.html
| +-index.ts
|
+-.compilerc
+-.gitignore
+-package-lock.json
+-package.json
+-tsconfig.json
+-tslint.json
Write Some Code.
As our first step, let’s add a model file that we will match our database schema. For this simple example, let’s create a class called Item. Each item will contain an id and a name property. Save the file in your project at src/assets/model/item.schema.ts
.
We will be using TypeORM for our object relational mapping. First install TypeORM.
$ npm install typeorm --save
We will be following the TypeORM guide for creating schema here. When finished the file should look like this:
TypeORM makes use of typescript decorators. We use the Entity decorator to declare our Item class a table. The @PrimaryGeneratedColumn()
decorator declares id
as our unique identification and tells the database to automatically generate it. We will worry about linking to a database later on.
Create the Service.
Our next likely action would be to create an app service that handles communication from the front to the back end. Electron makes available the IpcRenderer
class for just this thing. IpcRenderer
is Electron’s inter process communication class that is used in the renderer process. Basically, we want to use the IpcRenderer
to send messages to Electron’s main process. These messages will pass information to the main process so that it can handle the database interactions.
Implementing the IpcRenderer
is where we come across our first hurdle. Electron is relying on the window.require() method, which is only available inside of Electron’s renderer process. This is a well documented issue. To get around this, we can use ThornstonHans’ ngx-electron package, which wraps all the Electron API’s exposed to the renderer process into a single Electron Service. You can read more about this here.
Before we can use ngx-electron
, we need to install it.
$ npm install ngx-electron --save
Now let’s create a service to handle our IpcRenderer
communication. Create src/app.service.ts
.
In app.service.ts
we create a class calledAppService
and add the @Injectable()
decorator. This allows us to use angular’s built in dependency injection (DI). In our constructor, we create a local variable _electronService
of type ElectronService
. The ElectronService
class is provided to us by ngrx-electron
. It allows us to use Electron’s IpcRender
class without any of the aforementioned issues.
We create three functions: one that get’s all Items in the database, one to add an Item to the database, and one to delete an Item. Each function will return an Observable.
Observables are part of the RxJs Library and provide a good way of handling our database interactions asynchronously. You can read more about Observables here. Note the use of the Observable operator of
to denote that we are wrapping our response from this._electronService.ipcRenderer.sendSync()
as an Observable value.
Registering Services and Writing Component.
With our service complete, let’s go into src/app.component.ts
and register it for DI. While in there, we will add a simple html template and functions to handle our button events.
Make sure to add AppService
as a provider in the @NgModule
decorator arguments and also as a private variable in the AppComponent
constructor. We also need to add ElectronService
as a provider.
On initialization of our component, we want to load all contents of our database and display it. To do this, we subscribe to the addItem()
function of the service we created. If you remember, all of our service functions return Observables. To get data from our observable, we subscribe to it, passing a callback function that runs when the data is received. In the example above, (items) => (this.itemList = items)
will populate our class variable itemList
with the contents of the database once it is retrieved.
We follow similar tactics for adding and deleting items from the database. Each time repopulating itemList
with the updated contents of the database.
Part II: Electron
Installing SQLite3.
Now that we finished up our front end, we need to create the Electron backend. The Electron backend will handle and process messages sent from the front and manage the sqlite3 database.
We will be using sqlite3 for our database and need to install it.
$ npm install sqlite3 --save
A hurdle I ran into while working with sqlite3 and Electron initially, was that sqlite’s native binaries need to be recompiled for use with Electron. Electron-Forge should take care of this for you. One thing to note, Electron-Forge will use node-gyp to compile the binaries. You may need to have it properly installed and configured prior to use, which includes installing Python. As of now, node-gyp uses python 2. If you have multiple versions on your machine, you must ensure that current build is using the proper one.
Connecting to the Database.
Now let’s open our src/index.ts
file and add some code to connect to the database. The two things we need to do are, connect to the database, and add functions to handle our requests from the renderer process. The finished file looks like this:
An in depth explanation of TypeORM and Electron is beyond the scope of this
guide, so I will only briefly discuss the above file. First we need to import the createConnection
class from the TypeORM library. We also need to import or Item schema.
As expected, the createConnection
class will create a connection to our database. We pass it a constructor with parameters such as type, database, and entities. Type is a string that describes what type of database we are using. Database is a string that points to the database location. Entities is where we tell TypeORM what schemas to expect. For our purpose: type is ‘sqlite’, Database is ‘./src/assets/data/database.sqlite’, and Entities is our imported Item class.
TypeORM allows you two options when working with database transactions: EntityManager and Repository. Both will give you access to functions for querying the database, without writing the SQL. We create a Repository object with the line itemRepo = connection.getRepository(Item)
. This gives us access to transaction methods for our Item table.
The last step is to create functions to handle the messages being sent from the IpcRenderer
. Each function will use the itemRepo
object we created to access the database. After successful completion of each transaction, the functions will pass the new state of the database back to the renderer.
Part III: Run it!
With everything complete, we can now run the app. Electron-Forge handles this process for us. All we need to do is run the command:
$ npm run start
If everything is correct, Electron will open your app and you can test it out.
Thanks for reading!