├── .gitignore ├── README.md ├── hybrid-app ├── README.md ├── index.html ├── index.js ├── package.json ├── preload.js ├── remote-web-app │ ├── index.html │ ├── js │ │ ├── app.js │ │ ├── controller.js │ │ ├── helpers.js │ │ ├── model.js │ │ ├── store.js │ │ ├── template.js │ │ └── view.js │ ├── package.json │ └── readme.md └── renderer.js ├── ipc-demo ├── index.html ├── main.js ├── non-blocking-work.js ├── package.json ├── renderer.js ├── show-dialog.js └── work.js ├── main-renderer-communication ├── index.html ├── index.js ├── package.json └── renderer.js ├── package.json ├── preload-scripts ├── index.html ├── index.js ├── package.json ├── preload.js ├── renderer.js └── webview.html ├── storing-data ├── index.html ├── index.js ├── package.json └── store.js └── storing-sensitive-data ├── index.html ├── index.js ├── package.json └── renderer.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron tutorials 2 | 3 | This is a collection of small Electron sample apps. To run it, clone the repo, `npm install`, `cd` to the example and `npm run start`. 4 | 5 | 6 | Be sure to also check out my Egghead.io course https://egghead.io/courses/build-a-desktop-application-with-electron! 7 | -------------------------------------------------------------------------------- /hybrid-app/README.md: -------------------------------------------------------------------------------- 1 | # Additions in the TodoMVC app for integrating with the Electron app 2 | `isElectron` function to feature detect if we're in the Electron app or plain old web app: 3 | https://github.com/ccnokes/electron-tutorials/blob/master/hybrid-app/remote-web-app/js/helpers.js#L51 4 | 5 | `markAllAsComplete` function that the Electron app calls via IPC when the user clicks the corresponding menu item 6 | https://github.com/ccnokes/electron-tutorials/blob/master/hybrid-app/remote-web-app/js/app.js#L19 7 | 8 | `setDockBadge` function that sets the dock badge count on macOS 9 | https://github.com/ccnokes/electron-tutorials/blob/master/hybrid-app/remote-web-app/js/controller.js#L232 10 | 11 | 12 | # Installing and running 13 | To run the Todo app from localhost: `cd remote-web-app` and `http-server`. 14 | 15 | Then `npm run start` to start the Electron app. 16 | 17 | You can verify that the web app works as normal by just going to `localhost:8080`. 18 | 19 | The todo app is included here locally just for convenience. In an actual app they'd be in separate locations. 20 | -------------------------------------------------------------------------------- /hybrid-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Todos 4 | 33 | 34 | 35 | 36 | 37 |
38 |

LOADING...

39 |
40 | 41 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /hybrid-app/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Menu, webContents } = require('electron'); 2 | const path = require('path'); 3 | let mainWindow; 4 | let webviewId; 5 | 6 | app.on('ready', () => { 7 | mainWindow = new BrowserWindow({ 8 | titleBarStyle: 'hidden-inset', 9 | frame: false 10 | }); 11 | mainWindow.loadURL(path.join('file://', __dirname, 'index.html')); 12 | mainWindow.openDevTools({ mode: 'bottom' }); 13 | 14 | createMenu(); 15 | }); 16 | 17 | // get the webview's webContents 18 | function getWebviewWebContents() { 19 | return webContents.getAllWebContents() 20 | // TODO replace `localhost` with whatever the remote web app's URL is 21 | .filter(wc => wc.getURL().search(/localhost/gi) > -1) 22 | .pop(); 23 | } 24 | 25 | function createMenu() { 26 | 27 | const topLevelItems = [ 28 | { 29 | label: 'Application', 30 | submenu: [ 31 | { 32 | label: 'Quit', 33 | accelerator: 'CmdOrCtrl+Q', 34 | click() { 35 | app.quit(); 36 | } 37 | } 38 | ] 39 | }, 40 | { 41 | label: 'Edit', 42 | submenu: [ 43 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, 44 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, 45 | { type: 'separator' }, 46 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, 47 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, 48 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, 49 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' } 50 | ] 51 | }, 52 | { 53 | label: 'Actions', 54 | submenu: [ 55 | { 56 | label: 'Mark All As Complete', 57 | click() { 58 | // send an IPC message to the webview for handling 59 | const wc = getWebviewWebContents(); 60 | wc.send('markAllComplete'); 61 | } 62 | } 63 | ] 64 | } 65 | ]; 66 | 67 | Menu.setApplicationMenu(Menu.buildFromTemplate(topLevelItems)); 68 | } 69 | -------------------------------------------------------------------------------- /hybrid-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hybridApp", 3 | "main": "index.js", 4 | "scripts": { 5 | "start": "electron index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /hybrid-app/preload.js: -------------------------------------------------------------------------------- 1 | // in preload scripts, we have access to node.js and electron APIs 2 | // the remote web app will not, so this is safe 3 | const { ipcRenderer: ipc, remote } = require('electron'); 4 | 5 | 6 | init(); 7 | 8 | 9 | function init() { 10 | attachIPCListeners(); 11 | 12 | // Expose a bridging API to remote app's window. 13 | // We'll add methods to it here first, and when the remote web app loads, 14 | // it'll add some additional methods as well. 15 | // 16 | // !CAREFUL! do not expose any functionality or APIs that could compromise the 17 | // user's computer. E.g. don't directly expose core Electron (even IPC) or node.js modules. 18 | window.Bridge = { 19 | setDockBadge 20 | }; 21 | } 22 | 23 | function attachIPCListeners() { 24 | // we get this message from the main process, and then tell the todo app to make it so 25 | // the todo app defines this function 26 | ipc.on('markAllComplete', () => { 27 | window.Bridge.markAllComplete(); 28 | }); 29 | } 30 | 31 | // the todo app calls this when the todo count changes 32 | function setDockBadge(count) { 33 | if(process.platform === 'darwin') { 34 | remote.app.dock.setBadge('' + (count || '')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VanillaJS • TodoMVC 6 | 7 | 8 | 9 | 10 |
11 |
12 |

todos

13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 | 35 |
36 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/js/app.js: -------------------------------------------------------------------------------- 1 | /*global app, $on, isElectron */ 2 | (function () { 3 | 'use strict'; 4 | 5 | /** 6 | * Sets up a brand new Todo list. 7 | * 8 | * @param {string} name The name of your new to do list. 9 | */ 10 | function Todo(name) { 11 | this.storage = new app.Store(name); 12 | this.model = new app.Model(this.storage); 13 | this.template = new app.Template(); 14 | this.view = new app.View(this.template); 15 | this.controller = new app.Controller(this.model, this.view); 16 | 17 | // *ADDITION* we expose this method here to Electron 18 | if(isElectron()) { 19 | window.Bridge.markAllComplete = () => this.controller.toggleAll(true); 20 | } 21 | } 22 | 23 | var todo = new Todo('todos-vanillajs'); 24 | 25 | function setView() { 26 | todo.controller.setView(document.location.hash); 27 | } 28 | $on(window, 'load', setView); 29 | $on(window, 'hashchange', setView); 30 | })(); 31 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/js/controller.js: -------------------------------------------------------------------------------- 1 | /*global isElectron */ 2 | (function (window) { 3 | 'use strict'; 4 | 5 | /** 6 | * Takes a model and view and acts as the controller between them 7 | * 8 | * @constructor 9 | * @param {object} model The model instance 10 | * @param {object} view The view instance 11 | */ 12 | function Controller(model, view) { 13 | var self = this; 14 | self.model = model; 15 | self.view = view; 16 | 17 | self.view.bind('newTodo', function (title) { 18 | self.addItem(title); 19 | }); 20 | 21 | self.view.bind('itemEdit', function (item) { 22 | self.editItem(item.id); 23 | }); 24 | 25 | self.view.bind('itemEditDone', function (item) { 26 | self.editItemSave(item.id, item.title); 27 | }); 28 | 29 | self.view.bind('itemEditCancel', function (item) { 30 | self.editItemCancel(item.id); 31 | }); 32 | 33 | self.view.bind('itemRemove', function (item) { 34 | self.removeItem(item.id); 35 | }); 36 | 37 | self.view.bind('itemToggle', function (item) { 38 | self.toggleComplete(item.id, item.completed); 39 | }); 40 | 41 | self.view.bind('removeCompleted', function () { 42 | self.removeCompletedItems(); 43 | }); 44 | 45 | self.view.bind('toggleAll', function (status) { 46 | self.toggleAll(status.completed); 47 | }); 48 | } 49 | 50 | /** 51 | * Loads and initialises the view 52 | * 53 | * @param {string} '' | 'active' | 'completed' 54 | */ 55 | Controller.prototype.setView = function (locationHash) { 56 | var route = locationHash.split('/')[1]; 57 | var page = route || ''; 58 | this._updateFilterState(page); 59 | }; 60 | 61 | /** 62 | * An event to fire on load. Will get all items and display them in the 63 | * todo-list 64 | */ 65 | Controller.prototype.showAll = function () { 66 | var self = this; 67 | self.model.read(function (data) { 68 | self.view.render('showEntries', data); 69 | }); 70 | }; 71 | 72 | /** 73 | * Renders all active tasks 74 | */ 75 | Controller.prototype.showActive = function () { 76 | var self = this; 77 | self.model.read({ completed: false }, function (data) { 78 | self.view.render('showEntries', data); 79 | }); 80 | }; 81 | 82 | /** 83 | * Renders all completed tasks 84 | */ 85 | Controller.prototype.showCompleted = function () { 86 | var self = this; 87 | self.model.read({ completed: true }, function (data) { 88 | self.view.render('showEntries', data); 89 | }); 90 | }; 91 | 92 | /** 93 | * An event to fire whenever you want to add an item. Simply pass in the event 94 | * object and it'll handle the DOM insertion and saving of the new item. 95 | */ 96 | Controller.prototype.addItem = function (title) { 97 | var self = this; 98 | 99 | if (title.trim() === '') { 100 | return; 101 | } 102 | 103 | self.model.create(title, function () { 104 | self.view.render('clearNewTodo'); 105 | self._filter(true); 106 | }); 107 | }; 108 | 109 | /* 110 | * Triggers the item editing mode. 111 | */ 112 | Controller.prototype.editItem = function (id) { 113 | var self = this; 114 | self.model.read(id, function (data) { 115 | self.view.render('editItem', {id: id, title: data[0].title}); 116 | }); 117 | }; 118 | 119 | /* 120 | * Finishes the item editing mode successfully. 121 | */ 122 | Controller.prototype.editItemSave = function (id, title) { 123 | var self = this; 124 | title = title.trim(); 125 | 126 | if (title.length !== 0) { 127 | self.model.update(id, {title: title}, function () { 128 | self.view.render('editItemDone', {id: id, title: title}); 129 | }); 130 | } else { 131 | self.removeItem(id); 132 | } 133 | }; 134 | 135 | /* 136 | * Cancels the item editing mode. 137 | */ 138 | Controller.prototype.editItemCancel = function (id) { 139 | var self = this; 140 | self.model.read(id, function (data) { 141 | self.view.render('editItemDone', {id: id, title: data[0].title}); 142 | }); 143 | }; 144 | 145 | /** 146 | * By giving it an ID it'll find the DOM element matching that ID, 147 | * remove it from the DOM and also remove it from storage. 148 | * 149 | * @param {number} id The ID of the item to remove from the DOM and 150 | * storage 151 | */ 152 | Controller.prototype.removeItem = function (id) { 153 | var self = this; 154 | self.model.remove(id, function () { 155 | self.view.render('removeItem', id); 156 | }); 157 | 158 | self._filter(); 159 | }; 160 | 161 | /** 162 | * Will remove all completed items from the DOM and storage. 163 | */ 164 | Controller.prototype.removeCompletedItems = function () { 165 | var self = this; 166 | self.model.read({ completed: true }, function (data) { 167 | data.forEach(function (item) { 168 | self.removeItem(item.id); 169 | }); 170 | }); 171 | 172 | self._filter(); 173 | }; 174 | 175 | /** 176 | * Give it an ID of a model and a checkbox and it will update the item 177 | * in storage based on the checkbox's state. 178 | * 179 | * @param {number} id The ID of the element to complete or uncomplete 180 | * @param {object} checkbox The checkbox to check the state of complete 181 | * or not 182 | * @param {boolean|undefined} silent Prevent re-filtering the todo items 183 | */ 184 | Controller.prototype.toggleComplete = function (id, completed, silent) { 185 | var self = this; 186 | self.model.update(id, { completed: completed }, function () { 187 | self.view.render('elementComplete', { 188 | id: id, 189 | completed: completed 190 | }); 191 | }); 192 | 193 | if (!silent) { 194 | self._filter(); 195 | } 196 | }; 197 | 198 | /** 199 | * Will toggle ALL checkboxes' on/off state and completeness of models. 200 | * Just pass in the event object. 201 | */ 202 | Controller.prototype.toggleAll = function (completed) { 203 | var self = this; 204 | self.model.read({ completed: !completed }, function (data) { 205 | data.forEach(function (item) { 206 | self.toggleComplete(item.id, completed, true); 207 | }); 208 | }); 209 | 210 | self._filter(); 211 | }; 212 | 213 | /** 214 | * Updates the pieces of the page which change depending on the remaining 215 | * number of todos. 216 | */ 217 | Controller.prototype._updateCount = function () { 218 | var self = this; 219 | self.model.getCount(function (todos) { 220 | self.view.render('updateElementCount', todos.active); 221 | self.view.render('clearCompletedButton', { 222 | completed: todos.completed, 223 | visible: todos.completed > 0 224 | }); 225 | 226 | self.view.render('toggleAll', {checked: todos.completed === todos.total}); 227 | self.view.render('contentBlockVisibility', {visible: todos.total > 0}); 228 | 229 | // *ADDITION* 230 | // if we're in Electron, we also want this to update dock badge count 231 | if(isElectron()) { 232 | window.Bridge.setDockBadge(todos.active); 233 | } 234 | }); 235 | }; 236 | 237 | /** 238 | * Re-filters the todo items, based on the active route. 239 | * @param {boolean|undefined} force forces a re-painting of todo items. 240 | */ 241 | Controller.prototype._filter = function (force) { 242 | var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1); 243 | 244 | // Update the elements on the page, which change with each completed todo 245 | this._updateCount(); 246 | 247 | // If the last active route isn't "All", or we're switching routes, we 248 | // re-create the todo item elements, calling: 249 | // this.show[All|Active|Completed](); 250 | if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) { 251 | this['show' + activeRoute](); 252 | } 253 | 254 | this._lastActiveRoute = activeRoute; 255 | }; 256 | 257 | /** 258 | * Simply updates the filter nav's selected states 259 | */ 260 | Controller.prototype._updateFilterState = function (currentPage) { 261 | // Store a reference to the active route, allowing us to re-filter todo 262 | // items as they are marked complete or incomplete. 263 | this._activeRoute = currentPage; 264 | 265 | if (currentPage === '') { 266 | this._activeRoute = 'All'; 267 | } 268 | 269 | this._filter(); 270 | 271 | this.view.render('setFilter', currentPage); 272 | }; 273 | 274 | // Export to window 275 | window.app = window.app || {}; 276 | window.app.Controller = Controller; 277 | })(window); 278 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/js/helpers.js: -------------------------------------------------------------------------------- 1 | /*global NodeList */ 2 | (function (window) { 3 | 'use strict'; 4 | 5 | // Get element(s) by CSS selector: 6 | window.qs = function (selector, scope) { 7 | return (scope || document).querySelector(selector); 8 | }; 9 | window.qsa = function (selector, scope) { 10 | return (scope || document).querySelectorAll(selector); 11 | }; 12 | 13 | // addEventListener wrapper: 14 | window.$on = function (target, type, callback, useCapture) { 15 | target.addEventListener(type, callback, !!useCapture); 16 | }; 17 | 18 | // Attach a handler to event for all elements that match the selector, 19 | // now or in the future, based on a root element 20 | window.$delegate = function (target, selector, type, handler) { 21 | function dispatchEvent(event) { 22 | var targetElement = event.target; 23 | var potentialElements = window.qsa(selector, target); 24 | var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0; 25 | 26 | if (hasMatch) { 27 | handler.call(targetElement, event); 28 | } 29 | } 30 | 31 | // https://developer.mozilla.org/en-US/docs/Web/Events/blur 32 | var useCapture = type === 'blur' || type === 'focus'; 33 | 34 | window.$on(target, type, dispatchEvent, useCapture); 35 | }; 36 | 37 | // Find the element's parent with the given tag name: 38 | // $parent(qs('a'), 'div'); 39 | window.$parent = function (element, tagName) { 40 | if (!element.parentNode) { 41 | return; 42 | } 43 | if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) { 44 | return element.parentNode; 45 | } 46 | return window.$parent(element.parentNode, tagName); 47 | }; 48 | 49 | // *ADDITION* 50 | // Just check for the prescence of the global that our Electron app sets 51 | window.isElectron = function() { 52 | return 'Bridge' in window; 53 | }; 54 | 55 | // Allow for looping on nodes by chaining: 56 | // qsa('.foo').forEach(function () {}) 57 | NodeList.prototype.forEach = Array.prototype.forEach; 58 | })(window); 59 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/js/model.js: -------------------------------------------------------------------------------- 1 | (function (window) { 2 | 'use strict'; 3 | 4 | /** 5 | * Creates a new Model instance and hooks up the storage. 6 | * 7 | * @constructor 8 | * @param {object} storage A reference to the client side storage class 9 | */ 10 | function Model(storage) { 11 | this.storage = storage; 12 | } 13 | 14 | /** 15 | * Creates a new todo model 16 | * 17 | * @param {string} [title] The title of the task 18 | * @param {function} [callback] The callback to fire after the model is created 19 | */ 20 | Model.prototype.create = function (title, callback) { 21 | title = title || ''; 22 | callback = callback || function () {}; 23 | 24 | var newItem = { 25 | title: title.trim(), 26 | completed: false 27 | }; 28 | 29 | this.storage.save(newItem, callback); 30 | }; 31 | 32 | /** 33 | * Finds and returns a model in storage. If no query is given it'll simply 34 | * return everything. If you pass in a string or number it'll look that up as 35 | * the ID of the model to find. Lastly, you can pass it an object to match 36 | * against. 37 | * 38 | * @param {string|number|object} [query] A query to match models against 39 | * @param {function} [callback] The callback to fire after the model is found 40 | * 41 | * @example 42 | * model.read(1, func); // Will find the model with an ID of 1 43 | * model.read('1'); // Same as above 44 | * //Below will find a model with foo equalling bar and hello equalling world. 45 | * model.read({ foo: 'bar', hello: 'world' }); 46 | */ 47 | Model.prototype.read = function (query, callback) { 48 | var queryType = typeof query; 49 | callback = callback || function () {}; 50 | 51 | if (queryType === 'function') { 52 | callback = query; 53 | return this.storage.findAll(callback); 54 | } else if (queryType === 'string' || queryType === 'number') { 55 | query = parseInt(query, 10); 56 | this.storage.find({ id: query }, callback); 57 | } else { 58 | this.storage.find(query, callback); 59 | } 60 | }; 61 | 62 | /** 63 | * Updates a model by giving it an ID, data to update, and a callback to fire when 64 | * the update is complete. 65 | * 66 | * @param {number} id The id of the model to update 67 | * @param {object} data The properties to update and their new value 68 | * @param {function} callback The callback to fire when the update is complete. 69 | */ 70 | Model.prototype.update = function (id, data, callback) { 71 | this.storage.save(data, callback, id); 72 | }; 73 | 74 | /** 75 | * Removes a model from storage 76 | * 77 | * @param {number} id The ID of the model to remove 78 | * @param {function} callback The callback to fire when the removal is complete. 79 | */ 80 | Model.prototype.remove = function (id, callback) { 81 | this.storage.remove(id, callback); 82 | }; 83 | 84 | /** 85 | * WARNING: Will remove ALL data from storage. 86 | * 87 | * @param {function} callback The callback to fire when the storage is wiped. 88 | */ 89 | Model.prototype.removeAll = function (callback) { 90 | this.storage.drop(callback); 91 | }; 92 | 93 | /** 94 | * Returns a count of all todos 95 | */ 96 | Model.prototype.getCount = function (callback) { 97 | var todos = { 98 | active: 0, 99 | completed: 0, 100 | total: 0 101 | }; 102 | 103 | this.storage.findAll(function (data) { 104 | data.forEach(function (todo) { 105 | if (todo.completed) { 106 | todos.completed++; 107 | } else { 108 | todos.active++; 109 | } 110 | 111 | todos.total++; 112 | }); 113 | callback(todos); 114 | }); 115 | }; 116 | 117 | // Export to window 118 | window.app = window.app || {}; 119 | window.app.Model = Model; 120 | })(window); 121 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/js/store.js: -------------------------------------------------------------------------------- 1 | /*jshint eqeqeq:false */ 2 | (function (window) { 3 | 'use strict'; 4 | 5 | /** 6 | * Creates a new client side storage object and will create an empty 7 | * collection if no collection already exists. 8 | * 9 | * @param {string} name The name of our DB we want to use 10 | * @param {function} callback Our fake DB uses callbacks because in 11 | * real life you probably would be making AJAX calls 12 | */ 13 | function Store(name, callback) { 14 | callback = callback || function () {}; 15 | 16 | this._dbName = name; 17 | 18 | if (!localStorage[name]) { 19 | var data = { 20 | todos: [] 21 | }; 22 | 23 | localStorage[name] = JSON.stringify(data); 24 | } 25 | 26 | callback.call(this, JSON.parse(localStorage[name])); 27 | } 28 | 29 | /** 30 | * Finds items based on a query given as a JS object 31 | * 32 | * @param {object} query The query to match against (i.e. {foo: 'bar'}) 33 | * @param {function} callback The callback to fire when the query has 34 | * completed running 35 | * 36 | * @example 37 | * db.find({foo: 'bar', hello: 'world'}, function (data) { 38 | * // data will return any items that have foo: bar and 39 | * // hello: world in their properties 40 | * }); 41 | */ 42 | Store.prototype.find = function (query, callback) { 43 | if (!callback) { 44 | return; 45 | } 46 | 47 | var todos = JSON.parse(localStorage[this._dbName]).todos; 48 | 49 | callback.call(this, todos.filter(function (todo) { 50 | for (var q in query) { 51 | if (query[q] !== todo[q]) { 52 | return false; 53 | } 54 | } 55 | return true; 56 | })); 57 | }; 58 | 59 | /** 60 | * Will retrieve all data from the collection 61 | * 62 | * @param {function} callback The callback to fire upon retrieving data 63 | */ 64 | Store.prototype.findAll = function (callback) { 65 | callback = callback || function () {}; 66 | callback.call(this, JSON.parse(localStorage[this._dbName]).todos); 67 | }; 68 | 69 | /** 70 | * Will save the given data to the DB. If no item exists it will create a new 71 | * item, otherwise it'll simply update an existing item's properties 72 | * 73 | * @param {object} updateData The data to save back into the DB 74 | * @param {function} callback The callback to fire after saving 75 | * @param {number} id An optional param to enter an ID of an item to update 76 | */ 77 | Store.prototype.save = function (updateData, callback, id) { 78 | var data = JSON.parse(localStorage[this._dbName]); 79 | var todos = data.todos; 80 | 81 | callback = callback || function () {}; 82 | 83 | // If an ID was actually given, find the item and update each property 84 | if (id) { 85 | for (var i = 0; i < todos.length; i++) { 86 | if (todos[i].id === id) { 87 | for (var key in updateData) { 88 | todos[i][key] = updateData[key]; 89 | } 90 | break; 91 | } 92 | } 93 | 94 | localStorage[this._dbName] = JSON.stringify(data); 95 | callback.call(this, todos); 96 | } else { 97 | // Generate an ID 98 | updateData.id = new Date().getTime(); 99 | 100 | todos.push(updateData); 101 | localStorage[this._dbName] = JSON.stringify(data); 102 | callback.call(this, [updateData]); 103 | } 104 | }; 105 | 106 | /** 107 | * Will remove an item from the Store based on its ID 108 | * 109 | * @param {number} id The ID of the item you want to remove 110 | * @param {function} callback The callback to fire after saving 111 | */ 112 | Store.prototype.remove = function (id, callback) { 113 | var data = JSON.parse(localStorage[this._dbName]); 114 | var todos = data.todos; 115 | 116 | for (var i = 0; i < todos.length; i++) { 117 | if (todos[i].id == id) { 118 | todos.splice(i, 1); 119 | break; 120 | } 121 | } 122 | 123 | localStorage[this._dbName] = JSON.stringify(data); 124 | callback.call(this, todos); 125 | }; 126 | 127 | /** 128 | * Will drop all storage and start fresh 129 | * 130 | * @param {function} callback The callback to fire after dropping the data 131 | */ 132 | Store.prototype.drop = function (callback) { 133 | var data = {todos: []}; 134 | localStorage[this._dbName] = JSON.stringify(data); 135 | callback.call(this, data.todos); 136 | }; 137 | 138 | // Export to window 139 | window.app = window.app || {}; 140 | window.app.Store = Store; 141 | })(window); 142 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/js/template.js: -------------------------------------------------------------------------------- 1 | /*jshint laxbreak:true */ 2 | (function (window) { 3 | 'use strict'; 4 | 5 | var htmlEscapes = { 6 | '&': '&', 7 | '<': '<', 8 | '>': '>', 9 | '"': '"', 10 | '\'': ''', 11 | '`': '`' 12 | }; 13 | 14 | var escapeHtmlChar = function (chr) { 15 | return htmlEscapes[chr]; 16 | }; 17 | 18 | var reUnescapedHtml = /[&<>"'`]/g; 19 | var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source); 20 | 21 | var escape = function (string) { 22 | return (string && reHasUnescapedHtml.test(string)) 23 | ? string.replace(reUnescapedHtml, escapeHtmlChar) 24 | : string; 25 | }; 26 | 27 | /** 28 | * Sets up defaults for all the Template methods such as a default template 29 | * 30 | * @constructor 31 | */ 32 | function Template() { 33 | this.defaultTemplate 34 | = '
  • ' 35 | + '
    ' 36 | + '' 37 | + '' 38 | + '' 39 | + '
    ' 40 | + '
  • '; 41 | } 42 | 43 | /** 44 | * Creates an
  • HTML string and returns it for placement in your app. 45 | * 46 | * NOTE: In real life you should be using a templating engine such as Mustache 47 | * or Handlebars, however, this is a vanilla JS example. 48 | * 49 | * @param {object} data The object containing keys you want to find in the 50 | * template to replace. 51 | * @returns {string} HTML String of an
  • element 52 | * 53 | * @example 54 | * view.show({ 55 | * id: 1, 56 | * title: "Hello World", 57 | * completed: 0, 58 | * }); 59 | */ 60 | Template.prototype.show = function (data) { 61 | var i, l; 62 | var view = ''; 63 | 64 | for (i = 0, l = data.length; i < l; i++) { 65 | var template = this.defaultTemplate; 66 | var completed = ''; 67 | var checked = ''; 68 | 69 | if (data[i].completed) { 70 | completed = 'completed'; 71 | checked = 'checked'; 72 | } 73 | 74 | template = template.replace('{{id}}', data[i].id); 75 | template = template.replace('{{title}}', escape(data[i].title)); 76 | template = template.replace('{{completed}}', completed); 77 | template = template.replace('{{checked}}', checked); 78 | 79 | view = view + template; 80 | } 81 | 82 | return view; 83 | }; 84 | 85 | /** 86 | * Displays a counter of how many to dos are left to complete 87 | * 88 | * @param {number} activeTodos The number of active todos. 89 | * @returns {string} String containing the count 90 | */ 91 | Template.prototype.itemCounter = function (activeTodos) { 92 | var plural = activeTodos === 1 ? '' : 's'; 93 | 94 | return '' + activeTodos + ' item' + plural + ' left'; 95 | }; 96 | 97 | /** 98 | * Updates the text within the "Clear completed" button 99 | * 100 | * @param {[type]} completedTodos The number of completed todos. 101 | * @returns {string} String containing the count 102 | */ 103 | Template.prototype.clearCompletedButton = function (completedTodos) { 104 | if (completedTodos > 0) { 105 | return 'Clear completed'; 106 | } else { 107 | return ''; 108 | } 109 | }; 110 | 111 | // Export to window 112 | window.app = window.app || {}; 113 | window.app.Template = Template; 114 | })(window); 115 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/js/view.js: -------------------------------------------------------------------------------- 1 | /*global qs, qsa, $on, $parent, $delegate */ 2 | 3 | (function (window) { 4 | 'use strict'; 5 | 6 | /** 7 | * View that abstracts away the browser's DOM completely. 8 | * It has two simple entry points: 9 | * 10 | * - bind(eventName, handler) 11 | * Takes a todo application event and registers the handler 12 | * - render(command, parameterObject) 13 | * Renders the given command with the options 14 | */ 15 | function View(template) { 16 | this.template = template; 17 | 18 | this.ENTER_KEY = 13; 19 | this.ESCAPE_KEY = 27; 20 | 21 | this.$todoList = qs('.todo-list'); 22 | this.$todoItemCounter = qs('.todo-count'); 23 | this.$clearCompleted = qs('.clear-completed'); 24 | this.$main = qs('.main'); 25 | this.$footer = qs('.footer'); 26 | this.$toggleAll = qs('.toggle-all'); 27 | this.$newTodo = qs('.new-todo'); 28 | } 29 | 30 | View.prototype._removeItem = function (id) { 31 | var elem = qs('[data-id="' + id + '"]'); 32 | 33 | if (elem) { 34 | this.$todoList.removeChild(elem); 35 | } 36 | }; 37 | 38 | View.prototype._clearCompletedButton = function (completedCount, visible) { 39 | this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount); 40 | this.$clearCompleted.style.display = visible ? 'block' : 'none'; 41 | }; 42 | 43 | View.prototype._setFilter = function (currentPage) { 44 | qs('.filters .selected').className = ''; 45 | qs('.filters [href="#/' + currentPage + '"]').className = 'selected'; 46 | }; 47 | 48 | View.prototype._elementComplete = function (id, completed) { 49 | var listItem = qs('[data-id="' + id + '"]'); 50 | 51 | if (!listItem) { 52 | return; 53 | } 54 | 55 | listItem.className = completed ? 'completed' : ''; 56 | 57 | // In case it was toggled from an event and not by clicking the checkbox 58 | qs('input', listItem).checked = completed; 59 | }; 60 | 61 | View.prototype._editItem = function (id, title) { 62 | var listItem = qs('[data-id="' + id + '"]'); 63 | 64 | if (!listItem) { 65 | return; 66 | } 67 | 68 | listItem.className = listItem.className + ' editing'; 69 | 70 | var input = document.createElement('input'); 71 | input.className = 'edit'; 72 | 73 | listItem.appendChild(input); 74 | input.focus(); 75 | input.value = title; 76 | }; 77 | 78 | View.prototype._editItemDone = function (id, title) { 79 | var listItem = qs('[data-id="' + id + '"]'); 80 | 81 | if (!listItem) { 82 | return; 83 | } 84 | 85 | var input = qs('input.edit', listItem); 86 | listItem.removeChild(input); 87 | 88 | listItem.className = listItem.className.replace('editing', ''); 89 | 90 | qsa('label', listItem).forEach(function (label) { 91 | label.textContent = title; 92 | }); 93 | }; 94 | 95 | View.prototype.render = function (viewCmd, parameter) { 96 | var self = this; 97 | var viewCommands = { 98 | showEntries: function () { 99 | self.$todoList.innerHTML = self.template.show(parameter); 100 | }, 101 | removeItem: function () { 102 | self._removeItem(parameter); 103 | }, 104 | updateElementCount: function () { 105 | self.$todoItemCounter.innerHTML = self.template.itemCounter(parameter); 106 | }, 107 | clearCompletedButton: function () { 108 | self._clearCompletedButton(parameter.completed, parameter.visible); 109 | }, 110 | contentBlockVisibility: function () { 111 | self.$main.style.display = self.$footer.style.display = parameter.visible ? 'block' : 'none'; 112 | }, 113 | toggleAll: function () { 114 | self.$toggleAll.checked = parameter.checked; 115 | }, 116 | setFilter: function () { 117 | self._setFilter(parameter); 118 | }, 119 | clearNewTodo: function () { 120 | self.$newTodo.value = ''; 121 | }, 122 | elementComplete: function () { 123 | self._elementComplete(parameter.id, parameter.completed); 124 | }, 125 | editItem: function () { 126 | self._editItem(parameter.id, parameter.title); 127 | }, 128 | editItemDone: function () { 129 | self._editItemDone(parameter.id, parameter.title); 130 | } 131 | }; 132 | 133 | viewCommands[viewCmd](); 134 | }; 135 | 136 | View.prototype._itemId = function (element) { 137 | var li = $parent(element, 'li'); 138 | return parseInt(li.dataset.id, 10); 139 | }; 140 | 141 | View.prototype._bindItemEditDone = function (handler) { 142 | var self = this; 143 | $delegate(self.$todoList, 'li .edit', 'blur', function () { 144 | if (!this.dataset.iscanceled) { 145 | handler({ 146 | id: self._itemId(this), 147 | title: this.value 148 | }); 149 | } 150 | }); 151 | 152 | $delegate(self.$todoList, 'li .edit', 'keypress', function (event) { 153 | if (event.keyCode === self.ENTER_KEY) { 154 | // Remove the cursor from the input when you hit enter just like if it 155 | // were a real form 156 | this.blur(); 157 | } 158 | }); 159 | }; 160 | 161 | View.prototype._bindItemEditCancel = function (handler) { 162 | var self = this; 163 | $delegate(self.$todoList, 'li .edit', 'keyup', function (event) { 164 | if (event.keyCode === self.ESCAPE_KEY) { 165 | this.dataset.iscanceled = true; 166 | this.blur(); 167 | 168 | handler({id: self._itemId(this)}); 169 | } 170 | }); 171 | }; 172 | 173 | View.prototype.bind = function (event, handler) { 174 | var self = this; 175 | if (event === 'newTodo') { 176 | $on(self.$newTodo, 'change', function () { 177 | handler(self.$newTodo.value); 178 | }); 179 | 180 | } else if (event === 'removeCompleted') { 181 | $on(self.$clearCompleted, 'click', function () { 182 | handler(); 183 | }); 184 | 185 | } else if (event === 'toggleAll') { 186 | $on(self.$toggleAll, 'click', function () { 187 | handler({completed: this.checked}); 188 | }); 189 | 190 | } else if (event === 'itemEdit') { 191 | $delegate(self.$todoList, 'li label', 'dblclick', function () { 192 | handler({id: self._itemId(this)}); 193 | }); 194 | 195 | } else if (event === 'itemRemove') { 196 | $delegate(self.$todoList, '.destroy', 'click', function () { 197 | handler({id: self._itemId(this)}); 198 | }); 199 | 200 | } else if (event === 'itemToggle') { 201 | $delegate(self.$todoList, '.toggle', 'click', function () { 202 | handler({ 203 | id: self._itemId(this), 204 | completed: this.checked 205 | }); 206 | }); 207 | 208 | } else if (event === 'itemEditDone') { 209 | self._bindItemEditDone(handler); 210 | 211 | } else if (event === 'itemEditCancel') { 212 | self._bindItemEditCancel(handler); 213 | } 214 | }; 215 | 216 | // Export to window 217 | window.app = window.app || {}; 218 | window.app.View = View; 219 | }(window)); 220 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "todomvc-common": "^1.0.1", 5 | "todomvc-app-css": "^2.0.1" 6 | }, 7 | "devDependencies": { 8 | "jasmine-core": "^2.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hybrid-app/remote-web-app/readme.md: -------------------------------------------------------------------------------- 1 | # Vanilla JavaScript TodoMVC Example 2 | 3 | > JavaScript® (often shortened to JS) is a lightweight, interpreted, object-oriented language with first-class functions, most known as the scripting language for Web pages, but used in many non-browser environments as well such as node.js or Apache CouchDB. 4 | 5 | > _[JavaScript - developer.mozilla.org](http://developer.mozilla.org/en-US/docs/JavaScript) 6 | -------------------------------------------------------------------------------- /hybrid-app/renderer.js: -------------------------------------------------------------------------------- 1 | 2 | const $webview = document.querySelector('webview'); 3 | const $loader = document.querySelector('.loader'); 4 | let isInitialLoad = true; 5 | 6 | $webview.addEventListener('did-start-loading', () => { 7 | // we use client side rendering so the loader is only needed on the first page load 8 | if(isInitialLoad) { 9 | $webview.classList.add('hide'); 10 | $loader.classList.remove('loader-hide'); 11 | isInitialLoad = false; 12 | } 13 | }); 14 | 15 | $webview.addEventListener('dom-ready', () => { 16 | $webview.classList.remove('hide'); 17 | // have to delay in order for the webview show/resize to settle 18 | setTimeout(() => { 19 | $loader.classList.add('loader-hide'); 20 | }, 100); 21 | }); 22 | 23 | // this is just for development convenience 24 | // (because the todo app's dev tools are in a separate process) 25 | window.openWebviewDevTools = () => { 26 | $webview.getWebContents().openDevTools(); 27 | }; 28 | -------------------------------------------------------------------------------- /ipc-demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ipc-demo/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain } = require('electron'); 2 | const path = require('path'); 3 | const { showDialog } = require('./show-dialog'); 4 | let mainWindow; 5 | 6 | app.on('ready', () => { 7 | 8 | let mainWindow = new BrowserWindow({ 9 | height: 1400, 10 | width: 1000, 11 | title: 'Renderer Process' 12 | }); 13 | 14 | mainWindow.loadURL('file://' + path.join(__dirname, 'index.html')); 15 | mainWindow.openDevTools({mode: 'bottom'}); 16 | 17 | 18 | 19 | 20 | ipcMain.on('sendMainMessage', (event, props) => { 21 | showDialog(props.greeting); 22 | 23 | // sending a message back is a little different 24 | mainWindow.webContents.send('sendRendererMessage', { result: true }); 25 | }); 26 | 27 | 28 | 29 | ipcMain.on('doBlockingWork', () => { 30 | const work = require('./work'); 31 | work(); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /ipc-demo/non-blocking-work.js: -------------------------------------------------------------------------------- 1 | // This works in the either the main or renderer processes. 2 | 3 | const { requireTaskPool } = require('electron-remote'); 4 | const work = requireTaskPool(require.resolve('./work')); 5 | const _ = require('lodash'); 6 | 7 | function doWork() { 8 | console.log('start work'); 9 | 10 | // `work` will get executed concurrently in separate background processes 11 | // and resolve with a promise 12 | _.times(25, () => { 13 | work().then(result => { 14 | console.log(`work done in ${result} ms`); 15 | }); 16 | }); 17 | } 18 | 19 | module.exports = doWork; 20 | -------------------------------------------------------------------------------- /ipc-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ipc-demo", 3 | "main": "main.js", 4 | "scripts": { 5 | "start": "electron main.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ipc-demo/renderer.js: -------------------------------------------------------------------------------- 1 | require('devtron').install(); 2 | const { ipcRenderer, remote } = require('electron'); 3 | 4 | 5 | // IPC 6 | document.querySelector('#sendMsgMain').addEventListener('click', () => { 7 | ipcRenderer.send('sendMainMessage', { 8 | greeting: 'Hello' 9 | }); 10 | }); 11 | 12 | ipcRenderer.on('sendRendererMessage', (event, props) => { 13 | console.log({event, props}); 14 | }); 15 | 16 | 17 | // REMOTE 18 | document.querySelector('#openDialog').addEventListener('click', () => { 19 | remote.require('./show-dialog').showDialog('Konnichiwa'); 20 | // we could also do it directly here like so: 21 | // remote.dialog.showMessageBox({ 22 | // type: 'info', 23 | // title: 'Greetings', 24 | // message: `Konnichiwa!` 25 | // }); 26 | }); 27 | 28 | 29 | 30 | 31 | // CPU INTENSIVE 32 | document.querySelector('#doBlockingWork').addEventListener('click', () => { 33 | ipcRenderer.send('doBlockingWork'); 34 | }); 35 | 36 | document.querySelector('#doNonBlockingWork').addEventListener('click', () => { 37 | const nonBlockingWork = require('./non-blocking-work'); 38 | nonBlockingWork(); 39 | }); 40 | -------------------------------------------------------------------------------- /ipc-demo/show-dialog.js: -------------------------------------------------------------------------------- 1 | // dialog can only be directly run in the main process 2 | // if we're in the renderer, we can interact with it normally using `remote` 3 | const dialog = require('electron').dialog || require('electron').remote.dialog; 4 | 5 | function showDialog(greeting) { 6 | dialog.showMessageBox({ 7 | type: 'info', 8 | title: 'Greetings', 9 | message: `${greeting}!`, 10 | buttons: [] 11 | }); 12 | } 13 | 14 | module.exports = { showDialog }; 15 | -------------------------------------------------------------------------------- /ipc-demo/work.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | // do random work 4 | // usually takes a few seconds on newer MBP 5 | // return how long it took to complete 6 | function work(limit = 100000) { 7 | let start = Date.now(); 8 | for(let i = 0; i <= limit; i++) { 9 | crypto.randomBytes(2048); 10 | } 11 | return Date.now() - start; 12 | } 13 | 14 | module.exports = work; 15 | -------------------------------------------------------------------------------- /main-renderer-communication/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

    Main/Renderer interprocess communication

    7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /main-renderer-communication/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, dialog, ipcMain: ipc } = require('electron'); 2 | const path = require('path'); 3 | let mainWindow; 4 | 5 | app.on('ready', function() { 6 | mainWindow = new BrowserWindow(); 7 | mainWindow.loadURL('file://' + path.join(__dirname, 'index.html')); 8 | 9 | ipc.on('show-dialog', (event, {type}) => { 10 | dialog.showMessageBox(mainWindow, { 11 | type: type, 12 | buttons: [], 13 | message: 'Hello, how are you?' 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /main-renderer-communication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mainRendererCommunication", 3 | "main": "index.js", 4 | "scripts": { 5 | "start": "electron index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /main-renderer-communication/renderer.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer: ipc, remote } = require('electron'); 2 | require('devtron').install(); 3 | 4 | document.querySelector('#btn').addEventListener('click', () => { 5 | ipc.send('show-dialog', { 6 | type: 'info' 7 | }); 8 | }); 9 | 10 | document.querySelector('#btn2').addEventListener('click', () => { 11 | remote.dialog.showMessageBox(remote.getCurrentWindow(), { 12 | type: 'info', 13 | buttons: [], 14 | message: 'Hello, how are you?' 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-tutorials", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "postinstall": "electron-rebuild" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "devtron": "^1.4.0", 13 | "electron": "^1.4.15", 14 | "electron-rebuild": "^1.5.7" 15 | }, 16 | "dependencies": { 17 | "bootstrap": "^3.3.7", 18 | "electron-remote": "^1.0.9", 19 | "keytar": "^3.0.2", 20 | "lodash": "^4.15.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /preload-scripts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Preload Scripts 4 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /preload-scripts/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | const path = require('path'); 3 | let mainWindow; //do this so that the window object doesn't get GC'd 4 | 5 | // can't create a BrowserWindow until `ready` is fired 6 | app.on('ready', function() { 7 | mainWindow = new BrowserWindow(); 8 | mainWindow.loadURL('file://' + path.join(__dirname, 'index.html')); 9 | mainWindow.webContents.openDevTools(); 10 | }); 11 | -------------------------------------------------------------------------------- /preload-scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preloadScripts", 3 | "main": "index.js", 4 | "scripts": { 5 | "start": "electron index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /preload-scripts/preload.js: -------------------------------------------------------------------------------- 1 | // I have access to all electron, node.js, *and* DOM APIs. 2 | const { ipcRenderer: ipc } = require('electron'); 3 | 4 | console.log('Hey, this is being run in the context of the webview renderer process'); 5 | 6 | document.addEventListener('DOMContentLoaded', function(event) { 7 | for(let el of document.querySelectorAll('*')) { 8 | console.log(el.tagName); 9 | // send the info to the parent renderer 10 | // the id of it is conveniently always 1 in this example, but really you'd want 11 | // a more robust method of getting it 12 | ipc.sendTo(1, 'elFound', { tagName: el.tagName }); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /preload-scripts/renderer.js: -------------------------------------------------------------------------------- 1 | // I have access to all electron, node.js, *and* DOM APIs. 2 | const { ipcRenderer: ipc } = require('electron'); 3 | 4 | const webview = document.getElementById('webview'); 5 | const btn = document.getElementById('devtools'); 6 | 7 | btn.addEventListener('click', () => webview.openDevTools()); 8 | 9 | ipc.on('elFound', (event, props) => { 10 | console.log(`Message received from webview ${JSON.stringify(props)}`); 11 | }); 12 | -------------------------------------------------------------------------------- /preload-scripts/webview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

    6 | Hello from inside the webview. 7 |

    8 | 9 | 10 | -------------------------------------------------------------------------------- /storing-data/index.html: -------------------------------------------------------------------------------- 1 |

    Storing Data

    2 | -------------------------------------------------------------------------------- /storing-data/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | const path = require('path'); 3 | const Store = require('./store.js'); 4 | let mainWindow; //do this so that the window object doesn't get GC'd 5 | 6 | // initialize the store 7 | const store = new Store({ 8 | configName: 'user-preferences', 9 | defaults: { 10 | windowBounds: { 11 | width: 800, 12 | height: 600 , 13 | x: 0, 14 | y: 0 15 | } 16 | } 17 | }); 18 | 19 | // can't create a BrowserWindow until `ready` is fired 20 | app.on('ready', function() { 21 | mainWindow = new BrowserWindow(store.get('windowBounds')); 22 | 23 | function saveWindowBounds() { 24 | store.set('windowBounds', mainWindow.getBounds()); 25 | } 26 | 27 | // listen to `resize` and `move` and save the settings 28 | mainWindow.on('resize', saveWindowBounds); 29 | mainWindow.on('move', saveWindowBounds); 30 | 31 | mainWindow.loadURL('file://' + path.join(__dirname, 'index.html')); 32 | }); 33 | -------------------------------------------------------------------------------- /storing-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storingData", 3 | "main": "index.js", 4 | "scripts": { 5 | "start": "electron index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /storing-data/store.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | class Store { 6 | constructor(opts) { 7 | // renderer has to get `app` module via remote, main gets it directly 8 | const userDataPath = (electron.app || electron.remote.app).getPath('userData'); 9 | this.path = path.join(userDataPath, opts.configName + '.json'); 10 | this.data = parseDataFile(this.path, opts.defaults); 11 | } 12 | 13 | get(key) { 14 | return this.data[key]; 15 | } 16 | 17 | set(key, val) { 18 | this.data[key] = val; 19 | fs.writeFileSync(this.path, JSON.stringify(this.data)); 20 | } 21 | } 22 | 23 | function parseDataFile(filePath, defaults) { 24 | try { 25 | return JSON.parse(fs.readFileSync(filePath)); 26 | } catch(error) { 27 | return defaults; 28 | } 29 | } 30 | 31 | module.exports = Store; 32 | -------------------------------------------------------------------------------- /storing-sensitive-data/index.html: -------------------------------------------------------------------------------- 1 |

    Storing Sensitive Data

    2 | 3 | 4 | 5 | 6 | 7 | 8 |

    9 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /storing-sensitive-data/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron'); 2 | const path = require('path'); 3 | let mainWindow; 4 | 5 | app.on('ready', () => { 6 | mainWindow = new BrowserWindow(); 7 | mainWindow.loadURL(path.join('file://', __dirname, 'index.html')); 8 | }); 9 | -------------------------------------------------------------------------------- /storing-sensitive-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "storingSensitiveData", 3 | "main": "index.js", 4 | "scripts": { 5 | "start": "electron index.js" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /storing-sensitive-data/renderer.js: -------------------------------------------------------------------------------- 1 | const { remote } = require('electron'); 2 | // use `remote` require so that it's run in the context of the main process 3 | // this makes it so that the application listed Access Control List is our main app, not `MyApp Helper` 4 | // this isn't 100% necessary and probably somewhat of a personal preference 5 | const keytar = remote.require('keytar'); 6 | 7 | const getBtn = document.querySelector('#getBtn'); 8 | const setBtn = document.querySelector('#setBtn'); 9 | const secretValEl = document.querySelector('#secretVal'); 10 | const output = document.querySelector('#output'); 11 | 12 | 13 | getBtn.addEventListener('click', () => { 14 | // Params are: service name, account name. Both are arbitrary 15 | const secret = keytar.getPassword('KeytarTest', 'AccountName'); 16 | output.innerText = secret || 'Nothing set'; 17 | }); 18 | 19 | setBtn.addEventListener('click', () => { 20 | const secret = secretValEl.value; 21 | keytar.replacePassword('KeytarTest', 'AccountName', secret); 22 | }); 23 | --------------------------------------------------------------------------------