1&&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 |
52 |
53 |
57 |
58 |
59 | From onclick event
60 |
71 |
72 |
94 |
95 |
104 |
105 |
106 |
110 |
111 |
112 | Custom messages and validation
113 |
147 |
148 |
155 |
156 |
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 |
--------------------------------------------------------------------------------