Trang chủ Khóa họcWeb Lập trình thay đổi CSS cho Google Form

Lập trình thay đổi CSS cho Google Form

Bởi tr1nh

Google Form là một công cụ cho phép người dùng tạo các biểu mẫu với mục đích thăm dò ý kiến, khảo sát hoặc thu thập thông tin. Dữ liệu thu thập được từ Google Form có thể thống kê ra biểu đồ trực quan và có thể xuất bản ra Google Spreadsheet hoặc Microsoft Excel.

Tuy nhiên, khi nhúng Google Form vào trang web của mình, một số bạn sẽ không thích khoảng cách quá lớn giữa các ô nhập liệu. Điều này dẫn đến form được nhúng vào trang web sẽ bị cuộn lại mặc dù form có rất ít ô nhập liệu.

Do đó, trong bài viết này, mình sẽ hướng dẫn các bạn sửa lại CSS của Google Form, hay nói chính xác hơn là chúng ta sẽ tự làm một công cụ generate Bootstrap form từ Google Form.

Chuẩn bị

  • Máy tính có kết nối internet
  • Đã cài Nodejs
  • Kiến thức về HTML, CSS, JavaScript

Thực hiện

Tìm hiểu dữ liệu của Google Form

Đầu tiên, chúng ta phải có ý tưởng làm sao để lấy mã nguồn HTML của Google Form, làm sao để lấy dữ liệu người dùng nhập vào và gửi đến kết quả của Google Form.

Để tìm hiểu các vấn đề trên, chúng ta sẽ tiến hành tạo thử một biểu mẫu mới. Sau khi nhấn nút xem thử, kết quả sẽ được mở trong một thẻ mới. Tiếp đó, các bạn mở công cụ Inspect Element để xem các thành phần HTML và tìm đến thành phần form.

Trong thẻ form, chúng ta thấy thuộc tính action với giá trị là một đường dẫn liên kết, đây chính là địa chỉ để gửi dữ liệu trong form đến kết quả Google Form khi có sự kiện submit.

Thường thì sự kiện submit của form sẽ lấy từng giá trị của các thành phần input nằm trong form rồi gửi chúng, các giá trị này được xác định bằng thuộc tính name của thành phần input. Nhưng trong thẻ input chúng ta không tìm thấy thuộc tính name, làm sao bây giờ?

Cách đơn giản nhất là các bạn chuyển sang tab Network, sau đó chọn Persit log để không bị xóa log cũ khi chuyển trang, tiếp theo nhập vào dữ liệu và nhấn submit. 

Một loạt các request sẽ xuất hiện, chúng ta tìm đến request có tên là formRespone, trong phần chi tiết, chọn request để xem các giá trị được gửi dưới dạng key-value, các key này chính là thuộc tính name trong thẻ input.

Cách còn lại chính là tìm các dữ liệu có dấu hiệu khả nghi ::smile:: trong thành phần HTML. Sau khi biết tên của các thuộc tính name ở các đầu tiên chúng ta dễ dàng bắt gặp hai nơi chứa dữ liệu cho thẻ input.

Đầu tiên là các thẻ div có lớp m2, mỗi một thẻ input sẽ được bao bởi thẻ này, và trong thẻ này có thuộc tính data-params có chứa giá trị giống với các key lấy được từ request.

Thứ hai, ở thẻ script thứ hai từ cuối trang đếm lên, các bạn cũng sẽ thấy một đoạn mã khai báo mảng cho biến FB_PUBLIC_LOAD_DATA, trong mảng này cũng chứa dữ liệu giống với các key từ request.

Vậy là chúng ta đã xong giai đoạn khó khăn rồi.

Tạo thử một form

Thử tạo ngay một trang web để áp dụng các giá trị vừa tìm được nào:

<!DOCTYPE html>
<html lang="vi">
  <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Contact Information</title>
   <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
   <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
   <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
 </head>
  <body class="bg-transparent">
   <form action="https://docs.google.com/forms/u/0/d/e/1FAIpQLScu_cAxXnxLoKr1yeFrEthoJrrP-43ztL4siCEZi6cZ_e9ozQ/formResponse"
   method="post">
     <div class="card" id="section-0">
       <div class="card-body">
         <div class="form-group">
           <label for="entry.2005620554">Name</label>
           <input class="form-control" id="entry.2005620554" name="entry.2005620554"
           type="text">
         </div>
         <div class="form-group">
           <label for="entry.1045781291">Email</label>
           <input class="form-control" id="entry.1045781291" name="entry.1045781291"
           type="text">
         </div>
         <div class="form-group">
           <label for="entry.1065046570">Address</label>
           <textarea class="form-control" id="entry.1065046570" name="entry.1065046570"
           rows="2"></textarea>
         </div>
         <div class="form-group">
           <label for="entry.1166974658">Phone number</label>
           <input class="form-control" id="entry.1166974658"
           name="entry.1166974658" type="text">
         </div>
         <div class="form-group">
           <label for="entry.839337160">Comments</label>
           <textarea class="form-control" id="entry.839337160" name="entry.839337160"
           rows="2"></textarea>
         </div>
         <input class="btn btn-primary" type="submit" value="Gửi">
       </div>
     </div>
   </form>
 </body>
 
</html>

Vậy là chúng ta đã có một form với giao diện nhà làm rồi.

Tạo công cụ tự lấy dữ liệu và tạo trang web

Nếu lâu lâu mới dùng một, hai lần, chúng ta có thể không cần làm thêm một công cu tự động tạo form làm chi cho rườm rà. Nhưng nếu thường sử dụng hoặc muốn chia sẻ cho những người khác cùng sử dụng, thì lúc này chúng ta cần một công cụ rồi.

Trong bài viết này, mình dùng Node.js Express để tạo hai API: tạo form và tải mã nguồn về, vừa nhanh, vừa nhẹ lại đơn giản.

Mình dùng công cụ express-generator để tạo một ứng dụng Express, các bạn có thể cài nó bằng lệnh sau:

npm install --global express-generator

Sau đó, tạo một ứng dụng có tên là customize-google-form bằng lệnh:

express --pug customize-google-form

Do mình dùng view-engine là pug (trước đây là jade) nên sẽ thêm tham số –pug vào câu lệnh.

Di chuyển vào thư mục vừa tạo và cài đặt các packages:

npm install

Sau đó mở dự án bằng trình soạn thảo ưa thích của bạn, nếu bạn không biết chọn cái nào thì mình gợi ý dùng Visual Studio Code cho dễ.

Trong thư mục routes, tạo thêm tệp form.js để lập trình API cho người dùng tạo và tải form. Trong tệp app.js ở ngoài cùng thư mục dự án, thêm khai báo cho routes dẫn đến form API.

// app.js
...
var formRouter = require('./routes/form');
…
app.use('/form', formRouter);
...

Để lấy dữ liệu của Google Form, chúng ta sẽ dùng thư viện got để lấy nội dung trang web từ liên kết đến form và dùng thư viện cheerio đển truy vấn các thành phần HTML.

npm install got cheerio

Đầu tiên là request trang web:

var got = require("got");
 
async function request(url) {
 let response = await got(url);
 return response.body;
}

Sau đó lấy dữ liệu HTML:
var cheerio = require("cheerio");
 
function loadDOM(response) {
 return cheerio.load(response);
}
 
function loadHTML($) {
 return $.html();
}

Chạy thử mỗi tệp form.js để kiểm tra thử:

node routes/form.js

Ở phần trên, chúng ta biết có hai chỗ để lấy dữ liệu Google Form, giờ là lúc quyết định nên chọn chỗ nào? Đối với nhóm đối tượng thứ nhất, chúng ta có thể dễ dàng lấy được các thẻ div có lớp m2, sau đó lấy thuộc tính data-params và cắt chuỗi để ra được mảng dữ liệu:

function getGoogleFormData1($) {
 let data = { formAction: null, dataParams: [] };
 
 // form action
 data.formAction = $('form')[0].attribs.action;
 
 // form input name
 $('.m2').each(function (index, element) {
   if ('data-params' in element.attribs) {
     let raw = element.attribs['data-params'];
     raw = raw.replace(/%.@./, '');
     raw = raw.replace(/\],[a-zA-Z0-9\"\,]*\]/, ']');
     raw = JSON.parse(raw);
 
     let temporary = {
       name: 'entry.' + raw[4][0][0],
       label: raw[1],
       type: raw[3],
       childs: raw[4][0][1]
     }
 
     data.dataParams.push(temporary);
   }
 });
 
 return data;
}
 
(async() => {
 let data = getGoogleFormData1(loadDOM(await request('https://your_form_url')));
 console.log(data);
})();

Nhóm đối tượng thứ hai có vẻ khó lấy hơn vì thẻ script không có id hoặc class nào để nhận dạng, do đó chúng ta đành phải lấy tất cả thẻ script, sau đó tìm xem thẻ nào có nội dung chứa cụm từ ‘FB_PUBLIC_LOAD_DATA’:

function getGoogleFormData2($) {
 let data = null;
 let redundancy = "var FB_PUBLIC_LOAD_DATA_ = ";
 
 $("script").each((index, element) => {
   let inner = $(element).html();
   if (inner.indexOf(redundancy) !== -1) {
     inner = inner.replace(/[\n\r]*/g, "");
     data = inner.slice(redundancy.length, -1);
   }
 });
 
 return data;
}
 
(async() => {
 let data = getGoogleFormData2(loadDOM(await request('https://your_form_url')));
 console.log(data);
})();

Mặc dù nhóm đối tượng thứ nhất dễ lấy dữ liệu hơn, nhưng chúng ta nhận ra rằng khi có các form với nhiều section, thì cách này không lấy được dữ liệu cho các input của section còn lại. Do đó chúng ta sẽ lấy nhóm đối tượng thứ hai.

Sau khi đã lấy được dữ liệu của form, chúng ta sẽ lọc lại để loại bỏ các dữ liệu không cần thiết, trong ví dụ này, mình chỉ lấy các thông tin cơ bản như input name, input label và form action:

function convertGoogleFormData(string) {
 if (!string) {
   throw new Error('Not a valid form');
 }
 
 let raw = JSON.parse(string);
 let data = { title: "", description: "", action: "", sections: [] };
 
 data.title = raw[3];
 data.description = raw[1][0];
 data.action = `https://docs.google.com/forms/u/0/d/${raw[14]}/formResponse`;
 
 let sections = raw[1][1];
 let section = { title: "", fields: [] };
 
 sections.forEach((field) => {
   if (!field[4]) {
     data.sections.push(section);
     section = { title: field[1], fields: [] };
   } else {
     let temp = {
       label: field[1],
       description: field[2] || "",
       type: field[3],
       name: "entry." + field[4][0][0],
       options: field[4][0][1],
     };
     section.fields.push(temp);
   }
 });
 data.sections.push(section);
 
 return data;
}
 
(async() => {
 let data = convertGoogleFormData(getGoogleFormData2(loadDOM(await request(‘https://your_form_url'))));
 console.log(JSON.stringify(data));
})();

Tiếp theo, chúng ta tạo ra một trang web từ các dữ liệu form đã lọc được. Trong thư mục views, tạo một tệp mới có tên là form.pug rồi lập trình giao diện của form mà mình muốn trong này, mình sẽ dùng thư viện Bootstrap cho thuận tiện.

Chèn thư viện Jquery và Bootstrap vào trước đã:

doctype html
html(lang='vi')
 head
   meta(charset='utf-8')
   meta(name='viewport' content='width=device-width, initial-scale=1.0')
   title= title
   link(rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css')
   script(src='https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js')
   script(src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js')
   script(src='https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js')
 body

Sau đó tạo ra các mixin cho từng loại input:

mixin short-answer(label, name)
 .form-group
   label(for=name)= label
   input.form-control(id=name name=name type='text')
 
mixin paragraph(label, name)
 .form-group
   label(for=name)= label
   textarea.form-control(id=name name=name rows=2)
 
mixin multiple-choice(label, name, options)
 .form-group
   label= label
   each option, i in options
     .form-check
       - id = name + '-' + i
       label.form-check-label(for=id)
       input(id=id class='form-check-input' name=name type='radio' value=option[0])/ #{option[0]}
 
mixin dropdown(label, name, options)
 .form-group
   label(for=name)= label
   select.form-control(id=name name=name)
     each option in options
       option(value=option[0])= option[0]
 
mixin checkboxes(label, name, options)
 .form-group
   label= label
   each option, i in options
     .form-check
       - id = name + '-' + i
       label.form-check-label(for=id)
       input.form-check-input(id=id name=name type='checkbox' value=option[0])/ #{option[0]}
 
mixin range(label, name, options)
 - min = options[0][0]
 - max = options[options.length - 1][0]
 label= label
 input.form-control-range(id=name name=name type='range' min=min max=max step='1')
 
mixin linear-scale(label, name, options)
 label= label
 .form-group
   each option, i in options
     .form-check.form-check-inline.mx-1.mx-md-3
       - value = i + parseInt(options[0])
       label.form-check-label.text-center= value
         input.form-check-input.d-block.position-relative.mt-2.mx-auto(name=name value=value type='radio')
 
mixin date(label, name)
 .form-group
   label(for=name)= label
   input.form-control(id=name name=name type='date')
 
mixin time(label, name)
 .form-group
   label(for=name)= label
   input.form-control(id=name name=name type='time' value='06:00')

Chúng ta đang giả sử đang làm việc với các form có nhiều sections, như vậy mình sẽ cho nội dung của mỗi section vào một component card, đồng thời kiểm tra xem có section khác không để tạo ra các nút chuyển đến section khác:

body.bg-transparent
 
   form(action=action method='post')
     each section, i in sections
       - id = 'section-' + i
       - prevId = '#section-' + (i - 1)
       - nextId = '#section-' + (i + 1)
 
       .card(id=id style=(i === 0) ? '' : 'display: none')
         .card-body
           if section.title
             h5.card-title= section.title
           each field in section.fields
             case field.type
               when 0
                 +short-answer(field.label, field.name)
               when 1
                 +paragraph(field.label, field.name)
               when 2
                 +multiple-choice(field.label, field.name, field.options)
               when 3
                 +dropdown(field.label, field.name, field.options)
               when 4
                 +checkboxes(field.label, field.name, field.options)
               when 5
                 +linear-scale(field.label, field.name, field.options)
               when 9
                 +date(field.label, field.name)
               when 10
                 +time(field.label, field.name)
           if sections[i - 1]
             a.btn.btn-secondary.mr-2(href=prevId onclick=`$('.card').hide(); $('${prevId}').show()`) Trang trước
           if sections[i + 1]
             a.btn.btn-secondary.mr-2(href=nextId onclick=`$('.card').hide(); $('${nextId}').show()`) Trang sau
           if !sections[i + 1]
             input.btn.btn-primary(type='submit' value='Gửi')

Và để gửi một form có nhiều sections, khi gửi dữ liệu, chúng ta cần phải thêm vào một trường dữ liệu pageHistory với giá trị là thứ tự từ 0 đến số lượng trừ đi 1 và cách nhau bởi dấu phẩy. Cho nên, chúng ta phải thêm một thẻ input ẩn nằm ở cuối cùng của form, đồng thời tự điền vào giá trị cho nó.

 - pageHistory = []
 
   form(action=action method='post')
     each section, i in sections
       - id = 'section-' + i
       - prevId = '#section-' + (i - 1)
       - nextId = '#section-' + (i + 1)
       - pageHistory.push(i)
 
…
 
           if !sections[i + 1]
             input.btn.btn-primary(type='submit' value='Gửi')
 
.form-group.d-none
       - pageHistory = pageHistory.join(',')
       label(for='page-history') Hidden Input
       input.form-control(id='page-history', name='pageHistory', type='text', value=pageHistory)

Kết hợp các hàm lấy dữ liệu form ở trên và giao diện web vừa tạo, chúng ta có API tạo form khi request đường dẫn http://localhost:3000/form/view?url=duong_dan_form.

var express = require("express");
var got = require("got");
var cheerio = require("cheerio");
 
async function request(url) {
 let response = await got(url);
 return response.body;
}
 
function loadDOM(response) {
 return cheerio.load(response);
}
 
function findGoogleFormData($) {
 let data = null;
 let redundancy = "var FB_PUBLIC_LOAD_DATA_ = ";
 
 $("script").each((index, element) => {
   let inner = $(element).html();
   if (inner.indexOf(redundancy) !== -1) {
     inner = inner.replace(/[\n\r]*/g, "");
     data = inner.slice(redundancy.length, -1);
   }
 });
 
 return data;
}
 
function convertGoogleFormData(string) {
 if (!string) {
   throw new Error('Not a valid form');
 }
 
 let raw = JSON.parse(string);
 let data = { title: "", description: "", action: "", sections: [] };
 
 data.title = raw[3];
 data.description = raw[1][0];
 data.action = `https://docs.google.com/forms/u/0/d/${raw[14]}/formResponse`;
 
 let sections = raw[1][1];
 let section = { title: "", fields: [] };
 
 sections.forEach((field) => {
   if (!field[4]) {
     data.sections.push(section);
     section = { title: field[1], fields: [] };
   } else {
     let temp = {
       label: field[1],
       description: field[2] || "",
       type: field[3],
       name: "entry." + field[4][0][0],
       options: field[4][0][1],
     };
     section.fields.push(temp);
   }
 });
 data.sections.push(section);
 
 return data;
}
 
var router = express.Router();
 
router.get("/view", async (req, res, next) => {
 try {
   res.status(200).render("form", convertGoogleFormData(findGoogleFormData(loadDOM(await request(req.query.url)))));
 }
 catch (error) {
   res.status(404).send('Không tìm thấy');
 }
});
 
module.exports = router;

Thử gọi API xem sao?

Sau đó tạo thêm một API cho phép tải trang web về máy.

var html = require("html");
 
function tidyHTML(source) {
 return html.prettyPrint(source, { indent_size: 2 });
}
 
router.get("/download", async (req, res, next) => {
 let source = tidyHTML(loadHTML(loadDOM(await request(req.query.url))));
 
 res
   .status(200)
   .header("Content-Type", "text/html")
   .attachment("google-form-customize.html")
   .send(source);
});

Mọi thứ có vẻ ổn, chúng ta tạo thêm một trang chủ để cho người dùng nhập đường dẫn liên kết vào và xem thử kết quả.

doctype html
html(lang='vi')
 head
   meta(charset='UTF-8')
   meta(name='viewport', content='width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0')
   title Tùy chỉnh Google Form
   link(rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css')
   script(src='https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js')
   script(src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js')
   script(src='https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js')
 body
 
   nav.navbar.navbar-expand-lg.navbar-light.bg-transparent.mb-4
     .container
       a.navbar-brand(href='/') Tùy chỉnh Google Form
       button.navbar-toggler.border-0(type='button', data-toggle='collapse', data-target='#navbarNav', aria-controls='navbarNav', aria-expanded='false', aria-label='Toggle navigation')
         span.navbar-toggler-icon
       .collapse.navbar-collapse#navbarNav
         ul.navbar-nav.ml-auto
           li.nav-item
             a.nav-link(href='#how-to') Hướng dẫn
           li.nav-item
             a.nav-link(href='#') Thông tin
 
   .container
     .jumbotron.text-center
       .col-12.col-md-8.mx-auto
         h1.display-4 Tùy chỉnh Google Form
         p.lead Thay đổi giao diện của Google Form với chủ đề Bootstrap hoặc tự sửa CSS với mã nguồn có sẵn
 
         form
           .form-group
             input.form-control.text-center.form-control-lg#google-form-url(type='text', placeholder='Nhập liên kết Google Form URL ở đây')
           button.btn.btn-lg.btn-primary(type='button', onclick='generateForm();') Tạo biểu mẫu
             span#loader.spinner-border.ml-3(role='status' aria-hidden='true' style='display: none; width: 1.5rem; height: 1.5rem;')
 
       #result.row.mt-5(style='display:none;')
         .col-12.col-md-6.mx-auto.mb-3.text-center.text-md-left
           h3 Hoàn thành!
           p Sao chép đường dẫn liên kết iframe bên dưới để nhúng vào trang web của bạn
           .form-group
             .input-group
               input#result-embed.form-control(type='text', readonly)
               .input-group-append
                 input.btn.btn-secondary(type='button', onclick=`$('#result-embed').select(); document.execCommand('copy')`, value='Sao chép')
           p Vẫn không thích giao diện Bootstrap? Tải mã nguồn HTML về và tự thêm CSS của bạn ngay
           a#result-download.btn.btn-success(href='#') Tải về
         .col-12.col-md-6.mx-auto.text-center.text-md-left
           h3 Xem thử
           iframe#result-preview(src='', frameborder='0', style='width: 100%; height: 90vh;') Đang tải...
 
     .row.py-4
       .col-12.col-md-8.mx-auto.text-center
         h2 Sửa giao diện Google Form
         p Công cụ này giúp bạn tạo ra một biểu mẫu mới với giao diện của Bootstrap từ biểu mẫu có sẵn trên Google Form. Sau đó nhúng liên kết kèm với iframe trả về vào trang web của bạn là được, hoặc bạn có thể tải mã nguồn HTML rồi tự chỉnh sửa giao diện CSS.
 
     hr
 
     .row#how-to.py-4
       .col-12.col-md-6
         h3 Hướng dẫn
         ol
           li Chuẩn bị một biểu mẫu trong #[a(href='https://docs.google.com/forms') Google Form]
           li Lấy đường dẫn liên kết của Google Form
           li Dán đường dẫn vào khung nhập liệu
           li Nhấn nút "Tạo biểu mẫu" và lấy đường dẫn liên kết để nhúng vào trang web
       .col-12.col-md-6
         h3 Đặc trưng
         ul
           li Giao diện Bootstrap gọn gàng, đơn giản
           li Có thể nhúng trực tiếp kết quả trả về vào trang web mà không cần tải mã nguồn
           li Tạo sẵn mã HTML cho biểu mẫu, bạn chỉ cần tải về và viết CSS
 
   footer.border-top.py-4
     .container
       .row
         .col-12.text-center
           - year = new Date().getFullYear()
           span
             | &copy; #{year} -
             a.text-dark(href='http://share4happy.com', target='_blank') Share4Happy.com
 
 script.
   function generateForm() {
     $('#loader').show();
     let baseUrl = document.URL.split(':')[0] + '://' + location.host;
     let urlView = baseUrl + '/form/view?url=' + $('#google-form-url').val();
     let urlDownload = baseUrl + '/form/download?url=' + urlView;
     let iframe = `<iframe src="${urlView}" width="640" height="1425" frameborder="0" marginheight="0" marginwidth="0">Loading…</iframe>`
 
     fetch(urlView).then(response => {
       response.json().then(data => {
       });
       if(response.status === 200) {
         $('#result-preview')[0].setAttribute('src', urlView);
         $('#result-download').attr('href', urlDownload);
         $('#result-embed').val(iframe);
         $('#result').show();
       }
       else {
         alert('Không tìm thấy form');
       }
       $('#loader').hide();
     });
   }

Kết

Vậy là chúng ta đã tạo xong một công cụ để tự động tạo form mới từ Google Form rồi, đến bước này thì công cụ vẫn còn một số lỗi như: chưa hỗ trợ tải tệp, chưa nhận các ràng buộc từ Google Form và chưa chuyển đúng trình tự section. Hy vọng có các phiên bản cập nhật sau này.

Mã nguồn trên Github: https://github.com/tr1nh/customize-google-form

Related Posts

Để lại một bình luận