├── .clasp.json
├── .claspignore
├── .editorconfig
├── .gitignore
├── .tern-project
├── README.md
├── appsscript.json
├── dist
├── appsscript.json
├── code.js
├── dialog.html
└── main.js
├── package.json
├── src
├── client
│ ├── components
│ │ ├── form-input.tsx
│ │ ├── sheet-button.tsx
│ │ └── sheet-editor.tsx
│ ├── dialog-template.html
│ ├── index.tsx
│ └── styles.scss
└── server
│ ├── code.ts
│ └── sheets-utilities.ts
├── tsconfig.json
├── tslint.json
├── webpack.config.js
└── yarn.lock
/.clasp.json:
--------------------------------------------------------------------------------
1 | {
2 | "rootDir": "dist",
3 | "scriptId": "...paste scriptId here..."
4 | }
5 |
--------------------------------------------------------------------------------
/.claspignore:
--------------------------------------------------------------------------------
1 | dist/main.js
2 | src/import.js
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | end_of_line = lf
4 | insert_final_newline = true
5 | indent_style = space
6 | indent_size = 2
7 |
8 | [{.babelrc, .stylelintrc, jest.config, .eslintrc, .prettierrc, *.json, *.jsb3, *.jsb2, *.bowerrc}]
9 | indent_style = space
10 | indent_size = 2
11 |
12 | [*.js]
13 | indent_style = tab
14 | tab_width = 2
15 |
16 | [{*.ats, *.ts}]
17 | indent_style = tab
18 | tab_width = 2
19 |
20 | [*.tsx]
21 | indent_style = tab
22 | tab_width = 2
23 |
24 | [*.js.flow]
25 | indent_style = tab
26 | tab_width = 2
27 |
28 | [{tsconfig.app.json, tsconfig.e2e.json, tsconfig.json, tsconfig.spec.json}]
29 | indent_style = space
30 | indent_size = 2
31 |
32 | [*.js.map]
33 | indent_style = space
34 | indent_size = 2
35 |
36 | [*.css]
37 | indent_style = space
38 | indent_size = 2
39 |
40 | [*.scss]
41 | indent_style = space
42 | indent_size = 2
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .clasp*
2 |
3 | .env
4 |
5 | # dependencies
6 | /node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | .idea/
24 |
--------------------------------------------------------------------------------
/.tern-project:
--------------------------------------------------------------------------------
1 | {
2 | "ecmaVersion": 7,
3 | "libs": [
4 | ],
5 | "plugins": {
6 | "complete_strings": {},
7 | "googleappsscript": {},
8 | "es_modules": {},
9 | "node": {},
10 | "doc_comment": {
11 | "fullDocs": true,
12 | "strong": true
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ## TypeScript + React + Google Apps Script
3 | *Use this demo project as your boilerplate React app for HTML dialogs in Google Sheets, Docs and Forms.*
4 |
5 | This project is a fork of [React-Google-Apps-Script](https://github.com/enuchi/React-Google-Apps-Script) (which uses labnol's excellent [apps-script-starter](https://github.com/labnol/apps-script-starter) as a starting point), adding support for TypeScript and 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 |
7 | 
8 | *The demo app for Google Sheets shows insertion/deletion/activation of sheets through React-built HTML dialog.*
9 |
10 | ## Installation
11 |
12 | Clone the sample project and install dependencies:
13 | ```
14 | git clone https://github.com/enuchi/React-Google-Apps-Script.git
15 | cd React-Google-Apps-Script
16 | yarn
17 | ```
18 | Then [create a new Google Sheets spreadsheet](https://sheets.google.com). Open the Script Editor and copy the script's scriptId. [**Tools > Script Editor**, then **File > Project properties**].
19 |
20 | Paste the **scriptId** into the .clasp.json file as below:
21 | ```
22 | // .clasp.json
23 | {"rootDir": "dist",
24 | "scriptId":"...paste scriptId here..."}
25 | ```
26 | If you have not enabled Google's Apps Script API, do so by visiting https://script.google.com/home/usersettings.
27 | Log into CLASP to push code to the server from the command line:
28 | ```
29 | yarn login:clasp
30 | ```
31 | 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!
32 | ```
33 | yarn deploy:stage
34 | ```
35 |
36 | or
37 |
38 | ```bash
39 | yarn deploy:prod
40 | ```
41 | Webpack will display any linting errors, bundle your files in `dist`, and push your files to Google's servers using CLASP. You can run `yarn build` to just build.
42 |
43 | Using `deploy:stage` will give an easier to debug minified version, where `deploy:prod` will minify it to basic obscurity.
44 |
45 | ## The sample app
46 | 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.
47 |
48 | ## How it works
49 | "[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."
50 |
51 | 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.
52 |
53 | 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.
54 |
55 | On the client-side, there are restrictions on the way HTML dialogs are used in Google Apps (Sheets, Docs and Forms). In web development you can simply reference a separate css file:
56 | ```
57 |
58 | ```
59 | 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. 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.
60 |
61 | ## Features
62 | - Support for JSX syntax:
63 | ```
64 | return
Name: {person.firstName}
65 | ```
66 | - Support for external packages. Simply install with yarn or from a file and `import`:
67 | ```
68 | $ yarn add react-addons-css-transition-group
69 | ```
70 | ```
71 | // index.jsx
72 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
73 | ```
74 | - `import` CSS from another file:
75 | ```
76 | import "./styles.css";
77 | ```
78 | - Make server calls in React with `google.script.run`:
79 | ```
80 | componentDidMount() {
81 | google.script.run
82 | .withSuccessHandler((data) => this.setState({names: data}))
83 | .withFailureHandler((error) => alert(error))
84 | .getSheetsData()
85 | }
86 | ```
87 | - Use newer ES6/7 code, including arrow functions, spread operators, `const`/`let`, and more:
88 | ```
89 | const getSheetsData = () => {
90 | let activeSheetName = getActiveSheetName();
91 | return getSheets().map((sheet, index) => {
92 | let sheetName = sheet.getName();
93 | return {
94 | text: sheetName,
95 | sheetIndex: index,
96 | isActive: sheetName === activeSheetName,
97 | };
98 | });
99 | };
100 | ```
101 | ## Tern support
102 | 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.
103 |
104 | 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.
105 |
106 | - Autocomplete example. Lists all available methods from the appropriate Google Apps Script API:
107 | 
108 |
109 | - Full definitions with links to official documentation, plus information on argument and return type:
110 | 
111 |
112 |
113 |
114 | ## Extending this app
115 | - You can split up server-side code into multiple files and folders using `import` and `export` statements.
116 | - Make sure to expose all public functions including any functions called from the client with `google.script.run` as well as onOpen. Example below shows assignment to `global` object:
117 | ```
118 | const onOpen = () => {
119 | SpreadsheetApp.getUi() // Or DocumentApp or FormApp.
120 | .createMenu('Dialog')
121 | .addItem('Add sheets', 'openDialog')
122 | .addToUi();
123 | }
124 |
125 | global.onOpen = onOpen
126 | ```
127 | - 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:
128 | ```
129 | // webpack.config.js
130 |
131 | const clientConfig = Object.assign({}, sharedConfigSettings, {
132 | ...
133 | module: {
134 | rules: [
135 | // eslintConfig,
136 | {
137 | ```
138 | ## Suggestions
139 | Open a pull request!
140 |
--------------------------------------------------------------------------------
/appsscript.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeZone": "America/New_York",
3 | "dependencies": {
4 | },
5 | "oauthScopes": [
6 | "https://www.googleapis.com/auth/script.container.ui",
7 | "https://www.googleapis.com/auth/spreadsheets"
8 | ]
9 | }
--------------------------------------------------------------------------------
/dist/appsscript.json:
--------------------------------------------------------------------------------
1 | {
2 | "timeZone": "America/New_York",
3 | "dependencies": {
4 | },
5 | "oauthScopes": [
6 | "https://www.googleapis.com/auth/script.container.ui",
7 | "https://www.googleapis.com/auth/spreadsheets"
8 | ]
9 | }
--------------------------------------------------------------------------------
/dist/code.js:
--------------------------------------------------------------------------------
1 | function onOpen() {
2 | }
3 | function openDialog() {
4 | }
5 | function getSheetsData() {
6 | }
7 | function addSheet() {
8 | }
9 | function deleteSheet() {
10 | }
11 | function setActiveSheet() {
12 | }!function(e, a) {
13 | for (var i in a) e[i] = a[i];
14 | }(this, /******/ function(modules) {
15 | // webpackBootstrap
16 | /******/ // The module cache
17 | /******/ var installedModules = {};
18 | /******/
19 | /******/ // The require function
20 | /******/ function __webpack_require__(moduleId) {
21 | /******/
22 | /******/ // Check if module is in cache
23 | /******/ if (installedModules[moduleId])
24 | /******/ return installedModules[moduleId].exports;
25 | /******/
26 | /******/ // Create a new module (and put it into the cache)
27 | /******/ var module = installedModules[moduleId] = {
28 | /******/ i: moduleId,
29 | /******/ l: !1,
30 | /******/ exports: {}
31 | /******/ };
32 | /******/
33 | /******/ // Execute the module function
34 | /******/
35 | /******/
36 | /******/ // Return the exports of the module
37 | /******/ return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__),
38 | /******/
39 | /******/ // Flag the module as loaded
40 | /******/ module.l = !0, module.exports;
41 | /******/ }
42 | /******/
43 | /******/
44 | /******/ // expose the modules object (__webpack_modules__)
45 | /******/
46 | /******/
47 | /******/
48 | /******/ // Load entry module and return exports
49 | /******/ return __webpack_require__.m = modules,
50 | /******/
51 | /******/ // expose the module cache
52 | /******/ __webpack_require__.c = installedModules,
53 | /******/
54 | /******/ // define getter function for harmony exports
55 | /******/ __webpack_require__.d = function(exports, name, getter) {
56 | /******/ __webpack_require__.o(exports, name) ||
57 | /******/ Object.defineProperty(exports, name, {
58 | enumerable: !0,
59 | get: getter
60 | })
61 | /******/;
62 | },
63 | /******/
64 | /******/ // define __esModule on exports
65 | /******/ __webpack_require__.r = function(exports) {
66 | /******/ "undefined" != typeof Symbol && Symbol.toStringTag &&
67 | /******/ Object.defineProperty(exports, Symbol.toStringTag, {
68 | value: "Module"
69 | })
70 | /******/ , Object.defineProperty(exports, "__esModule", {
71 | value: !0
72 | });
73 | },
74 | /******/
75 | /******/ // create a fake namespace object
76 | /******/ // mode & 1: value is a module id, require it
77 | /******/ // mode & 2: merge all properties of value into the ns
78 | /******/ // mode & 4: return value when already ns object
79 | /******/ // mode & 8|1: behave like require
80 | /******/ __webpack_require__.t = function(value, mode) {
81 | /******/ if (
82 | /******/ 1 & mode && (value = __webpack_require__(value)), 8 & mode) return value;
83 | /******/ if (4 & mode && "object" == typeof value && value && value.__esModule) return value;
84 | /******/ var ns = Object.create(null);
85 | /******/
86 | /******/ if (__webpack_require__.r(ns),
87 | /******/ Object.defineProperty(ns, "default", {
88 | enumerable: !0,
89 | value: value
90 | }), 2 & mode && "string" != typeof value) for (var key in value) __webpack_require__.d(ns, key, function(key) {
91 | return value[key];
92 | }.bind(null, key));
93 | /******/ return ns;
94 | /******/ },
95 | /******/
96 | /******/ // getDefaultExport function for compatibility with non-harmony modules
97 | /******/ __webpack_require__.n = function(module) {
98 | /******/ var getter = module && module.__esModule ?
99 | /******/ function getDefault() {
100 | return module["default"];
101 | } :
102 | /******/ function getModuleExports() {
103 | return module;
104 | };
105 | /******/
106 | /******/ return __webpack_require__.d(getter, "a", getter), getter;
107 | /******/ },
108 | /******/
109 | /******/ // Object.prototype.hasOwnProperty.call
110 | /******/ __webpack_require__.o = function(object, property) {
111 | return Object.prototype.hasOwnProperty.call(object, property);
112 | },
113 | /******/
114 | /******/ // __webpack_public_path__
115 | /******/ __webpack_require__.p = "", __webpack_require__(__webpack_require__.s = 1);
116 | /******/}
117 | /************************************************************************/
118 | /******/ ([
119 | /* 0 */
120 | /***/ function(module, __webpack_exports__, __webpack_require__) {
121 | "use strict";
122 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "d", function() {
123 | return onOpen;
124 | }),
125 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "e", function() {
126 | return openDialog;
127 | }),
128 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "c", function() {
129 | return getSheetsData;
130 | }),
131 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() {
132 | return addSheet;
133 | }),
134 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "b", function() {
135 | return deleteSheet;
136 | }),
137 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "f", function() {
138 | return setActiveSheet;
139 | });
140 | var onOpen = function() {
141 | SpreadsheetApp.getUi().createMenu("Custom scripts").addItem("Edit sheets [sample React project]", "openDialog").addToUi();
142 | }, openDialog = function() {
143 | var html = HtmlService.createHtmlOutputFromFile("dialog").setWidth(400).setHeight(600);
144 | SpreadsheetApp.getUi().showModalDialog(html, "Sheet Editor");
145 | }, getSheets = function() {
146 | return SpreadsheetApp.getActive().getSheets();
147 | }, getSheetsData = function() {
148 | var activeSheetName = SpreadsheetApp.getActive().getSheetName();
149 | return getSheets().map(function(sheet, index) {
150 | var sheetName = sheet.getName();
151 | return {
152 | text: sheetName,
153 | sheetIndex: index,
154 | isActive: sheetName === activeSheetName
155 | };
156 | });
157 | }, addSheet = function(sheetTitle) {
158 | return SpreadsheetApp.getActive().insertSheet(sheetTitle), getSheetsData();
159 | }, deleteSheet = function(sheetIndex) {
160 | var sheets = getSheets();
161 | return SpreadsheetApp.getActive().deleteSheet(sheets[sheetIndex]), getSheetsData();
162 | }, setActiveSheet = function(sheetName) {
163 | return SpreadsheetApp.getActive().getSheetByName(sheetName).activate(), getSheetsData();
164 | };
165 | },
166 | /* 1 */
167 | /***/ function(module, __webpack_exports__, __webpack_require__) {
168 | "use strict";
169 | __webpack_require__.r(__webpack_exports__),
170 | /* WEBPACK VAR INJECTION */ function(global) {
171 | /* harmony import */ var _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);
172 | global.onOpen = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__.d, global.openDialog = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__.e,
173 | global.getSheetsData = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__.c, global.addSheet = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__.a,
174 | global.deleteSheet = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__.b, global.setActiveSheet = _sheets_utilities__WEBPACK_IMPORTED_MODULE_0__.f;
175 | }.call(this, __webpack_require__(2))
176 | /***/;
177 | },
178 | /* 2 */
179 | /***/ function(module, exports) {
180 | var g;
181 | // This works in non-strict mode
182 | g = function() {
183 | return this;
184 | }();
185 | try {
186 | // This works if eval is allowed (see CSP)
187 | g = g || new Function("return this")();
188 | } catch (e) {
189 | // This works if the window reference is available
190 | "object" == typeof window && (g = window);
191 | }
192 | // g can still be undefined, but nothing to do about it...
193 | // We return undefined, instead of nothing here, so it's
194 | // easier to handle this case. if(!global) { ...}
195 | module.exports = g;
196 | }
197 | /******/ ]));
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typescript-react-google-apps-script",
3 | "version": "1.0.0",
4 | "description": "Starter project for Google Apps Script using TypeScript and React",
5 | "scripts": {
6 | "test": "test",
7 | "login:clasp": "npx clasp login",
8 | "start": "webpack --mode development",
9 | "build": "webpack --mode production",
10 | "deploy:stage": "yarn run build && npx clasp push",
11 | "deploy:prod": "NODE_ENV=production yarn run build && npx clasp push"
12 | },
13 | "keywords": [
14 | "typescript",
15 | "react",
16 | "sass",
17 | "scss",
18 | "webpack",
19 | "google",
20 | "apps",
21 | "script",
22 | "sheets"
23 | ],
24 | "author": "Danny Hinshaw",
25 | "license": "MIT",
26 | "dependencies": {},
27 | "devDependencies": {
28 | "@google/clasp": "^1.7.0",
29 | "@types/google-apps-script": "0.0.28",
30 | "@types/node": "^11.9.5",
31 | "@types/react": "^16.8.5",
32 | "@types/react-dom": "^16.8.2",
33 | "awesome-typescript-loader": "^5.2.1",
34 | "clean-webpack-plugin": "^0.1.19",
35 | "copy-webpack-plugin": "^5.0.0",
36 | "css-loader": "^2.1.0",
37 | "gas-lib": "^2.0.2",
38 | "gas-webpack-plugin": "^1.0.2",
39 | "html-webpack-inline-source-plugin": "^0.0.10",
40 | "html-webpack-plugin": "^3.2.0",
41 | "node-sass": "^4.11.0",
42 | "prettier": "^1.16.4",
43 | "prop-types": "^15.7.2",
44 | "react": "^16.8.3",
45 | "react-dom": "^16.8.3",
46 | "sass-loader": "^7.1.0",
47 | "source-map-loader": "^0.2.4",
48 | "style-loader": "^0.23.1",
49 | "tern": "^0.23.0",
50 | "tern-googleappsscript": "^1.0.2",
51 | "terser-webpack-plugin": "^1.2.3",
52 | "tslint": "^5.13.0",
53 | "tslint-config-prettier": "^1.18.0",
54 | "tslint-react": "^3.6.0",
55 | "tslint-webpack-plugin": "^2.0.2",
56 | "typescript": "^3.3.3333",
57 | "webpack": "^4.29.5",
58 | "webpack-cli": "^3.2.3"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/client/components/form-input.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "../styles.scss"; // tslint:disable-line
3 |
4 |
5 | interface IFormInputProps {
6 | newSheetFormHandler: any
7 | }
8 |
9 |
10 | export default class FormInput extends React.Component {
11 | public state: { text: string };
12 |
13 | constructor(props) {
14 | super(props);
15 | this.state = { text: "" };
16 | this.handleChange = this.handleChange.bind(this);
17 | this.handleSubmit = this.handleSubmit.bind(this);
18 | }
19 |
20 | public handleChange(e) {
21 | this.setState({ text: e.target.value });
22 | }
23 |
24 | public handleSubmit = (e) => {
25 | e.preventDefault();
26 | if (this.state.text.length) {
27 | this.props.newSheetFormHandler(e, this.state.text);
28 | this.setState({ text: "" });
29 | }
30 | };
31 |
32 | public render() {
33 | return (
34 |