├── .clasp.json ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── src ├── Code.js ├── EmailTemplate.html └── appsscript.json /.clasp.json: -------------------------------------------------------------------------------- 1 | { "scriptId": "1CAyzGbXdwMlko81SbJAjRp7ewxhyGKhDipDK4v8ZvlpYqrMAAzbFNccL", "rootDir": "src" } 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | draft.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All the new version changes to the FormEasy library will be documented in this file. Find guidelines in the [readme](https://github.com/Basharath/FormEasy/blob/master/README.md). 4 | 5 | ## Version 2 - 2022-08-22 6 | 7 | ### Added 8 | 9 | - Support for Google reCaptcha to avoid spam submissions - [details](https://github.com/Basharath/FormEasy#google-recaptcha-v2) 10 | 11 | ### Changed 12 | 13 | - Latest submissions will be logged to the top rows instead bottom 14 | 15 | ## Version 1 - 2022-06-22 16 | 17 | The first public version of the FormEasy library 18 | 19 | ### Added 20 | 21 | Form submssion using Google Apps Script 22 | 23 | Features: 24 | 25 | - Form fields can be customized 26 | - Form subject can be customized 27 | - Form heading can be customized 28 | - Form submission notification can be sent to the desired email 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to FormEasy 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Code of Conduct 9 | 10 | ### Our Pledge 11 | 12 | In the interest of fostering an open and welcoming environment, we as 13 | contributors and maintainers pledge to making participation in our project and 14 | our community a harassment-free experience for everyone, regardless of age, body 15 | size, disability, ethnicity, gender identity and expression, level of experience, 16 | nationality, personal appearance, race, religion, or sexual identity and 17 | orientation. 18 | 19 | ### Our Standards 20 | 21 | Examples of behavior that contributes to creating a positive environment 22 | include: 23 | 24 | - Using welcoming and inclusive language 25 | - Being respectful of differing viewpoints and experiences 26 | - Gracefully accepting constructive criticism 27 | - Focusing on what is best for the community 28 | - Showing empathy towards other community members 29 | 30 | Examples of unacceptable behavior by participants include: 31 | 32 | - The use of sexualized language or imagery and unwelcome sexual attention or 33 | advances 34 | - Trolling, insulting/derogatory comments, and personal or political attacks 35 | - Public or private harassment 36 | - Publishing others' private information, such as a physical or electronic 37 | address, without explicit permission 38 | - Other conduct which could reasonably be considered inappropriate in a 39 | professional setting 40 | 41 | ### Our Responsibilities 42 | 43 | Project maintainers are responsible for clarifying the standards of acceptable 44 | behavior and are expected to take appropriate and fair corrective action in 45 | response to any instances of unacceptable behavior. 46 | 47 | Project maintainers have the right and responsibility to remove, edit, or 48 | reject comments, commits, code, wiki edits, issues, and other contributions 49 | that are not aligned to this Code of Conduct, or to ban temporarily or 50 | permanently any contributor for other behaviors that they deem inappropriate, 51 | threatening, offensive, or harmful. 52 | 53 | ### Scope 54 | 55 | This Code of Conduct applies both within project spaces and in public spaces 56 | when an individual is representing the project or its community. Examples of 57 | representing a project or community include using an official project e-mail 58 | address, posting via an official social media account, or acting as an appointed 59 | representative at an online or offline event. Representation of a project may be 60 | further defined and clarified by project maintainers. 61 | 62 | ### Enforcement 63 | 64 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 65 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 66 | complaints will be reviewed and investigated and will result in a response that 67 | is deemed necessary and appropriate to the circumstances. The project team is 68 | obligated to maintain confidentiality with regard to the reporter of an incident. 69 | Further details of specific enforcement policies may be posted separately. 70 | 71 | Project maintainers who do not follow or enforce the Code of Conduct in good 72 | faith may face temporary or permanent repercussions as determined by other 73 | members of the project's leadership. 74 | 75 | ### Attribution 76 | 77 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 78 | available at [http://contributor-covenant.org/version/1/4][version] 79 | 80 | [homepage]: http://contributor-covenant.org 81 | [version]: http://contributor-covenant.org/version/1/4/ 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Basharath 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

FormEasy

2 | 3 |

4 | 5 | FormEasy licence 6 | 7 | 8 | FormEasy forks 9 | 10 | 11 | FormEasy stars 12 | 13 | 14 | FormEasy issues 15 | 16 | 17 | FormEasy pull-requests 18 | 19 |

20 | 21 | FormEasy is a free and open source apps script library that lets you receive forms from your static sites very easily. 22 | 23 | Script ID: `1CAyzGbXdwMlko81SbJAjRp7ewxhyGKhDipDK4v8ZvlpYqrMAAzbFNccL` 24 | 25 | ## Adding FormEasy library to Apps Script 26 | 27 | 1. Open a new Google sheets file(this is where your form data gets stored) 28 | 2. From the menu bar click Extensions > Apps Script and it opens up a new apps script file 29 | 3. In the left bar of apps script file click `+` button beside `Libraries` 30 | 4. Add the `Script ID` listed above and click `Look up` button and select the latest version. Note the identifier it is going to be used to invoke the functions in the library and finally click `Add` button. 31 | 32 | Now you can use `FormEasy` object inside the apps script file. 33 | 34 | ## Usage 35 | 36 | Clear the default function if any in the apps script file and add the below function. 37 | 38 | **Simplest case** 39 | 40 | ```js 41 | function doPost(req) { 42 | FormEasy.setEmail('youremail@domain.com'); // To receive email notification(optional) 43 | return FormEasy.action(req); // Mandatory to return action method 44 | } 45 | ``` 46 | 47 | The default data fields are: name, email and message. To add more, use `setFields` method as shown below. 48 | 49 | **With more customizations** 50 | 51 | ```js 52 | function doPost(req) { 53 | FormEasy.setSheet('Sheet1'); // Optional 54 | FormEasy.setEmail('youremail@domain.com'); // To receive email notification(optional) 55 | FormEasy.setSubject('Email subject'); // Optional 56 | FormEasy.setFormHeading('Form heading inside email'); // Optional 57 | FormEasy.setFields('name', 'email', 'website', 'message', ...); // Optional(name, email, messsage are default) 58 | return FormEasy.action(req); // It should be at the end and return it 59 | } 60 | ``` 61 | 62 | After adding the above function click the `Deploy` button at top right corner and select **New deployment** and select type to `Web app` from the gear icon. 63 | 64 | Select/fill the below options 65 | 66 | - Description(optional), 67 | - Execute as `Me(you email)` 68 | - Who has access `Anyone` 69 | 70 | Click `Deploy` button(authorize the script if you haven't done before) and you will get a URL under `Web app`, copy that and it is going to be the end point for submitting the POST request. 71 | 72 | Note: You need not make _New deployment_ everytime if you want to use the same web app URL. Select **Manage deployments** and update the version to keep the same URL. 73 | 74 | ## Form submission using `fetch` 75 | 76 | ```js 77 | const data = { 78 | name: 'John', 79 | email: 'john@domain.com', 80 | message: 'Receiving forms is easy and simple now!', 81 | }; 82 | 83 | const url = 'https://script.google.com/macros/s//exec'; 84 | 85 | fetch(url, { 86 | method: 'POST', 87 | headers: { 88 | 'Content-Type': 'text/plain;charset=utf-8', 89 | }, 90 | body: JSON.stringify(data), 91 | }) 92 | .then((res) => res.json()) 93 | .then((data) => console.log('data', data)) 94 | .catch((err) => console.log('err', err)); 95 | ``` 96 | 97 | Note: The keys of the `data` object should match with the fields that are set using `setFields` method in the apps script file. The default keys are `name`, `email` and `message`. 98 | 99 | Article: https://devapt.com/formspree-alternative-formeasy 100 | 101 | ## Demo submission with live Google sheet 102 | 103 | Here is the [demo code](https://stackblitz.com/edit/js-55dzc8?file=index.html,index.js) and the live [Google sheet](https://docs.google.com/spreadsheets/d/13sGrLUk0ScU1qfRyOZzFG5pnksh7IAiTJw1Eio1jfaE/edit#gid=0) to get an idea on how this FormEasy library helps in receiving forms. 104 | 105 | ## Captcha validation 106 | 107 | FormEasy supports multiple captcha providers to allow you to prevent unverified submissions by robots. Each provider is unique and requires a unique configuration. Please refer to the documentation below to enable a specific captcha provider. 108 | 109 | ### Google reCAPTCHA V2 110 | 111 | 1. Register a site and get your secret key, and site key: [https://www.google.com/recaptcha/admin/create](https://www.google.com/recaptcha/admin/create) 112 | 113 | 2. In your apps script file, inside function `doPost`, add the following configuration: 114 | 115 | ```js 116 | function doPost(req) { 117 | // ... 118 | FormEasy.setRecaptcha('YOUR_SECRET_KEY'); // To validate reCAPTCHA 119 | // ... 120 | return FormEasy.action(req); // Mandatory to return action method 121 | } 122 | ``` 123 | 124 | 3. On your website, add the reCAPTCHA library at the end of the `` tag: 125 | 126 | ```html 127 | 128 | 129 | 130 | 131 | 132 | ``` 133 | 134 | 4. Add reCAPTCHA input into your form: 135 | 136 | ```html 137 |
138 | ``` 139 | 140 | 5. You should see `I am not a robot` box on your site. If you don't, please refer to [reCAPTCHA Docs](https://developers.google.com/recaptcha/docs/display) for debugging. 141 | 142 | 6. Inside your `fetch()` method, add a reCAPTCHA response from the input: 143 | 144 | ```js 145 | const data = { 146 | // ... 147 | gCaptchaResponse: document.getElementById('g-recaptcha-response').value, 148 | }; 149 | 150 | // ... 151 | ``` 152 | 153 | ### Google reCAPTCHA V3 154 | 155 | Steps 1 & 2 same as above. 156 | 157 | 3. On your website, add the reCAPTCHA library at the end of the `` tag: 158 | 159 | ```html 160 | 161 | 162 | 163 | 164 | 165 | ``` 166 | 167 | 4. Read the form data, reCAPTCHA V3 response token and send the request. 168 | 169 | ```js 170 | const siteKey = ''; 171 | 172 | const url = 'https://script.google.com/macros/s//exec'; 173 | 174 | function handleSubmit(event) { 175 | event.preventDefault(); 176 | 177 | // Make an API call to get the reCAPTCHA token 178 | grecaptcha.ready(function () { 179 | grecaptcha.execute(siteKey, { action: 'submit' }).then(function (token) { 180 | // Add the reCAPTCHA token to the form data 181 | data.gCaptchaResponse = token; 182 | data.name = document.getElementById('name').value; 183 | data.website = document.getElementById('website').value; 184 | data.email = document.getElementById('email').value; 185 | data.message = document.getElementById('message').value; 186 | 187 | fetch(url, { 188 | method: 'POST', 189 | headers: { 190 | 'Content-Type': 'text/plain;charset=utf-8', 191 | }, 192 | body: JSON.stringify(data), 193 | }) 194 | .then((res) => res.json()) 195 | .then((data) => console.log('data', data)) 196 | .catch((err) => console.log('err', err)); 197 | }); 198 | }); 199 | } 200 | 201 | document.getElementById('').addEventListener('submit', handleSubmit); 202 | ``` 203 | 204 | ## Video instructions 205 | 206 | To see all the above instructions step by step, check this quick [demo video](https://www.youtube.com/watch?v=0u75mtnhifM/). 207 | 208 | ## FAQs 209 | 210 |
211 | 1. Is it safe to grant permission to the apps script file while using FormEasy library? 212 | 213 | Yes, it is completely safe. 214 | 215 | FormEasy code doesn't interact with any remote servers. You can check the source code of the FormEasy library using its ScriptID. 216 | 217 | Google shows it unsafe because it hasn't verified the script. Even if you write your own script and grant permission the same message will be shown. 218 | 219 |
220 | 221 |
222 | 2. Can I customize FormEasy script? 223 | 224 | 225 | Yes. You're free to customize any part of the FormEasy script and deploy on your own to reflect the same. 226 | 227 | If you want even others to use your customizations then you can contribute your code and once verified it will be pushed to the main script. You can check [contributing guidelines](https://github.com/Basharath/FormEasy/blob/master/CONTRIBUTING.md). 228 | 229 |
230 | 231 |
232 | 3. What are the limitations of FormEasy? 233 | 234 | 235 | There are no specific limitations for FormEasy library. 236 | 237 | But Google Apps Script limits the email to 100/day and script run time to 6min/execution. You can see more about those [here](https://developers.google.com/apps-script/guides/services/quotas) 238 | 239 |
240 | 241 | ## Contributing 242 | 243 | Pull Requests are always welcome! 244 | 245 | If you wish to contribute using Github, you can work on any feature ideas you have or any bug fixes if you have noticed. 246 | 247 | After your PR gets merged, you'll be apparing on the [contributors page](https://github.com/Basharath/FormEasy/graphs/contributors). 248 | 249 | - Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and [open a pull request](https://github.com/Basharath/FormEasy/compare). 250 | 251 | - Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details on CODE OF CONDUCT, and the process for submitting pull requests. 252 | 253 | ## License 254 | 255 | FormEasy is distributed using the MIT License. Check the [License details](https://github.com/Basharath/FormEasy/blob/master/LICENSE). 256 | 257 | ## Support 258 | 259 | If you found this library helpful, please give a star ⭐️ 260 | 261 | If you like this open source work, consider supporting with a coffee ↓ 262 | 263 |
264 | 265 | 266 | 267 |
268 | -------------------------------------------------------------------------------- /src/Code.js: -------------------------------------------------------------------------------- 1 | let sheetName = ''; 2 | let emailSubject = 'New submission using FormEasy'; 3 | let formHeading = 'Form submission - FormEasy'; 4 | let email = ''; 5 | let fields = []; 6 | let captcha = null; 7 | 8 | /** 9 | * @param {String} name Name of the sheet to log the data 10 | */ 11 | function setSheet(name) { 12 | sheetName = name; 13 | } 14 | 15 | /** 16 | * @param {String} id Email ID to which message has to be sent 17 | */ 18 | function setEmail(id) { 19 | email = id; 20 | } 21 | 22 | /** 23 | * @param {String} subject Subject of the email 24 | */ 25 | function setSubject(subject) { 26 | emailSubject = subject; 27 | } 28 | 29 | /** 30 | * @param {String} heading Heading of the form 31 | */ 32 | function setFormHeading(heading) { 33 | formHeading = heading; 34 | } 35 | 36 | /** 37 | * @param {string} fieldsArr Fields in the contact form as string params 38 | */ 39 | function setFields(...fieldsArr) { 40 | fields = [...fieldsArr]; 41 | 42 | const length = fields.length; 43 | 44 | let sheet; 45 | 46 | if (!sheetName) { 47 | sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); 48 | } else sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); 49 | 50 | const firstRow = sheet.getDataRange().getValues()[0]; 51 | 52 | // Checking if columns in sheet and fields are matching 53 | if (firstRow.toString() !== '') { 54 | if ( 55 | firstRow[0].toLowerCase() === fields[0].toLowerCase() && 56 | firstRow[firstRow.length - 2].toString().toLowerCase() === fields[length - 1].toString().toLowerCase() 57 | ) { 58 | return; 59 | } 60 | if (firstRow.length > length + 1) sheet.getRange(1, 1, 1, 30).clearContent(); // Clearing upto 30 columns 61 | } 62 | 63 | const formatFirstLetter = (str) => str[0].toUpperCase() + str.slice(1); 64 | 65 | for (let idx = 0; idx < length; idx++) { 66 | sheet.getRange(1, idx + 1).setValue(formatFirstLetter(fields[idx])); 67 | if (idx === length - 1) { 68 | sheet.getRange(1, idx + 2).setValue('Date'); 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Google reCAPTCHA V2 implementation 75 | * 76 | * @param {String} secretKey Private key of reCAPTCHA site 77 | */ 78 | function setRecaptcha(secretKey) { 79 | captcha = { type: 'recaptcha_v2', data: { secretKey } }; 80 | } 81 | 82 | /** 83 | * @param {Object} req POST request object 84 | * @return {Object} response to the POST request 85 | */ 86 | function action(req) { 87 | let { postData: { contents, type } = {} } = req; 88 | let response = {}; 89 | 90 | let jsonData; 91 | 92 | try { 93 | jsonData = JSON.parse(contents); 94 | } catch (err) { 95 | response = { 96 | status: 'error', 97 | message: 'Invalid JSON format', 98 | }; 99 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 100 | } 101 | 102 | if (captcha) { 103 | switch (captcha.type) { 104 | case 'recaptcha_v2': 105 | const siteKey = jsonData['gCaptchaResponse']; 106 | 107 | if (!siteKey) { 108 | response = { 109 | status: 'error', 110 | message: "reCAPTCHA verification under key 'gCaptchaResponse' is required.", 111 | }; 112 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 113 | } 114 | 115 | const captchaResponse = UrlFetchApp.fetch('https://www.google.com/recaptcha/api/siteverify', { 116 | method: 'post', 117 | payload: { 118 | response: siteKey, 119 | secret: captcha.data.secretKey, 120 | }, 121 | }); 122 | 123 | const captchaJson = JSON.parse(captchaResponse.getContentText()); 124 | 125 | if (!captchaJson.success) { 126 | response = { 127 | status: 'error', 128 | message: 'Please tick the box to verify you are not a robot.', 129 | }; 130 | 131 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 132 | } 133 | 134 | break; 135 | default: 136 | // Captcha not enabled 137 | } 138 | } 139 | 140 | let logSheet; 141 | 142 | const allSheets = SpreadsheetApp.getActiveSpreadsheet() 143 | .getSheets() 144 | .map((s) => s.getName()); 145 | 146 | if (sheetName) { 147 | const sheetExists = allSheets.includes(sheetName); 148 | if (!sheetExists) { 149 | response = { 150 | status: 'error', 151 | message: 'Invalid sheet name', 152 | }; 153 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 154 | } 155 | logSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); 156 | } else { 157 | logSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); 158 | } 159 | 160 | if (fields.length < 1) { 161 | setFields('name', 'email', 'message'); 162 | } 163 | 164 | const length = fields.length; 165 | 166 | const lastRow = logSheet.getLastRow(); 167 | 168 | const now = new Date(); 169 | const date = 170 | now.toLocaleDateString('en-US', { 171 | year: 'numeric', 172 | month: 'long', 173 | day: 'numeric', 174 | }) + 175 | ' ' + 176 | now.toLocaleTimeString('en-US'); 177 | 178 | // Inserting a row after the first row 179 | logSheet.insertRowAfter(1); 180 | 181 | // Filling the latest data in the second row 182 | for (let idx = 0; idx < length; idx++) { 183 | logSheet.getRange(2, idx + 1).setValue(jsonData[fields[idx]]); 184 | if (idx === length - 1) { 185 | logSheet.getRange(2, idx + 2).setValue(date); 186 | } 187 | } 188 | 189 | const emailData = fields.reduce((a, c) => ({ ...a, [c]: jsonData[c] }), {}); 190 | const htmlBody = HtmlService.createTemplateFromFile('EmailTemplate'); 191 | htmlBody.data = emailData; 192 | htmlBody.formHeading = formHeading; 193 | 194 | const emailBody = htmlBody.evaluate().getContent(); 195 | 196 | if (email) { 197 | MailApp.sendEmail({ 198 | to: email, 199 | subject: emailSubject, 200 | htmlBody: emailBody, 201 | replyTo: jsonData.email, 202 | }); 203 | } 204 | 205 | response = { 206 | status: 'OK', 207 | message: 'Data logged successfully', 208 | }; 209 | 210 | return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON); 211 | } 212 | -------------------------------------------------------------------------------- /src/EmailTemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 73 | 74 | 75 | 76 |
77 |
78 |

79 | 80 |

81 | 82 | str[0].toUpperCase() + str.slice(1); ?> 83 | 87 | 88 | 89 |
90 |

91 | 92 |

93 |

94 | 95 |

96 |
97 | 98 | 99 |
100 |

Submitted date: 101 | 102 |

103 |
104 | 105 |
106 |
107 |

108 | Created using: FormEasy 109 |

110 |
111 |
112 |
113 |
114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "Asia/Kolkata", 3 | "dependencies": {}, 4 | "exceptionLogging": "STACKDRIVER", 5 | "runtimeVersion": "V8" 6 | } 7 | --------------------------------------------------------------------------------