├── .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 |