├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
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 | = formHeading ?>
80 |
81 |
82 | const formatString = (str) => str[0].toUpperCase() + str.slice(1); ?>
83 | const now = new Date();
84 | const date = now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
85 | + ' ' + now.toLocaleTimeString('en-US');
86 | ?>
87 |
88 | for (const [key, value] of Object.entries(data)) { ?>
89 |
90 |
91 | = formatString(key) ?>
92 |
93 |
94 | = value ?>
95 |
96 |
97 | } ?>
98 |
99 |
100 |
Submitted date:
101 | = date ?>
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 |
--------------------------------------------------------------------------------