├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── VERSIONING.md ├── __mocks__ ├── mockForm.js └── mockXhr.js ├── __tests__ └── Form-test.js ├── docs ├── form-google-sheets.js └── index.html ├── package.json ├── src ├── Form.js ├── GoogleScript │ └── Code.gs └── index.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-class-properties", 7 | "transform-object-rest-spread" 8 | ], 9 | "env": { 10 | "test": { 11 | "plugins": [ 12 | "istanbul" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techcoop/form-google-sheets/143d4bb351abe4007971beffba3f3c6b17c41f40/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true 6 | } 7 | }, 8 | "env": { 9 | "browser": true, 10 | "es6": true, 11 | "jest": true 12 | }, 13 | "rules": { 14 | "import/no-extraneous-dependencies": 0 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Output directories 2 | /lib/ 3 | /out/ 4 | 5 | # System files 6 | node_modules/ 7 | devnodejsnpm-cache/ 8 | npm-debug.log 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | docs/ 3 | node_modules/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | notifications: 5 | email: 6 | on_success: never 7 | on_failure: change 8 | slack: 9 | secure: PqPimMD5V3h0qBsfWWg3Pt2ufWbWQBXmdyGmHU05PCppRtX6cB+lcl2iUTAmRheg+Ra2XMYU6m+lP6pOhpXBK9YKtkanvhnzV+bkXpUL3sRg1CfPunfQSRT/tNyQigpHQAnpuH+4C1txzTPy5P2O70BcXsBHb3Ab2slY5Lk7p2a6oW4+jNOka621UXUE/q5feWHo+XdrRdzoNhxijUot0aJQFeO1wr2rkln+PLlo86qj85yESA2LqV5VHXxJSVJ/urDFBz3FAqjFYZxmD+uNWIBRjDizsjW9uJwmUnRdx1TI3bFq40JcGx/ndaHCYnQAOKvzvr1oSNXaiYNFJQYYCpt3PwBz2ccZkhOQfZENTsVusfx1VKy47RWM8eehWZ09whp3u+8ehz4mc1n+2vuVto0dnhK2pLKNUMAYe/07EXJpc7XDTK2JvgmV3qN8aEIiHFdbNGVrlVW+TU0F/0VCe5+Cb/goIMRZiKYVixDMDDkd9vOPGKelPt5r4+MJ9HmdcJLn9Y3PMIMDoRKv3iNJzCbsFIq3sgwdDMnWBR3UTeBYFQqpxdtVBW949XOhSVzsV4vO6sKZxxUbQQfA0foGa0yMpwXAx6i256ZzaY1/XrlRKOb27gpGwTsatMsxsRj09WBPEuQX0gWDznYLH3TqpuE8QyOAgiZrsZBGL7t1IiU= 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [0.1.1](https://github.com/techcoop/form-google-sheets/compare/v0.1.0...v0.1.1) (2018-01-03) 7 | 8 | 9 | 10 | 11 | # 0.1.0 (2018-01-02) 12 | 13 | 14 | ### Features 15 | 16 | * Release/0.1.0 ([#1](https://github.com/techcoop/form-google-sheets/issues/1)) ([c2b99fb](https://github.com/techcoop/form-google-sheets/commit/c2b99fb)) 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Standards 4 | Please reference our central documentation on how you can get involved in our projects: 5 | 6 | https://github.com/techcoop/standards/blob/master/docs/contributing.md 7 | 8 | ## Project Standards 9 | None 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | 2 | [Colin Gagnon](https://github.com/colingagnon) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Tech Cooperative (https://techcoop.group) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # form-google-sheets 2 | [![Maintenance Status][status-image]][status-url] [![NPM version][npm-image]][npm-url] [![Travis build][travis-image]][travis-url] 3 | 4 | Uses Google Apps Scripts with Google Sheets to provide a POST endpoint for form 5 | data that is parsed into sheet fields. 6 | 7 | https://techcoop.github.io/form-google-sheets/ 8 | 9 | # Requirements 10 | 11 | 1) Google Account 12 | 2) Google Sheet with column headers 13 | 3) node > 6.0.0 (Optional) 14 | 4) yarn (or npm latest) > 0.10.0 (Optional) 15 | 16 | # Installation 17 | 18 | ### Google Scripts 19 | 20 | 1) Create a google sheet 21 | 2) Make the first column "timestamp" 22 | 3) Click on "Tools" > "Script Editor..." 23 | 4) Name your project something memorable 24 | 5) Replace contents of code.gs with [this file](https://github.com/techcoop/form-google-sheets/blob/master/src/GoogleScript/Code.gs) 25 | 6) Change the name of your tab in the sheet 26 | 7) Update the variable "SHEET_NAME" in the code from step 5 27 | 8) Add columns you want as fields in your form, the fields should match your HTML exactly 28 | 9) Add any required fields with messages to the "fields" object 29 | 10) Edit the "testData" to match your sheet fields 30 | 11) Click on Run > Run Function > "test_post" 31 | 12) Confirm this data is inserted into your sheet 32 | 13) If everything works, Click on Publish and select "Deploy as web app" 33 | 14) Select new and type a version name (e.g 0.1.0) (or update existing) 34 | 15) In "Execute the app as" select yourself 35 | 16) In "Who has access to the app" Select "Anyone, even anonymous" 36 | 17) Click Deploy or Update 37 | 18) Click "Review Permissions", to Authorize application 38 | 19) When you see a warning, Click "Advanced" and "Go to PROJECT_NAME" 39 | 20) Review list of permissions required, and click "Allow" 40 | 21) Copy and paste URL (note: if you're logged into multiple Google accounts you'll [have to manually remove "/u/0" or similar from the URL](https://stackoverflow.com/a/47050007/4869657) to avoid errors) 41 | 22) Setup your HTML form to post to the URL from step 21 42 | 43 | ### NPM package 44 | 45 | ```bash 46 | yarn add form-google-sheets 47 | ``` 48 | # Usage 49 | 50 | ### ES6 51 | ```javascript 52 | import { Form } from 'form-google-sheets' 53 | 54 | const endpoint = 'https://script.google.com/macros/s/AKfycbyEBGqfIUmxrLKMp_LlAlH8C_VO9vfRvtvwgjAS9lEi8Vu8xho/exec' 55 | const form = new Form(endpoint).then((event) => { 56 | console.log('SUCCESS') 57 | console.log(event) 58 | }).catch((event) => { 59 | console.log('ERROR') 60 | console.log(event) 61 | }) 62 | ``` 63 | 64 | ### Node 65 | ```javascript 66 | var Form = require('form-google-sheets').Form 67 | 68 | var endpoint = 'https://script.google.com/macros/s/AKfycbyEBGqfIUmxrLKMp_LlAlH8C_VO9vfRvtvwgjAS9lEi8Vu8xho/exec' 69 | var form = new Form(endpoint).then((event) => { 70 | console.log('SUCCESS') 71 | console.log(event) 72 | }).catch((event) => { 73 | console.log('ERROR') 74 | console.log(event) 75 | }) 76 | ``` 77 | 78 | ### Javascript 79 | ```html 80 | 81 | ``` 82 | 83 | ```javascript 84 | var endpoint = 'https://script.google.com/macros/s/AKfycbyEBGqfIUmxrLKMp_LlAlH8C_VO9vfRvtvwgjAS9lEi8Vu8xho/exec' 85 | var form = new FormGoogleSheets.Form(endpoint).then(function(event) { 86 | console.log('SUCCESS') 87 | console.log(event) 88 | }).catch(function(event) { 89 | console.log('ERROR') 90 | console.log(event) 91 | }) 92 | ``` 93 | 94 | # Testing 95 | 96 | ### Google Scripts 97 | There is a test function setup that you can change to include your own fields and make sure your form is setup correctly. 98 | 99 | 1) Change the testData in test_post() 100 | 2) Move to Run in the top menu 101 | 3) Click function test_post() 102 | 103 | ### Client library 104 | 105 | ```bash 106 | # Run unit test 107 | yarn test 108 | ``` 109 | 110 | # Releasing 111 | ```bash 112 | # Create new versioned release 113 | yarn run release 114 | ``` 115 | 116 | # Examples 117 | 118 | You can see examples of use in javascript under /docs. 119 | 120 | You can see the (view only) sheet that this posts to here: 121 | https://docs.google.com/spreadsheets/d/1SRRfFOpIJyW6tZB1TP3wJf01CFISviIMFEJDgwoqq5w/edit?usp=sharing 122 | 123 | # Original Source 124 | https://github.com/dwyl/html-form-send-email-via-google-script-without-server 125 | 126 | # Contributing 127 | All contributors are welcome, please follow [CONTRIBUTING.md](guidelines) 128 | 129 | # Contributors 130 | [colin@techcoop.group](admin) 131 | 132 | [admin]: https://github.com/colingagnon 133 | 134 | [status-image]: https://img.shields.io/badge/status-maintained-brightgreen.svg 135 | [status-url]: https://github.com/techcoop/form-google-sheets 136 | 137 | [npm-image]: https://img.shields.io/npm/v/form-google-sheets.svg 138 | [npm-url]: https://www.npmjs.com/package/form-google-sheets 139 | 140 | [travis-image]: https://travis-ci.org/techcoop/form-google-sheets.svg?branch=master 141 | [travis-url]: https://travis-ci.org/techcoop/form-google-sheets 142 | 143 | [license-image]: https://img.shields.io/badge/license-MIT-blue.svg 144 | [license-url]: https://raw.githubusercontent.com/techcoop/form-google-sheets/master/LICENSE 145 | 146 | -------------------------------------------------------------------------------- /VERSIONING.md: -------------------------------------------------------------------------------- 1 | # Versioning 2 | 3 | ## Standards 4 | This project uses [Semver](http://semver.org/) to manage new releases, you can find more details: 5 | 6 | https://github.com/techcoop/standards/blob/master/docs/versioning.md 7 | 8 | ## Project Standards 9 | None 10 | -------------------------------------------------------------------------------- /__mocks__/mockForm.js: -------------------------------------------------------------------------------- 1 | export const createMockForm = (name = 'test', value = 'test123', required = true) => { 2 | let form = document.createElement('form') 3 | 4 | let input = document.createElement('input') 5 | input.setAttribute('type', 'text') 6 | input.setAttribute('name', name) 7 | input.setAttribute('value', value) 8 | if (required) { 9 | input.setAttribute('required', 1) 10 | } 11 | 12 | form.appendChild(input) 13 | 14 | return form 15 | } 16 | -------------------------------------------------------------------------------- /__mocks__/mockXhr.js: -------------------------------------------------------------------------------- 1 | export const createMockXHR = (data) => { 2 | return { 3 | open: jest.fn(), 4 | send: jest.fn(), 5 | readyState: 4, 6 | responseText: JSON.stringify( 7 | data || {} 8 | ) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /__tests__/Form-test.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import { default as Form, defaultMessages, defaultValidate } from './../src/Form' 3 | import { createMockXHR } from './../__mocks__/mockXhr' 4 | import { createMockForm } from './../__mocks__/mockForm' 5 | 6 | describe('When creating a Form ', function() { 7 | it('it should take a URL in the constructor', function() { 8 | const url = 'http://localhost' 9 | const instance = new Form(url) 10 | expect(instance.url).toEqual(url) 11 | }) 12 | 13 | it('it should apply defaults for messages and validate function', function() { 14 | const form = new Form('http://localhost') 15 | expect(form.messages).toEqual(defaultMessages) 16 | expect(form.validate).toEqual(defaultValidate) 17 | }) 18 | 19 | it('it should accept parameters for messages and validate function', function() { 20 | const messages = { 21 | success: 'TEST SUCCESS', 22 | error: 'TEST ERROR' 23 | } 24 | 25 | const validate = (form) => { 26 | return true 27 | } 28 | 29 | const form = new Form('http://localhost', messages, validate) 30 | expect(form.messages).toEqual(messages) 31 | expect(form.validate).toEqual(validate) 32 | }) 33 | 34 | it('it should throw an exception if no URL is passed', function() { 35 | expect(() => { 36 | new Form() 37 | }).toThrow(TypeError) 38 | }) 39 | 40 | it('it should throw an exception if no function passed to then listener', function() { 41 | expect(() => { 42 | const form = new Form('http://localhost').then() 43 | }).toThrow(TypeError) 44 | }) 45 | 46 | it('it should add listener to thenListeners when passed a function', function() { 47 | const callback = () => { 48 | return true 49 | } 50 | 51 | const form = new Form('http://localhost').then(callback) 52 | expect(form.thenListeners[0]).toEqual(callback) 53 | }) 54 | 55 | it('it should throw an exception if no function passed to catch listener', function() { 56 | expect(() => { 57 | const form = new Form('http://localhost').catch() 58 | }).toThrow(TypeError) 59 | }) 60 | 61 | it('it should add listener to catchListeners when passed a function', function() { 62 | const callback = () => { 63 | return true 64 | } 65 | 66 | const form = new Form('http://localhost').catch(callback) 67 | expect(form.catchListeners[0]).toEqual(callback) 68 | }) 69 | 70 | it('it should reject promise if form is not valid when submitted', function(done) { 71 | const messages = { 72 | error: 'ERR' 73 | } 74 | 75 | const form = new Form('http://localhost', messages, () => (false)) 76 | const spy1 = sinon.spy() 77 | form.catch(spy1) 78 | 79 | const mockForm = createMockForm() 80 | form.submit(mockForm, () => {}, (event) => { 81 | expect(event.target).toEqual(mockForm) 82 | expect(event.error).toEqual(messages.error) 83 | expect(spy1.called).toEqual(true) 84 | done() 85 | }) 86 | }) 87 | 88 | it('it should POST data and handle errors with error from data', function(done) { 89 | // Keep reference to request 90 | const oldXMLHttpRequest = window.XMLHttpRequest 91 | const mockData = {error: 'ERROR', data: {test: 'test123'}} 92 | let mockXHR = createMockXHR(mockData) 93 | window.XMLHttpRequest = jest.fn(() => mockXHR) 94 | 95 | const form = new Form('http://localhost', {}, () => (true)) 96 | const spy1 = sinon.spy() 97 | form.catch(spy1) 98 | 99 | const mockForm = createMockForm() 100 | form.submit(mockForm, () => {}, (event) => { 101 | expect(event.target).toEqual(mockForm) 102 | expect(event.data).toEqual(mockData.data) 103 | expect(event.error).toEqual(mockData.error) 104 | expect(spy1.called).toEqual(true) 105 | done() 106 | }) 107 | 108 | mockXHR.onload() 109 | 110 | // Restore reference to request 111 | window.XMLHttpRequest = oldXMLHttpRequest 112 | }) 113 | 114 | it('it should POST data and use error from constructor if passed', function(done) { 115 | // Keep reference to request 116 | const oldXMLHttpRequest = window.XMLHttpRequest 117 | const mockData = {error: 'ERROR', data: {test: 'test123'}} 118 | let mockXHR = createMockXHR(mockData) 119 | window.XMLHttpRequest = jest.fn(() => mockXHR) 120 | 121 | const messages = { 122 | error: 'ERR' 123 | } 124 | 125 | const form = new Form('http://localhost', messages, () => (true)) 126 | const mockForm = createMockForm() 127 | form.submit(mockForm, () => {}, (event) => { 128 | expect(event.error).toEqual(messages.error) 129 | done() 130 | }) 131 | 132 | mockXHR.onload() 133 | 134 | // Restore reference to request 135 | window.XMLHttpRequest = oldXMLHttpRequest 136 | }) 137 | 138 | it('it should POST data and handle success with message', function(done) { 139 | // Keep reference to request 140 | const oldXMLHttpRequest = window.XMLHttpRequest 141 | const mockData = {message: 'SUCCESS'} 142 | let mockXHR = createMockXHR(mockData) 143 | window.XMLHttpRequest = jest.fn(() => mockXHR) 144 | 145 | const form = new Form('http://localhost', {}, () => (true)) 146 | const spy1 = sinon.spy() 147 | form.then(spy1) 148 | 149 | const mockForm = createMockForm() 150 | form.submit(mockForm, (event) => { 151 | expect(event.target).toEqual(mockForm) 152 | expect(event.message).toEqual(mockData.message) 153 | expect(spy1.called).toEqual(true) 154 | done() 155 | }) 156 | 157 | mockXHR.onload() 158 | 159 | // Restore reference to request 160 | window.XMLHttpRequest = oldXMLHttpRequest 161 | }) 162 | 163 | it('it should POST data and use success from constructor if passed', function(done) { 164 | // Keep reference to request 165 | const oldXMLHttpRequest = window.XMLHttpRequest 166 | const mockData = {message: 'SUCCESS'} 167 | let mockXHR = createMockXHR(mockData) 168 | window.XMLHttpRequest = jest.fn(() => mockXHR) 169 | 170 | const messages = { 171 | success: 'SUC' 172 | } 173 | 174 | const form = new Form('http://localhost', messages, () => (true)) 175 | const mockForm = createMockForm() 176 | form.submit(mockForm, (event) => { 177 | expect(event.message).toEqual(messages.success) 178 | done() 179 | }) 180 | 181 | mockXHR.onload() 182 | 183 | // Restore reference to request 184 | window.XMLHttpRequest = oldXMLHttpRequest 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /docs/form-google-sheets.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FormGoogleSheets=t():e.FormGoogleSheets=t()}("undefined"!=typeof self?self:this,function(){return function(e){function t(n){if(r[n])return r[n].exports;var o=r[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var r={};return t.m=e,t.c=r,t.d=function(e,r,n){t.o(e,r)||Object.defineProperty(e,r,{configurable:!1,enumerable:!0,get:n})},t.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(r,"a",r),r},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,r){"use strict";function n(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var o=r(1);Object.defineProperty(t,"Form",{enumerable:!0,get:function(){return n(o).default}})},function(e,t,r){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var r=0;r1&&void 0!==arguments[1]?arguments[1]:u,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:a;if(n(this,e),this.thenListeners=[],this.catchListeners=[],this.promise=void 0,this.messages=r,this.validate=o,!t)throw new TypeError("Form::constructor - You must pass a URL.");this.url=t}return o(e,[{key:"then",value:function(e){if(!(e instanceof Function))throw new TypeError("Form::then - You must a function callback to then.");return this.thenListeners.push(e),this}},{key:"catch",value:function(e){if(!(e instanceof Function))throw new TypeError("Form::catch - You must a function callback to catch.");return this.catchListeners.push(e),this}},{key:"submit",value:function(e,t,r){var n=this;new Promise(function(t,r){if(n.validate(e)){var o=new XMLHttpRequest;o.open("POST",n.url),o.onload=function(){var e=JSON.parse(o.responseText);e.error?r(e):t(e)},o.onerror=function(){return r({error:s})},o.send(new FormData(e))}else r({error:s})}).then(function(r){var o={target:e,message:n.messages.success?n.messages.success:r.message};r.data&&(o.data=r.data),n.thenListeners.map(function(e){e(o)}),t&&t(o)}).catch(function(t){var o={target:e,error:n.messages.error?n.messages.error:t.error};t.data&&(o.data=t.data),n.catchListeners.map(function(e){e(o)}),r&&r(o)});return!1}}]),e}();t.default=i}])}); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form-Google-Sheets 6 | 7 | 13 | 14 | 15 |

Form Google Sheets

16 |

From form submit

17 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 | 51 |
52 | 53 |
54 |
55 |
56 |
57 | 58 |
59 |

From onclick event

60 | 71 | 72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 |
85 |
86 | 87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 | 95 | 104 | 105 | 106 |
107 |
108 |
109 |
110 | 111 |
112 |

Custom messages and validation

113 | 147 | 148 |
149 |
150 | 151 | 152 |
153 | 154 |
155 | 156 |
157 |
158 |
159 |
160 | 161 | 162 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "form-google-sheets", 3 | "version": "0.1.1", 4 | "description": "Uses Google Apps Scripts with Google Sheets to provide a POST endpoint for form data that is parsed into sheet fields.", 5 | "main": "lib/index.js", 6 | "jsnext:main": "src/index.js", 7 | "author": "colingagnon", 8 | "license": "MIT", 9 | "repository": "techcoop/form-google-sheets", 10 | "keywords": [ 11 | "techcoop", 12 | "google", 13 | "sheets", 14 | "form", 15 | "webpack", 16 | "babel", 17 | "library", 18 | "AMD", 19 | "UMD" 20 | ], 21 | "devDependencies": { 22 | "babel-cli": "^6.18.0", 23 | "babel-core": "^6.21.0", 24 | "babel-eslint": "^7.1.1", 25 | "babel-loader": "^7.1.0", 26 | "babel-plugin-istanbul": "^4.1.4", 27 | "babel-plugin-transform-class-properties": "^6.19.0", 28 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 29 | "babel-polyfill": "^6.9.1", 30 | "babel-preset-es2015": "^6.9.0", 31 | "babel-preset-stage-2": "^6.18.0", 32 | "better-npm-run": "^0.1.0", 33 | "conventional-changelog": "^1.1.0", 34 | "css-loader": "^0.26.1", 35 | "eslint": "^3.12.2", 36 | "fs-extra-promise": "^1.0.1", 37 | "jest-cli": "^21.0.2", 38 | "node-sass": "^4.0.0", 39 | "postcss-loader": "^1.2.1", 40 | "rimraf": "^2.6.1", 41 | "sass-loader": "^6.0.6", 42 | "sitemap": "^1.12.0", 43 | "standard-version": "^4.0.0", 44 | "style-loader": "^0.13.1", 45 | "webpack": "^3.0.0" 46 | }, 47 | "files": [ 48 | "LICENSE", 49 | "README.md", 50 | "lib/", 51 | "out/" 52 | ], 53 | "scripts": { 54 | "preversion": "yarn test", 55 | "lint": "eslint src/**/*.js", 56 | "pretest": "yarn run lint", 57 | "test": "jest", 58 | "watch": "jest --watchAll", 59 | "docs": "echo \"TODO docs\"", 60 | "clean": "rimraf lib rimraf out", 61 | "compile:amd": "better-npm-run compile:amd", 62 | "compile:umd": "better-npm-run compile:umd", 63 | "compile:docs": "better-npm-run compile:docs", 64 | "start": "yarn run compile -- --watch", 65 | "release": "yarn run lint yarn run clean && yarn test && yarn run compile:amd && yarn run compile:umd && yarn run compile:docs && standard-version" 66 | }, 67 | "betterScripts": { 68 | "compile:amd": { 69 | "command": "babel src --ignore __tests__ --out-dir lib", 70 | "env": { 71 | "NODE_ENV": "production" 72 | } 73 | }, 74 | "compile:umd": { 75 | "command": "webpack", 76 | "env": { 77 | "NODE_ENV": "production" 78 | } 79 | }, 80 | "compile:docs": { 81 | "command": "yarn run compile:umd && cp -r out/* docs", 82 | "env": { 83 | "NODE_ENV": "production" 84 | } 85 | } 86 | }, 87 | "dependencies": { 88 | "sinon": "^4.1.3" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Form.js: -------------------------------------------------------------------------------- 1 | const defaultSuccess = 'Thank you for your interest.' 2 | const defaultError = 'There was an error process your form submission.' 3 | 4 | export const defaultMessages = { 5 | success: '', 6 | error: '' 7 | } 8 | 9 | export const defaultValidate = (form) => { 10 | if (!form) { 11 | return false 12 | } 13 | 14 | return form.checkValidity() 15 | } 16 | 17 | class Form { 18 | constructor (url, messages = defaultMessages, validate = defaultValidate) { 19 | this.thenListeners = [] 20 | this.catchListeners = [] 21 | this.promise = undefined 22 | this.messages = messages 23 | this.validate = validate 24 | 25 | if (!url) { 26 | throw new TypeError('Form::constructor - You must pass a URL.') 27 | } 28 | 29 | this.url = url 30 | } 31 | 32 | then(callback) { 33 | if (!(callback instanceof Function)) { 34 | throw new TypeError('Form::then - You must a function callback to then.') 35 | } else { 36 | this.thenListeners.push(callback) 37 | } 38 | 39 | return this 40 | } 41 | 42 | catch(callback) { 43 | if (!(callback instanceof Function)) { 44 | throw new TypeError('Form::catch - You must a function callback to catch.') 45 | } else { 46 | this.catchListeners.push(callback) 47 | } 48 | 49 | return this 50 | } 51 | 52 | submit (form, resolve, reject) { 53 | var submit = new Promise((resolve, reject) => { 54 | if (!this.validate(form)) { 55 | reject({error: defaultError}) 56 | } else { 57 | const xhr = new XMLHttpRequest() 58 | xhr.open('POST', this.url) 59 | xhr.onload = () => { 60 | const response = JSON.parse(xhr.responseText) 61 | if (response.error) { 62 | reject(response) 63 | } else { 64 | resolve(response) 65 | } 66 | } 67 | xhr.onerror = () => reject({error: defaultError}) 68 | xhr.send(new FormData(form)) 69 | } 70 | }).then((response) => { 71 | const event = {target: form, message: (this.messages.success ? this.messages.success : response.message)} 72 | 73 | if (response.data) { 74 | event.data = response.data 75 | } 76 | 77 | this.thenListeners.map((emit) => { 78 | emit(event) 79 | }) 80 | 81 | if (resolve) { 82 | resolve(event) 83 | } 84 | }).catch((response) => { 85 | const event = {target: form, error: (this.messages.error ? this.messages.error : response.error)} 86 | 87 | if (response.data) { 88 | event.data = response.data 89 | } 90 | 91 | this.catchListeners.map((emit) => { 92 | emit(event) 93 | }) 94 | 95 | if (reject) { 96 | reject(event) 97 | } 98 | }) 99 | 100 | return false 101 | } 102 | } 103 | 104 | export default Form 105 | -------------------------------------------------------------------------------- /src/GoogleScript/Code.gs: -------------------------------------------------------------------------------- 1 | /* 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Tech Cooperative (https://techcoop.group) 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | /* 26 | * Tech Cooperative - form-google-sheets 27 | * 28 | * Full Instructions: 29 | * https://github.com/techcoop/form-google-sheets 30 | * 31 | * TO USE: 32 | * 1) Change the values below as needed 33 | * 2) Make sure SHEET_NAME matches your spreadsheet exactly 34 | * 3) Click on Publish and select "Deploy as web app" 35 | * 4) Select new (or save over existing version) 36 | * 5) In "Execute the app as" select yourself 37 | * 6) In "Who has access to the app" Select "Anyone, even anonymous" 38 | * 7) Click Update 39 | * 8) Copy and paste the URL and use this to post information to 40 | * 9) Make sure form data fields match the header fields exactly 41 | * 42 | * TO TEST: 43 | * 1) Change the testData in test_post() 44 | * 2) Move to Run in the top menu) 45 | * 3) Click function test_post() 46 | * 47 | * Credits: 48 | * https://github.com/dwyl/html-form-send-email-via-google-script-without-server 49 | * 50 | */ 51 | 52 | // Configure sheet 53 | var SHEET_NAME = 'responses'; // CONFIGURE - The name of the sheet in your spreadsheet 54 | var NOTIFICATION_EMAIL = ''; // CONFIGURE - Should match the google account you are using 55 | var NOTIFICATION_ENABLED = false; // Should set to true if you want to email a notice 56 | var DEBUG = false; // Should set to true if you want to log to console 57 | 58 | // Configure defauly messages 59 | var INVALID_MESSAGE = 'There are errors with your form.'; 60 | var SUCCESS_MESSAGE = 'Thank you for your interest.'; 61 | 62 | // Setup fields that need validation 63 | var fields = { 64 | email: {required: true, message: 'You must provide an email address so that we can get in touch with you.'} 65 | } 66 | 67 | // TODO add regex patterns and mix / max length to validation setup for fields 68 | // TODO test injection techniques, google probably handling properly 69 | // TODO add captcha to form 70 | // TODO improve notification email to default with no values 71 | 72 | // TEST CONFIGURE - Change these values as needed, they should match your form fields with sensible test data 73 | var testData = { 74 | email: 'test@test.com', // Can change this too 75 | name: 'Some Guy', 76 | company_name: 'Some Company', 77 | description: 'Test Descriptions', 78 | subscribe: 'on' 79 | } 80 | 81 | function test_post() { 82 | if (!DEBUG) { 83 | Logger.log('Warning, you are running test_post with DEBUG turned off!') 84 | } 85 | 86 | doPost({ parameters: testData }) 87 | } 88 | 89 | function doPost(e) { 90 | try { 91 | // Validate defined fields 92 | var errors = {} 93 | for (field in fields) { 94 | if(fields[field].required) { 95 | if (e.parameters[field] === undefined || e.parameters[field] === '') { 96 | errors[field] = fields[field] 97 | } 98 | } 99 | } 100 | 101 | // If the form has errors 102 | if (Object.getOwnPropertyNames(errors).length !== 0) { 103 | return handleError(INVALID_MESSAGE, errors); 104 | } 105 | 106 | // Get sheet references 107 | try { 108 | var doc = SpreadsheetApp.getActiveSpreadsheet(); 109 | var sheet = doc.getSheetByName(SHEET_NAME); // select the responses sheet 110 | } catch(error) { 111 | Logger.log(error) 112 | return handleError('Error thrown doc or spreadhseet: ' + SHEET_NAME); 113 | } 114 | 115 | if (sheet === null) { 116 | return handleError('Could not find sheet: ' + SHEET_NAME); 117 | } 118 | 119 | // Get fields from first row of sheet 120 | var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0]; 121 | var nextRow = sheet.getLastRow() + 1; 122 | 123 | var row = []; 124 | var mailBody = ''; 125 | for (var i = 0; i < headers.length; i++) { 126 | var value; 127 | if (e.parameters[headers[i]] !== undefined) { 128 | value = e.parameters[headers[i]] 129 | } else if (headers[i] === 'timestamp'){ 130 | value = new Date(); 131 | } else { 132 | value = ''; 133 | } 134 | 135 | row.push(value); 136 | mailBody += mailRow(headers[i], value); 137 | } 138 | 139 | // Write to sheet 140 | sheet.getRange(nextRow, 1, 1, row.length).setValues([row]); 141 | 142 | // If notifcation, send email 143 | if (NOTIFICATION_ENABLED) { 144 | MailApp.sendEmail({ 145 | to: NOTIFICATION_EMAIL, 146 | subject: 'New Request' + (e.parameters.name ? ' ' + e.parameters.name : ''), 147 | replyTo: String(e.parameters.email), 148 | htmlBody: mailBody 149 | }); 150 | } 151 | 152 | var message = SUCCESS_MESSAGE 153 | return handleSuccess(message) 154 | 155 | } catch(error) { 156 | var message = 'Unknown error ocurred' 157 | Logger.log(error); 158 | return handleError(message, {request: e, error: error}) 159 | } 160 | } 161 | 162 | 163 | // ***** Utility functions ***** 164 | 165 | // Forms a basic content row for email 166 | function mailRow(name, value) { 167 | return '

' + name + '

' + value + '
' 168 | } 169 | 170 | // Gets response from object 171 | function getResponse(data) { 172 | return ContentService 173 | .createTextOutput(JSON.stringify(data)) 174 | .setMimeType(ContentService.MimeType.JSON); 175 | } 176 | 177 | // Wraps error 178 | function handleError(message, data) { 179 | var error = {error: message} 180 | if (data) { 181 | error['data'] = data 182 | } 183 | 184 | if (DEBUG) { 185 | Logger.log(error) 186 | } 187 | 188 | return getResponse(error) 189 | } 190 | 191 | // Wraps success 192 | function handleSuccess(message, data) { 193 | var success = {message: message} 194 | if (data) { 195 | success['data'] = data 196 | } 197 | 198 | if (DEBUG) { 199 | Logger.log(success) 200 | } 201 | 202 | return getResponse(success) 203 | } 204 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Form } from './Form' 2 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | entry: { 6 | 'form-google-sheets': './src/index.js' 7 | }, 8 | output: { 9 | path: path.resolve(__dirname, './out'), 10 | filename: '[name].js', 11 | library: 'FormGoogleSheets', 12 | libraryTarget: 'umd', 13 | }, 14 | devtool: 'sourcemap', 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | exclude: [/node_modules/], 20 | use: [{ 21 | loader: 'babel-loader', 22 | options: { presets: ['es2015'] }, 23 | }], 24 | } 25 | ] 26 | }, 27 | plugins: [ 28 | new webpack.optimize.UglifyJsPlugin({ 29 | compress: { 30 | warnings: false, 31 | drop_console: true, 32 | unsafe: true, 33 | }, 34 | }) 35 | ], 36 | } 37 | --------------------------------------------------------------------------------