├── .gitignore ├── .npmignore ├── examples ├── sample_vuetify_gform.png ├── sample_custom_vuetify_form.png └── vue-bootstrap.vue ├── package.json ├── src ├── vueFns.js └── extractData.js ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | node_modules -------------------------------------------------------------------------------- /examples/sample_vuetify_gform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Graphicito/Custom-Google-Form-Design/HEAD/examples/sample_vuetify_gform.png -------------------------------------------------------------------------------- /examples/sample_custom_vuetify_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Graphicito/Custom-Google-Form-Design/HEAD/examples/sample_custom_vuetify_form.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-gform", 3 | "version": "1.3.2", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Shane Graphicito", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^0.21.1", 13 | "lodash": "^4.17.20" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Graphicito/Custom-Google-Form-Design.git" 18 | }, 19 | "keywords": [ 20 | "google form", 21 | "custom google form", 22 | "free form", 23 | "quick form create", 24 | "customize google form", 25 | "style google form" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/vueFns.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').create(); 2 | 3 | const vueSubmitForm = async (gFormData, formData) => { 4 | let postData = gFormData.questions.map((i) => { 5 | if (i.type === "checkbox") { 6 | const selectedData = formData[i.title].map((j) => { 7 | return `${i.entry}=${j}&`; 8 | }); 9 | 10 | return [...selectedData]; 11 | } 12 | 13 | return `${i.entry}=${formData[i.title]}&`; 14 | }); 15 | 16 | postData = postData.flat(); 17 | postData = postData.join(""); 18 | 19 | return await axios.post(gFormData.formAction, postData) 20 | .then(() => { return { code: 'success' } }) 21 | .catch(() => { return { code: 'failed' } }) 22 | } 23 | 24 | module.exports = { 25 | vueSubmitForm 26 | } -------------------------------------------------------------------------------- /examples/vue-bootstrap.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const axios = require('axios'); 3 | const { getShortAnswers, getParagraphs, getMultipleChoice,getDropDown, getCheckBoxes, getLinearScale } = require('./src/extractData.js'); 4 | const { vueSubmitForm } = require('./src/vueFns.js'); 5 | 6 | const getHtml = async (formUrl) => { 7 | return await axios.get(formUrl).then(rsp => rsp.data); 8 | } 9 | 10 | const extractFbPublicLoadData = (html) => { 11 | let data = html.split('FB_PUBLIC_LOAD_DATA_ = ')[1]; 12 | data = data.substring(0, data.indexOf(';')); 13 | 14 | return JSON.parse(data); 15 | } 16 | 17 | const getBasicData = (form) => { 18 | // Title 19 | const formTitle = form[8] || ''; 20 | //Form description 21 | const formDescription = form[0] || ''; 22 | 23 | return { 24 | formTitle, 25 | formDescription, 26 | } 27 | }; 28 | 29 | const getByCategory = async (formUrl) => { 30 | const html = await getHtml(formUrl); 31 | const fBData = extractFbPublicLoadData(html); 32 | 33 | // Extract form that stores description and questions 34 | const form = fBData[1] || null; 35 | 36 | const { formTitle, formDescription } = getBasicData(form); 37 | 38 | //Form questions 39 | const formQuestions = form[1] || null; 40 | 41 | //Extract all form data 42 | let shortAnswers = getShortAnswers(formQuestions); 43 | let paragraphs = getParagraphs(formQuestions); 44 | let multipleChoice = getMultipleChoice(formQuestions); 45 | let dropDown = getDropDown(formQuestions); 46 | let checkBoxes = getCheckBoxes(formQuestions); 47 | let linearScale = getLinearScale(formQuestions); 48 | 49 | return { 50 | formAction: `https://docs.google.com/forms/u/0/d/${fBData[14]}/formResponse`, 51 | formTitle, 52 | formDescription, 53 | questions: { 54 | shortAnswers, 55 | paragraphs, 56 | multipleChoice, 57 | dropDown, 58 | checkBoxes, 59 | linearScale, 60 | } 61 | } 62 | }; 63 | 64 | const get = async (formUrl) => { 65 | const html = await getHtml(formUrl); 66 | const fBData = extractFbPublicLoadData(html); 67 | 68 | // Extract form that stores description and questions 69 | const form = fBData[1] || null; 70 | 71 | const { formTitle, formDescription } = getBasicData(form); 72 | 73 | //Form questions 74 | const formQuestions = form[1] || null; 75 | 76 | let extractedQuestions = []; 77 | 78 | formQuestions.forEach(q => { 79 | switch(q[3]){ 80 | case 0: extractedQuestions.push(getShortAnswers(formQuestions)); break; 81 | case 1: extractedQuestions.push(getParagraphs(formQuestions)); break; 82 | case 2: extractedQuestions.push(getMultipleChoice(formQuestions)); break; 83 | case 3: extractedQuestions.push(getDropDown(formQuestions)); break; 84 | case 4: extractedQuestions.push(getCheckBoxes(formQuestions)); break; 85 | case 5: extractedQuestions.push(getLinearScale(formQuestions)); break; 86 | default: break; 87 | } 88 | }) 89 | 90 | //Hacky way below to get unique entries without writing separate functions 91 | extractedQuestions = _.flatten(extractedQuestions); 92 | 93 | //Obtain unique entries 94 | let allEntries = new Set(extractedQuestions.map(i => i.entry)); 95 | 96 | //Convert back to array for mapping 97 | allEntries = [...allEntries] 98 | 99 | extractedQuestions = allEntries.map(i => { 100 | const found = extractedQuestions.find(q => q.entry === i); 101 | 102 | return {...found} 103 | }) 104 | 105 | return { 106 | formAction: `https://docs.google.com/forms/u/0/d/${fBData[14]}/formResponse`, 107 | formTitle, 108 | formDescription, 109 | questions: extractedQuestions 110 | }; 111 | } 112 | 113 | module.exports = { 114 | get, 115 | getBasicData, 116 | getByCategory, 117 | vueSubmitForm, 118 | } -------------------------------------------------------------------------------- /src/extractData.js: -------------------------------------------------------------------------------- 1 | /* 2 | Question types: 3 | 4 | 0: Short answers 5 | 1: Paragraph 6 | 2: Multiple Choice 7 | 3: Drop Down 8 | 4: Checkboxes 9 | 5: Linear Scale 10 | 7: Multiple Choice Grid, Tick Box Grid 11 | 9: Date 12 | 10: Time 13 | 13: File Upload (Enabling File Upload will force user to sign in) 14 | */ 15 | const getShortAnswers = (formQuestions) => { 16 | let shortAnswers = formQuestions.filter(q => q[3] === 0); 17 | shortAnswers = shortAnswers.map(q => { 18 | return { 19 | type: 'short-answer', 20 | entry: `entry.${q[4][0][0]}`, 21 | title: q[1], 22 | description: q[2], 23 | required: Boolean(q[4][0][2]), 24 | placeholder: '' 25 | } 26 | }) 27 | 28 | return shortAnswers; 29 | } 30 | 31 | const getParagraphs = (formQuestions) => { 32 | let paragraphs = formQuestions.filter(q => q[3] === 1); 33 | paragraphs = paragraphs.map(q => { 34 | return { 35 | type: 'paragraph', 36 | entry: `entry.${q[4][0][0]}`, 37 | title: q[1], 38 | description: q[2], 39 | required: Boolean(q[4][0][2]), 40 | placeholder: '' 41 | } 42 | }) 43 | 44 | return paragraphs; 45 | } 46 | 47 | const getMultipleChoice = (formQuestions) => { 48 | let multipleChoice = formQuestions.filter(q => q[3] === 2); 49 | multipleChoice = multipleChoice.map(q => { 50 | let choices = q[4][0][1]; 51 | 52 | choices = choices.map(choice => { 53 | if(choice[4] === 1){ 54 | return 'Others'; 55 | } 56 | 57 | return choice[0]; 58 | }) 59 | 60 | return { 61 | type: 'multiple-choice', 62 | entry: `entry.${q[4][0][0]}`, 63 | title: q[1], 64 | description: q[2], 65 | choices, 66 | required: Boolean(q[4][0][2]), 67 | } 68 | }) 69 | 70 | return multipleChoice; 71 | } 72 | 73 | const getDropDown = (formQuestions) => { 74 | let dropDown = formQuestions.filter(q => q[3] === 3); 75 | 76 | dropDown = dropDown.map(q => { 77 | let choices = q[4][0][1]; 78 | 79 | choices = choices.map(choice => choice[0]) 80 | 81 | return { 82 | type: 'dropdown', 83 | entry: `entry.${q[4][0][0]}`, 84 | title: q[1], 85 | description: q[2], 86 | choices, 87 | required: Boolean(q[4][0][2]), 88 | placeholder: '' 89 | } 90 | }) 91 | 92 | return dropDown; 93 | } 94 | 95 | const getCheckBoxes = (formQuestions) => { 96 | let checkBoxes = formQuestions.filter(q => q[3] === 4); 97 | 98 | checkBoxes = checkBoxes.map(q => { 99 | let choices = q[4][0][1]; 100 | 101 | choices = choices.map(choice => choice[0]); 102 | 103 | return { 104 | type: 'checkbox', 105 | entry: `entry.${q[4][0][0]}`, 106 | title: q[1], 107 | description: q[2], 108 | choices, 109 | required: Boolean(q[4][0][2]), 110 | } 111 | }) 112 | 113 | return checkBoxes; 114 | } 115 | 116 | const getLinearScale = (formQuestions) => { 117 | let linearScale = formQuestions.filter(q => q[3] === 5); 118 | 119 | linearScale = linearScale.map(q => { 120 | let totalChoices = q[4][0][1].length || 0; 121 | 122 | return { 123 | type: 'linear-scale', 124 | entry: `entry.${q[4][0][0]}`, 125 | title: q[1], 126 | description: q[2], 127 | minLabel: q[4][0][3][0], 128 | maxLabel: q[4][0][3][1], 129 | totalChoices, 130 | required: Boolean(q[4][0][2]), 131 | } 132 | }) 133 | 134 | return linearScale; 135 | } 136 | 137 | module.exports = { 138 | getShortAnswers, 139 | getParagraphs, 140 | getMultipleChoice, 141 | getDropDown, 142 | getCheckBoxes, 143 | getLinearScale, 144 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Form Customizer 2 | ### Beautify and customize your Google Form with custom HTML and CSS. 3 |
4 | 5 | ## Update 4/11/2020: 6 | #### Added placeholder attribute to the shortanswer paragraph, and dropdown objects. Used for model data binding in custom form if user wishes to add placeholder (which does not exist in Google Form). 7 |
8 | 9 | 10 | graphicito custom google formgraphicito custom google form 11 |
12 | Original Google Form (left) vs Customized Google Form (right) 13 | 14 | ___ 15 | 16 | ### Install the package 17 | `npm i custom-gform` 18 | 19 | Run `npm install` to install all dependencies only if you are cloning the project. 20 | 21 | ___ 22 | ### API 23 | | API | Description | Params| 24 | | ---- | ----------- | ------| 25 | | get | Retrieve all form data in the same order listed | none 26 | | getBasicData | Returns the form title and description | none 27 | | getByCategory | Retrieve all form data and categorize the questions into its own category (short answers, multiple choice...) | none 28 | vueSubmitForm | Submit form on Vue app with one line of code |(gFormData, formData) 29 | 30 | ___ 31 | 32 | ### Supported Question Types 33 | 34 | 0: Short answers 35 | 1: Paragraph 36 | 2: Multiple Choice 37 | 3: Drop Down 38 | 4: Checkboxes 39 | 5: Linear Scale 40 | 41 | ___ 42 | 43 | ### Example .get Output 44 | 45 | ```js 46 | // Works with both edit and preview link 47 | const formData = await get("Replace with your Google Form link"); 48 | 49 | { 50 | formTitle: 'Custom G Form', 51 | formDescription: 'Form Description', 52 | questions: [ 53 | { 54 | entry: 'entry.159023240', 55 | type: 'multiple-choice', 56 | title: 'Multiple choice', 57 | description: 'MC Description', 58 | choices: [ [Object], [Object], [Object] ], 59 | required: false 60 | }, 61 | { 62 | entry: 'entry.1862994755', 63 | title: 'Multiple choice', 64 | type: 'multiple-choice', 65 | description: 'MC Description REQUIRED', 66 | choices: [ [Object], [Object] ], 67 | required: true }, 68 | { 69 | entry: 'entry.1536591222', 70 | title: 'Short Answer', 71 | type: 'short-answer', 72 | description: 'Description: REQUIRED', 73 | placeholder: '', 74 | required: true 75 | } 76 | ] 77 | } 78 | ``` 79 | 80 | ### Example .getByCategory Output 81 | 82 | ```js 83 | // Works with both edit and preview link 84 | const formData = await getByCategory("Replace with your Google Form link"); 85 | { 86 | formAction: 'https://docs.google.com/forms/u/0/d/e/1FAIpQLSeHM3lr79IGiu57NR6lwUMqBZDKsp9C5IpzRApgLfdZX2gwkw/formResponse', 87 | formTitle: 'Custom G Form', 88 | formDescription: 'Form Description', 89 | questions: { 90 | shortAnswers: [ [Object], [Object] ], 91 | paragraphs: [ [Object], [Object] ], 92 | multipleChoice: [ [Object], [Object] ], 93 | dropDown: [ [Object] ], 94 | checkBoxes: [ [Object], [Object] ], 95 | linearScale: [ [Object] ] 96 | } 97 | } 98 | ``` 99 | ___ 100 | ### Return Object Info 101 | | Key | Description | 102 | | ---- | ----------- | 103 | | formAction | POST api to submit the form response 104 | | entry | Use this in the `name` attribute of each form input 105 | | required | Check for required form field 106 | ___ 107 | 108 | ### *NEW*: Submit forms with one line of code (VUE ONLY) 109 | 110 | Always validate your form and don't submit empty forms. Remember to import the api as well. 111 | 112 | ```js 113 | import { vueSubmitForm } from 'custom-gform'; 114 | ``` 115 | ```js 116 | methods: { 117 | /* 118 | Retrieve this.gFormData with the .get api and pass it to the first param 119 | Pass all v-model formData into the second param 120 | Check code in the next section to learn how to dynamically assign v-models for your form 121 | */ 122 | 123 | async submitForm() { 124 | // Returns a promise with {code: success/fail}! 125 | await vueSubmitForm(this.gFormData, this.formData); 126 | }, 127 | } 128 | ``` 129 | Dynamically assigns v-model (view example in examples/vue-bootstrap.vue for more info) 130 | ```js 131 | mounted(){ 132 | const dynamicVModel = this.gFormData.questions.map((i) => i.title); 133 | 134 | dynamicVModel.forEach((i) => { 135 | this.$set(this.formData, i, null); 136 | }); 137 | } 138 | ``` 139 | ___ 140 | 141 | ### Vue + Bootstrap Example 142 | Check out the `examples/vue-bootstrap.vue` folder in GitHub for example usage 143 | ___ 144 | ### Credits 145 | Custom GForm is created and maintained by [Graphicito](http://graphicito.com). For custom web development solution or Google Form customization, contact us at [Contact Graphicito](http://graphicito.com/contact). 146 | 147 | ___ 148 | ### Changelog 149 | ``` 150 | 4/11/2020: Added placeholder attribute to the shortanswer paragraph, and dropdown objects. Used for model data binding in custom form if user wishes to add placeholder (which does not exist in Google Form). 151 | 152 | 3/11/2020: Added easy vue form submit without importing axios and name attribute dependency. 153 | ``` --------------------------------------------------------------------------------