|
1 | 1 |
|
2 | 2 |
|
3 | 3 | ## React + Google Apps Script
|
4 |
| -*Use this demo project as your boilerplate React app for HTML dialogs in Google Sheets, Docs and Forms.* |
| 4 | +*Use this project as your boilerplate for React apps inside Google Sheets, Docs and Forms pages.* |
5 | 5 |
|
6 |
| -This project uses labnol's excellent [apps-script-starter](https://github.com/labnol/apps-script-starter) as a starting point, adding support for React. It demonstrates how easy it is to build React apps that interact with Google Apps server-side scripts. Simply clone this project and modify the source code to get started developing with React for Google Apps Script client-side dialogs. |
| 6 | +This project uses labnol's excellent [apps-script-starter](https://github.com/labnol/apps-script-starter) as a starting point, and adds support for building React apps inside Google Sheets, Forms, and Docs pages. Simply clone this project and modify the source code to get started developing React apps with Google Apps Script. |
7 | 7 |
|
8 | 8 | 
|
9 | 9 | *The demo app for Google Sheets shows insertion/deletion/activation of sheets through React-built HTML dialog.*
|
10 | 10 |
|
11 | 11 | ## Installation
|
12 | 12 |
|
13 |
| - |
14 |
| - |
15 | 13 | 1. Clone the sample project and install dependencies:
|
16 |
| - ``` |
17 |
| - > git clone https://github.com/enuchi/React-Google-Apps-Script.git |
18 |
| - > cd React-Google-Apps-Script |
19 |
| - > npm install |
20 |
| - ``` |
| 14 | + ```bash |
| 15 | + > git clone https://github.com/enuchi/React-Google-Apps-Script.git |
| 16 | + > cd React-Google-Apps-Script |
| 17 | + > npm install |
| 18 | + ``` |
21 | 19 | 2. Enable the Google Apps Script API [(script.google.com/home/usersettings)](https://script.google.com/home/usersettings):
|
22 |
| - |
| 20 | + |
| 21 | +  |
23 | 22 |
|
24 |
| -3. Log in to `clasp` to manage your Google Apps Scripts from the command line: |
25 |
| - ``` |
26 |
| - > npm run login |
27 |
| - ``` |
| 23 | +3. Log in to `clasp`, google's tool to help you manage your Google Apps Script projects from the command line. |
| 24 | + ```bash |
| 25 | + > npm run login |
| 26 | + ``` |
28 | 27 | 4. Create a new Google Sheets file and a bound Google Scripts project for your React project:
|
29 |
| - ``` |
30 |
| - > npm run setup |
31 |
| - Created new Google Sheet: https://drive.google.com/open?id=1lVQUPZ************************************* |
32 |
| - Created new Google Sheets Add-on script: https://script.google.com/d/1K7MPtCH*************************************-**/edit |
33 |
| - ``` |
34 |
| - You've created a new Sheet and attached Script file! (But they're still empty for now.) See the notes below if you want to use an existing Google Sheet and Script instead of creating a new one. |
35 |
| -5. Build the app and deploy! |
36 |
| - ``` |
37 |
| - > npm run deploy |
38 |
| - ``` |
39 |
| - You can now visit your Google Sheet and see your new React app. |
| 28 | + ```bash |
| 29 | + > npm run setup |
| 30 | +
|
| 31 | + Created new Google Sheet: https://drive.google.com/open?id=1lVQUPZ************************************* |
| 32 | + Created new Google Sheets Add-on script: https://script.google.com/d/1K7MPtCH*************************************-**/edit |
| 33 | + ``` |
| 34 | + This will use `clasp` to create a new Google Sheets spreadsheet and a 'bound' Google Apps Script project, and save the credentials to the `.clasp.json` file in your root directory. If you don't want to create a new spreadsheet and script, and instead want to use this project with an existing project, [see the section below](#Using-an-existing-Sheet). |
| 35 | + |
| 36 | + Okay, now you've created a new sheet and bound script file! (But they're still empty for now.) |
| 37 | + |
| 38 | +5. Deploy the app! |
| 39 | + ```bash |
| 40 | + > npm run deploy |
| 41 | + ``` |
| 42 | + Open your new spreadsheet in Google Sheets. Your new React app will be available from a new menu item! |
40 | 43 |
|
41 | 44 | ### Using an existing Sheet
|
42 |
| -The above installation instructions create a new Google Sheets spreadsheet and 'bound' Apps Script, and saves the credentials to a `.clasp.json` file in the root directory. If you want to use an existing sheet's script file, then simply copy the scriptId into the `.clasp.json` file as below. You can find the script's scriptId by opening your sheet, selecting **Tools > Script Editor**, then **File > Project properties**. |
| 45 | +If you want to use an existing google script for your project: |
| 46 | +1. Copy the existing script project's `scriptId`. You can find it by opening your sheet, selecting **Tools > Script Editor**, then **File > Project properties**. |
| 47 | +
|
| 48 | +2. Run the command below to add the existing project's `scriptId` to your`.clasp.json` file. (You can also just edit the `.clasp.json` file directly. Make sure not to remove `"rootDir": "dist"` from the `.clasp.json` file.) |
| 49 | + |
| 50 | + ```bash |
| 51 | + # If your scriptId is 1K7MPtCHkjasdf93238234asdKFDF3sa9 then run |
| 52 | + > npm run setup:use-id 1K7MPtCHkjasdf93238234asdKFDF3sa9 |
| 53 | + ``` |
43 | 54 |
|
44 |
| -Paste the `scriptId` into the `.clasp.json` file. *Make sure to include `"rootDir": "dist"` in this file: |
45 |
| -``` |
46 |
| -// .clasp.json |
47 |
| -{"rootDir": "dist", "scriptId":"...paste scriptId here..."} |
48 |
| -``` |
49 | 55 | ### Making changes to the code
|
50 |
| -Modify the server-side and client-side source code in the `src` folder using ES6/7 and React. Change the scopes in `appsscript.json` if needed. When you're ready, build the app and deploy! You can run `npm run deploy` to build and deploy, or `npm run build` just to build the bundled files in the `./dist` directory. |
| 56 | +Modify the server-side and client-side source code in the `src` folder. [Add any additional scopes](https://developers.google.com/apps-script/concepts/scopes) to `appsscript.json` as needed. When you're ready, build the app and deploy! You can run `npm run deploy` to build and deploy, or `npm run build` just to build the bundled files in the `./dist` directory. |
51 | 57 |
|
52 | 58 |
|
53 | 59 | ## The sample app
|
54 | 60 | Insert/activate/delete sheets through a simple HTML dialog, built with React. Access the dialog through the new menu item that appears. You may need to refresh the spreadsheet and approve the app's permissions the first time you use it.
|
55 | 61 |
|
56 |
| -## How it works |
57 |
| -"[Google Apps Script](https://en.wikipedia.org/wiki/Google_Apps_Script) is based on JavaScript 1.6 with some portions of 1.7 and 1.8 and provides subset of ECMAScript 5 API." |
58 |
| - |
59 |
| -That means many JavaScript tools used today in modern web development will not work in the Google Apps Script environment, including `let`/`const` declarations, arrow functions, spread operator, etc. |
60 |
| - |
61 |
| -This project circumvents those restrictions by transpiling newer code to older code that Google Apps Script understands using Babel, and also bundles separate files and modules using Webpack. |
62 |
| - |
63 |
| -On the client-side, Google Apps Scripts has restrictions on the way HTML dialogs are used. While in normal web development you can simply reference a separate css file, e.g. |
64 |
| -``` |
65 |
| -<link rel="stylesheet" href="styles.css"> |
66 |
| -``` |
67 |
| -in the Google Apps Script environment you need to use [HTML templates](https://developers.google.com/apps-script/guides/html/templates), which can be cumbersome to work with. With this project, all files are bundled together by inlining .css and .js files. Using a transpiler and bundling tool also allows us to use JSX syntax, and external libraries such as React. |
68 | 62 |
|
69 | 63 | ## Features
|
70 |
| -- Support for JSX syntax: |
71 |
| -``` |
72 |
| -return <div>Name: {person.firstName}</div> |
73 |
| -``` |
74 | 64 | - Support for external packages. Simply install with npm or from a file and `import`:
|
75 |
| -``` |
76 |
| -$ npm install react-addons-css-transition-group |
77 |
| -``` |
78 |
| -``` |
79 |
| -// index.jsx |
80 |
| -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; |
81 |
| -``` |
| 65 | + ```bash |
| 66 | + > npm install react-transition-group |
| 67 | + ``` |
| 68 | + ```js |
| 69 | + // index.jsx |
| 70 | + import { TransitionGroup, CSSTransition } from 'react-transition-group'; |
| 71 | + ``` |
82 | 72 | - `import` CSS from another file:
|
83 |
| -``` |
84 |
| -import "./styles.css"; |
85 |
| -``` |
86 |
| - - Use promises with e.g. `.then`/`.catch` instead of `google.script.run`: |
87 |
| - ``` |
88 |
| -// instead of this: |
89 |
| -google.script.run |
90 |
| - .withSuccessHandler(successHandler) |
91 |
| - .withFailureHandler(failureHandler) |
92 |
| - .getSheetsData() |
93 |
| -
|
94 |
| -// you can do this: |
95 |
| -import server from '../server'; |
96 |
| -// access all your server functions here: |
97 |
| -const { getSheetsData } = server; |
98 |
| -addSheet(newSheetTitle) |
99 |
| - .then(successHandler) |
100 |
| - .catch(failureHandler); |
| 73 | + ```js |
| 74 | + import "./styles.css"; |
101 | 75 | ```
|
102 |
| -How does this work? We rewrite `google.script.run` to support Promises: |
103 |
| -``` |
104 |
| -// ./src/client/server.js |
105 |
| -
|
106 |
| -const serverMethods = {}; |
107 |
| -
|
108 |
| -// skip the reserved methods |
109 |
| -const ignoredMethods = [ |
110 |
| - 'withFailureHandler', |
111 |
| - 'withLogger', |
112 |
| - 'withSuccessHandler', |
113 |
| - 'withUserObject', |
114 |
| -]; |
115 |
| -
|
116 |
| -for (const method in google.script.run) { |
117 |
| - if (!ignoredMethods.includes(method)) { |
118 |
| - serverMethods[method] = (...args) => { |
119 |
| - return new Promise((resolve, reject) => { |
120 |
| - google.script.run |
121 |
| - .withSuccessHandler(resolve) |
122 |
| - .withFailureHandler(reject)[method](...args); |
123 |
| - }); |
124 |
| - }; |
125 |
| - } |
126 |
| -} |
127 |
| -export default serverMethods; |
128 |
| -``` |
129 |
| -Now we can use familiar Promises in our client-side code and have easy access to all server functions! |
130 |
| - |
131 |
| -- Use newer ES6/7 code, including arrow functions, spread operators, `const`/`let`, and more: |
132 |
| -``` |
133 |
| -const getSheetsData = () => { |
134 |
| - let activeSheetName = getActiveSheetName(); |
135 |
| - return getSheets().map((sheet, index) => { |
136 |
| - let sheetName = sheet.getName(); |
137 |
| - return { |
138 |
| - text: sheetName, |
139 |
| - sheetIndex: index, |
140 |
| - isActive: sheetName === activeSheetName, |
141 |
| - }; |
142 |
| - }); |
143 |
| -}; |
144 |
| -``` |
145 |
| -## Tern support |
146 |
| -This project includes support for GAS definitions and autocomplete through a [Tern](http://ternjs.net/) plugin. Tern is a code-analysis engine for JavaScript, providing many useful tools for developing. See Tern's site for setup instructions for many popular code editors, such as Sublime, Vim and others. |
147 |
| - |
148 |
| -Tern provides many indispensable tools for working with Google Apps Script, such as autocompletion on variables and properties, function argument hints and querying the type of an expression. |
149 |
| - |
150 |
| -- Autocomplete example. Lists all available methods from the appropriate Google Apps Script API: |
151 |
| - |
152 |
| - |
| 76 | + - Use promises to call and handle responses from the server instead of using `google.script.run`: |
| 77 | + ```js |
| 78 | + // Google's documentation wants you to do this. Boo. |
| 79 | + google.script.run |
| 80 | + .withSuccessHandler(response => doSomething(response)) |
| 81 | + .withFailureHandler(err => handleError(err)) |
| 82 | + .addSheet(sheetTitle) |
| 83 | +
|
| 84 | +
|
| 85 | + // Poof! With a little magic we can now do this: |
| 86 | + import server from '../server'; |
| 87 | +
|
| 88 | + // We now have access to all our server functions, which return promises! |
| 89 | + const { addSheet } = server; |
| 90 | + addSheet(sheetTitle) |
| 91 | + .then(response => doSomething(response)) |
| 92 | + .catch(handleError(err)); |
| 93 | +
|
| 94 | + // Or we can use async/await: |
| 95 | + async () => { |
| 96 | + try { |
| 97 | + const response = await addSheet(sheetTitle); |
| 98 | + doSomething(response); |
| 99 | + } catch (err) { |
| 100 | + handleError(err) |
| 101 | + } |
| 102 | + } |
| 103 | + ``` |
| 104 | +Now we can use familiar Promises in our client-side code and have easy access to all server functions. See [the code](./src/client/server.js) for the implementation details. |
| 105 | +
|
| 106 | +## Full Autocompletion |
| 107 | +This project includes support for the full autocompletion and type definitions. |
| 108 | +
|
| 109 | + |
| 110 | +
|
| 111 | +- Lists all available methods from the appropriate Google Apps Script API: |
153 | 112 | - Full definitions with links to official documentation, plus information on argument and return type:
|
154 |
| - |
155 |
| - |
156 | 113 |
|
157 | 114 |
|
158 | 115 | ## Extending this app
|
159 | 116 | - You can split up server-side code into multiple files and folders using `import` and `export` statements.
|
160 | 117 | - Make sure to expose all public functions including `onOpen` and any functions you are calling from the client. Example below shows assignment to `global` object:
|
161 |
| -``` |
162 |
| -const onOpen = () => { |
163 |
| - SpreadsheetApp.getUi() // Or DocumentApp or FormApp. |
164 |
| - .createMenu('Dialog') |
165 |
| - .addItem('Add sheets', 'openDialog') |
166 |
| - .addToUi(); |
167 |
| -} |
168 |
| -
|
169 |
| -global.onOpen = onOpen |
170 |
| -``` |
| 118 | + ```js |
| 119 | + const onOpen = () => { |
| 120 | + SpreadsheetApp.getUi() // Or DocumentApp or FormApp. |
| 121 | + .createMenu('Dialog') |
| 122 | + .addItem('Add sheets', 'openDialog') |
| 123 | + .addToUi(); |
| 124 | + } |
| 125 | +
|
| 126 | + global.onOpen = onOpen |
| 127 | + ``` |
171 | 128 | - You may wish to remove automatic linting when running Webpack. You can do so by editing the Webpack config file and commenting out the eslintConfig line in client or server settings:
|
172 |
| -``` |
173 |
| -// webpack.config.js |
174 |
| -
|
175 |
| -const clientConfig = Object.assign({}, sharedConfigSettings, { |
176 |
| - ... |
177 |
| - module: { |
178 |
| - rules: [ |
179 |
| - // eslintConfig, |
180 |
| - { |
181 |
| -``` |
| 129 | + ```js |
| 130 | + // webpack.config.js |
| 131 | +
|
| 132 | + const clientConfig = Object.assign({}, sharedConfigSettings, { |
| 133 | + ... |
| 134 | + module: { |
| 135 | + rules: [ |
| 136 | + // eslintConfig, |
| 137 | + { |
| 138 | + ``` |
182 | 139 |
|
183 | 140 | ## Multiple dialogs
|
184 |
| -This project now supports multiple dialogs and sidebars. Here is an example of the `server` code for a 'main.html' dialog and an 'about.html' sidebar: |
185 |
| -``` |
186 |
| -// ./src/server/sheets-utilities.js |
| 141 | +This project now supports multiple dialogs and sidebars. See the `server` code at [src/server/ui.js](./src/server/ui.js) for a 'main.html' dialog and an 'about.html' sidebar: |
| 142 | +```js |
| 143 | +// ./src/server/ui.js |
| 144 | +
|
| 145 | +export const onOpen = () => { |
| 146 | + SpreadsheetApp.getUi() |
| 147 | + .createMenu('My Sample React Project') // edit me! |
| 148 | + .addItem('Sheet Name Editor', 'openDialog') |
| 149 | + .addItem('About me', 'openAboutSidebar') |
| 150 | + .addToUi(); |
| 151 | +}; |
187 | 152 |
|
188 |
| -const openDialog = () => { |
| 153 | +export const openDialog = () => { |
189 | 154 | const html = HtmlService.createHtmlOutputFromFile('main')
|
190 | 155 | .setWidth(400)
|
191 | 156 | .setHeight(600);
|
192 |
| - SpreadsheetApp |
193 |
| - .getUi() // Or DocumentApp or FormApp. |
194 |
| - .showModalDialog(html, 'Sheet Editor'); |
| 157 | + SpreadsheetApp.getUi().showModalDialog(html, 'Sheet Editor'); |
195 | 158 | };
|
196 | 159 |
|
197 |
| -const openAboutSidebar = () => { |
| 160 | +export const openAboutSidebar = () => { |
198 | 161 | const html = HtmlService.createHtmlOutputFromFile('about');
|
199 |
| - SpreadsheetApp |
200 |
| - .getUi() |
201 |
| - .showSidebar(html); |
| 162 | + SpreadsheetApp.getUi().showSidebar(html); |
202 | 163 | };
|
203 | 164 | ```
|
204 | 165 | And here is the configuration in webpack that creates multiple html files. You will need to edit this if you want to add more dialog html files:
|
205 |
| -``` |
| 166 | +```js |
206 | 167 | // ./webpack.config.js
|
207 | 168 |
|
208 | 169 | // Client entrypoints:
|
209 | 170 | const clientEntrypoints = [
|
210 | 171 | {
|
211 |
| - name: "CLIENT - main dialog", |
212 |
| - entry: "./src/client/main.jsx", |
213 |
| - filename: "main.html" |
| 172 | + name: 'CLIENT - main dialog', |
| 173 | + entry: './src/client/MainDialog.jsx', |
| 174 | + filename: 'main.html', |
214 | 175 | },
|
215 | 176 | {
|
216 |
| - name: "CLIENT - about sidebar", |
217 |
| - entry: "./src/client/about.jsx", |
218 |
| - filename: "about.html" |
| 177 | + name: 'CLIENT - about sidebar', |
| 178 | + entry: './src/client/AboutDialog.jsx', |
| 179 | + filename: 'about.html', |
219 | 180 | },
|
220 | 181 | ];
|
221 | 182 | ```
|
222 | 183 |
|
223 | 184 | ## Suggestions
|
224 |
| -Open a pull request! |
| 185 | +Pull requests welcome! |
0 commit comments