Skip to content

Instantly share code, notes, and snippets.

@jameswilson
Last active April 27, 2021 03:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jameswilson/0dea86b63741ee4a723cc568c09796ea to your computer and use it in GitHub Desktop.
Save jameswilson/0dea86b63741ee4a723cc568c09796ea to your computer and use it in GitHub Desktop.
Google Sheets XHR insert via GET request, respond with JSON status.
/*!
* @file
* Google Sheets XHR insert script.
*
* From: https://gist.github.com/jameswilson/0dea86b63741ee4a723cc568c09796ea
*
* Setup instructions:
*
* 1. Create a new Google Sheet and add column names in Row 1.
* 2. Click "Tools" > "Script editor" from the toolbar.
* 3. Copy/Paste this Code.js script into the Google Scripts UI.
* 4. Click "Publish" > "Deploy as web app..." from the toolbar.
* 5. Ensure Project version is set to "1". (Choose "New" if
* deploying code changes.)
* 6. Ensure "Execute the app as" is set to "Me".
* 7. Ensure "Who has access" is set to "Anyone, Anonymous".
* 8. Save the generated "Current web app URL" for use in your
* HTML form in the `google_script` variable.
* 9. Create and customize HTML form to match the spreadsheet
* columns defined in step 1. (See example form.html in gist).
*
* Inspired by https://medium.com/@dmccoy/how-to-submit-an-html-form-to-google-sheets-without-google-forms-b833952cc175
*/
/**
* Sheet name (aka tab name) in Google sheets.
*/
var SHEET_NAME = "Form Responses";
/**
* Globally persistent properties across requests.
*/
var SCRIPT_PROP = PropertiesService.getScriptProperties();
/**
* One time setup command to initialize Sheets <=> Scripts integration.
*
* After pasting this script into Google Scripts UI, and publishing. Run the
* `setup` command once from within the Google Sheet UI where the script is
* attached.
*/
function setup() {
var doc = SpreadsheetApp.getActiveSpreadsheet();
SCRIPT_PROP.setProperty("key", doc.getId());
}
/**
* Allows the Google Sheet to handle GET requests.
*/
function doGet(e) {
return handleResponse(e);
}
/**
* Insert data into the Google Sheet and prepare the JSON response.
*
* Variables are parsed out from the GET request's query string, sent in the
* format: key1=value1&key2=value2, where key1 and key2 match exactly to
* specific column names from the spreadsheet. Key names are case sensitive and
* space characters should be avoided; CamelCase or snake_case are recommended.
*/
function handleResponse(e) {
// Google's LockService[1] prevents concurrent access overwritting data.
// [1] http://googleappsdeveloper.blogspot.co.uk/2011/10/concurrency-and-google-apps-script.html
// The public lock blocks all requests globally while data is being inserted.
var lock = LockService.getPublicLock();
// Wait to obtain a lock for up to 30 seconds before conceding defeat.
lock.waitLock(30000);
try {
// Prep the spreadsheet where this script is attached for data insert.
var doc = SpreadsheetApp.openById(SCRIPT_PROP.getProperty("key"));
var sheet = doc.getSheetByName(SHEET_NAME);
// Assume spreadsheet header row that contains the column names is in row 1.
// The header row may be overridden by passing header_row=N (where N is the
// integer row number) via the GET request data.
var headRow = e.parameter.header_row || 1;
var headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
var nextRow = sheet.getLastRow() + 1;
var row = [];
// Loop through the header row's column names.
for (i in headers) {
// If the spreadsheet contains a 'Timestamp' column, then insert the
// current datetime.
if (headers[i] == "Timestamp") {
row.push(new Date());
// Otherwise use column name to get data.
} else {
row.push(e.parameter[headers[i]]);
}
}
// It is more efficient to set values as [][] array than individually.
sheet.getRange(nextRow, 1, 1, row.length).setValues([row]);
// On success, return a simple JSON response with the row number that was
// inserted.
return ContentService
.createTextOutput(JSON.stringify({"result":"success", "row": nextRow}))
.setMimeType(ContentService.MimeType.JSON);
} catch(e) {
// On error, return the payload sent via GET request for debugging.
return ContentService
.createTextOutput(JSON.stringify({"result":"error", "error": e}))
.setMimeType(ContentService.MimeType.JSON);
} finally {
lock.releaseLock();
}
}
<!--
This example leverages modern ES6 async features with fetch()
API which may require polyfills for older browsers.
-->
<style>
body {
display: flex;
align-items: center;
align-content: center;
justify-content: center;
font-family: sans-serif;
}
label {
display: block;
}
label span {
color: red;
}
form {
padding: 2rem;
border: 1px solid;
border-radius: 3px;
}
input, textarea, button {
padding: .4rem .2rem;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 1rem;
min-width: 17rem;
}
button {
background: dodgerblue;
border-color: dodgerblue;
color: white;
}
button,
[role="listitem"] + [role="listitem"] {
margin-top: 1rem;
}
</style>
<form id="formElem">
<div role="list">
<div role="listitem">
<label for="Email" role="heading" aria-level="3">Email address <span aria-label="Required question">*</span></label>
<input type="email" autocomplete="email" aria-label="Your email" name="Email" id="Email" value="" required>
</div>
<div role="listitem">
<label for="Name" role="heading" aria-level="3">Your name <span aria-label="Required question">*</span></label>
<input type="text" autocomplete="name" aria-label="Your name" name="Name" id="Name" value="" required>
</div>
<div role="listitem">
<label for="Phone" role="heading" aria-level="3">Phone number</label>
<input type="text" autocomplete="tel-national" aria-label="Phone number" name="Phone" id="Phone" value="">
</div>
<div role="listitem">
<label for="Comment" role="heading" aria-level="3">Tell us more <span aria-label="Required question">*</span></label>
<textarea rows="3" aria-label="Tell us more about this photo" name="Comment" id="Comment" required></textarea>
</div>
<div role="listitem">
<label for="Url" role="heading" aria-level="3">Url <span aria-label="Required question">*</span></label>
<input type="url" autocomplete="off" aria-label="Url" name="Url" id="Url" value="" required>
</div>
</div>
<button type="submit">Submit</button>
</form>
<script>
const google_script = 'https://script.google.com/macros/s/XXXXXXXXXXXXXXXXXXXXX/exec';
formElem.addEventListener('submit', async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const params = new URLSearchParams(formData);
const url = google_script + '?' + params.toString();
let response = await fetch(url);
let result = await response.json();
console.log(result);
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment