Модуль I · IT Skills (Cloud Services & Documents) · тема 2, урок 2 з 2 · 120 хвилин
Досі твоя сторінка тільки показувала: історію, текст, таблицю. Сьогодні вона вперше почне приймати.
Однокласник відкриє її, відповість на кілька питань і натисне кнопку.
А на твоєму диску, у файлі ~/data/responses.csv, з'явиться
новий рядок.
Роль сторінки змінилася, і відповідальність теж. Тепер у тебе лежить файл із відповідями живих людей.
На минулому уроці ти спроєктував поля в fields.json. І розібрав,
як улаштований CSV усередині. Сьогодні кожне поле з того файлу стане тегом
у формі й колонкою в таблиці.
Слова <label>, fetch() і валідація поки нічого не означають — розберемо їх по черзі.
/api/collect/<логін>. Він приймає
POST, звіряє поля з твоїм fields.json — і дописує
один рядок у ~/data/responses.csv.
До кожного рядка він додає ще дві свої колонки. session_id —
випадкова мітка, за якою видно, що це різні люди, а не одна людина
двадцять разів. ts — час, коли відповідь надійшла; це скорочення
від англійського timestamp, «часова позначка».
Ти цей сервіс не пишеш і не запускаєш:
твоя частина — форма, яка правильно до нього звертається.form/index.html,
form/stats.json і оновлений fields.json.
responses.csv не завантажується вручну ніколи — його пише
сам сервіс у приватну теку ~/data/, яку nginx по HTTP не віддає.| Етап | Хв | Що робимо |
|---|---|---|
| Повторення | 10 | Звіряємо fields.json з трьома дослідницькими питаннями |
| Теорія: форма і поля | 20 | label, name проти id, типи input |
| Верстка форми | 30 | Конструктор → готовий код → form/index.html |
| Перерва | 10 | — |
Шлях даних і fetch | 15 | POST у DevTools, Content-Type, preventDefault() |
| Чому валідації мало | 10 | Учитель шле POST повз форму — рядок з'являється |
| Карусель відповідей | 15 | Заповнюємо форми одне одного, 10+ рядків |
| Підсумки і приватність | 10 | stats.json, аудит ~/www, check 04 |
| Разом | 120 |
~/www/form/.
Не шукай lesson-04: її немає.У всіх прикладах на цій сторінці логін — ivan.
Скрізь, де він трапляється, підставляй свій.
Тег <form>, атрибут name, атрибут
id і тег <label>. Плутанина між ними — джерело
половини поламаних анкет.
Форма — це конверт. Тег <form> каже, куди
надсилати (action) і яким способом (method).
У конверт кладеться не все. Туди потрапляє лише те, що має атрибут
name. Поле без name не поїде нікуди — навіть
заповнене.
name проти id: два різні світиЦі атрибути схожі й часто збігаються за значенням. Тому їх і плутають. Але живуть вони в різних місцях і потрібні різним читачам.
name | id | |
|---|---|---|
| Хто читає | сервер | браузер: CSS, JS, <label for> |
| Куди потрапляє | у тіло запиту, далі — колонка в responses.csv | нікуди не їде, лишається на сторінці |
| Має бути унікальним | ні: у радіогрупі name навмисно спільний | так, на всю сторінку — інакше label знайде не те поле |
| Якщо забути | поле мовчки зникає з даних | клік по підпису перестає працювати |
| Правила запису | лише латиниця, цифри, _ — це заголовок CSV | те саме, зазвичай із префіксом f_ |
name="Скільки годин" здається зручним, але саме цей рядок стане
заголовком колонки в CSV. Пробіл усередині і кирилиця перетворюють заголовок
на мінне поле: одні програми його прочитають, інші — ні. Пиши
sleep_hours, а людський підпис клади в <label>.<label> обов'язковий, а не «бажаний»Підпис поля — не декорація. Він виконує три роботи одночасно, і жодну
з них placeholder виконати не може.
Перша робота — для тих, хто сторінки не бачить. Незрячі користувачі ходять по сайтах із програмою, яка читає вміст екрана вголос. Таку програму звуть скрінрідером, від англійського screen reader.
<label>. Без нього людина чує «поле вводу» — і все.radio і checkbox на телефоні це різниця між
«поставив галочку» і «промазав утретє».Зв'язок робиться однією парою: <label for="f_sleep"> ↔
<input id="f_sleep">. Ці значення мають збігтися символ
у символ.
<input> і п'ять його атрибутів.
Зелений блок — єдиний, без якого поле не існує для сервера.placeholder — це приклад, а не назваplaceholder="7" у полі про сон — це добре. Він показує,
у якому вигляді чекають відповідь.
А от placeholder="Скільки годин ти спав" замість підпису —
погано. По-перше, текст зникне з першим натиснутим символом. По-друге,
у частині браузерів його не озвучує скрінрідер.
По-третє, підказку в полі малюють блідо-сірим. А наскільки текст відрізняється від тла, міряють числом. Беруть яскравість літер, яскравість фону — і ділять одну на одну.
Правила доступності вимагають, щоб для звичайного тексту вийшло щонайменше 4,5:1. Тобто літери мають бути яскравіші за тло у чотири з половиною раза. Бліде сіре на білому цю межу зазвичай не проходить.
Ти зробив поле лише з placeholder="Скільки годин ти спав", без
<label>. Однокласник зі скрінрідером не зрозумів, що вводити,
а сусід почав вводити й забув питання. Що виправляти?
У responses.csv з'явилася колонка з правильним заголовком, але
в усіх 12 рядках вона порожня. Поле в розмітці є, у браузері воно заповнюється.
Що не так?
Тип поля — це домовленість із трьома сторонами одразу: з людиною (яку клавіатуру вона побачить), з браузером (що він перевірить) і з таблицею (яким виявиться значення в CSV).
| Тип | Що бачить людина | Що приходить у CSV | Коли брати |
|---|---|---|---|
text | звичайне однорядкове поле | рядок як є | коротка вільна відповідь: назва, слово, ім'я предмета |
email | клавіатура з @, браузер перевіряє формат | рядок | лише якщо пошта справді потрібна — а в анонімній анкеті вона не потрібна |
number | лише цифри, стрілки, min/max/step | рядок, який читається як число | години, кількість, оцінка, бали |
date | календар рідною мовою системи | завжди 2026-09-02 (ISO) | дата, про яку відповідають |
radio | кружечки, обрати можна один | одне значення value | до 5 варіантів, які видно всі одразу |
checkbox | квадратики, обрати можна кілька | кілька значень — склеюємо через ; | «обери все, що підходить» |
select | випадний список | одне значення value | 6 і більше варіантів, коли місця мало |
textarea | велике поле на кілька рядків | рядок, часто з комами і переносами | розгорнута думка — але звести її формулами майже неможливо |
type="date" вирішує проблему минулого уроку
Тоді ти бачив, як «01.02» перетворюється то на дату, то на число — залежно від
налаштувань програми. Поле date закриває це назавжди: у дані завжди
приїжджає YYYY-MM-DD, незалежно від того, що людина бачила
в календарі. Такі рядки сортуються як текст і дають правильний порядок.nameТри радіокнопки стають однією групою через однаковий name.
Більше їх ніщо не пов'язує. А от value в них мають бути різні:
саме воно поїде в CSV.
Даси різні name — і це вже три незалежні перемикачі. Людина
зможе ввімкнути всі три одразу.
Три деталі, на яких спотикаються:
<fieldset> з <legend>.
Це і є «підпис» для всієї групи: окремі <label> підписують
варіанти, а питання підписує <legend>.<option value=""> у select —
порожній. Без нього перший пункт вибрано наперед, і required
нічого не вимагає: людина «відповіла», не подивившись.required на групі checkbox працює не так, як здається.
Він вимагає, щоб була позначена саме ця галочка, а не «хоча б одна».
Правило «хоча б один варіант» перевіряє сервер.| Атрибут | Що робить | Приклад |
|---|---|---|
required | не дає надіслати порожнє поле | required |
min / max | межі для number, date, range | min="0" max="14" |
step | крок числа: цілі, півгодини | step="0.5" |
maxlength | обмежує довжину тексту | maxlength="120" |
pattern | регулярний вираз для text | pattern="9[АБ]" |
type="email" | перевіряє, що є @ і крапка після нього | — |
check 04 шукає у твоїй формі щонайменше одне поле з min/max
або pattern. Найпростіше — поле number із розумними межами:
годин сну не буває 30, оцінка не буває 15.Ти зробив три <input type="radio"> для варіантів
«добре / так собі / погано», але в анкеті можна позначити всі три одночасно.
Що виправити?
Ліворуч ти проєктуєш поля, праворуч одразу видно готову форму
очима однокласника. Унизу синхронно збирається код, який можна вставити
у свій form/index.html.
Натисни «Надіслати» в правій колонці, не заповнивши обов'язкове поле. Побачиш рівно те, що побачить однокласник.
Тепер заповни й надішли. Унизу правої колонки з'явиться тіло запиту і рядок, який ляже в CSV. Це весь шлях даних, стиснутий до одного кліку.
Ця кнопка не відкриває твою сторінку взагалі — вона складає тіло запиту руками. Подивись, що при цьому потрапить у CSV.
form/index.html між
<body> і </body>, додати <h1>,
один абзац «що я збираю і навіщо» і посилання назад на головну. Далі —
правити руками: конструктор дає каркас, а не готову здачу.Ти поставив required на всі дев'ять полів. Половина класу
не змогла надіслати анкету, і в CSV лише 5 рядків замість 15. Що пішло не так?
П'ять станцій, дві з яких на твоєму сервері. Якщо десь у CSV з'явився не той рядок — ламалося на одній із цих п'яти.
fields.json — саме
тому файл із минулого уроку треба тримати актуальним. Якщо в формі з'явилося
нове поле, а в fields.json його немає, значення просто нікуди
буде покласти.method у формі — це не «швидше або надійніше». Це два різні
повідомлення серверові. GET означає «покажи мені щось». POST —
«зміни щось у себе». Дописати рядок у CSV — це зміна, тому POST.
ivan; підстав свій логін.Content-Type: чим позначено вміст запитуТіло запиту — просто послідовність байтів. Сервер не вгадує, як їх читати.
Йому це каже заголовок Content-Type — етикетка на посилці.
Помилишся в етикетці — сервіс розбере тіло неправильно. І відповість
400, навіть якщо дані бездоганні.
| Content-Type | Як виглядає тіло | Коли |
|---|---|---|
application/x-www-form-urlencoded |
mood=%D0%B4%D0%BE%D0%B1%D1%80%D0%B5&sleep=7 |
типове для <form> без JS: браузер робить це сам |
multipart/form-data |
тіло розрізане на частини межею boundary, у кожної свій заголовок |
єдиний спосіб надіслати файл: байти картинки не влізуть у рядок з & |
application/json |
{"mood":"добре","sleep":7} |
наш випадок: зручно збирати в JS, зберігає вкладеність і числа |
Останні два розрізнити просто. multipart громіздкіший, зате
вміє нести будь-які байти — з ним ідуть файли. json дешевий
і читабельний, але тільки текст. Наш сервіс файлів не приймає, тож беремо
application/json.
Учень залишив method="get". Форма ніби працює, але після
надсилання в адресному рядку видно ?mood=погано&sleep=4,
і ця адреса залишилася в історії шкільного комп'ютера. У чому проблема?
Твій fetch() шле тіло JSON.stringify(data), але
заголовок Content-Type ти не поставив. Сервіс відповідає
400 Bad Request. Чому?
fetch(): надіслати, не перезавантажуючи сторінкуФорма вміє надсилатися сама, без жодного рядка JS. Але тоді браузер піде на іншу сторінку, і людина побачить голу відповідь сервера замість твого «дякую».
Подія submit за замовчуванням означає «браузере, надішли форму
сам». Ми цю дію скасовуємо через e.preventDefault().
Далі запитом розпоряджаємося самі. Збираємо значення, шлемо
fetch() і показуємо повідомлення просто на місці.
new FormData(form) бере рівно ті поля, у яких є
name. Це найшвидший спосіб перевірити себе: якщо поля немає
в data — ти забув name.fd.getAll(key) потрібен через checkbox:
одне ім'я може мати кілька значень. Склеюємо їх через ;, бо
кома вже зайнята роздільником CSV.form.action замість переписаної адреси: тоді
action живе в одному місці — у розмітці — і не розходиться
з тим, що бачить чекер.res.ok, а не «запит пройшов». fetch() не
кидає помилку на 400 чи 500: він спокійно
повертає відповідь, яку треба перевірити самому.checkbox не потрапляє у FormData взагалі —
не як порожній рядок, а як відсутнє поле. Сервіс підставить у цю колонку
порожнє значення, і це нормально. А от undefined у CSV —
ознака того, що ти намагався читати поле вручну замість FormData.Ти повісив fetch() на подію submit, але сторінка
щоразу перезавантажується, а рядок у CSV то з'являється, то ні. Чого бракує?
required і min/max живуть у твоїй
сторінці. А сторінку можна просто не відкривати.
Браузер перевіряє все для людини. Щоб вона не надіслала анкету з порожнім полем і не витратила час дарма. Робота корисна. Але до того, що приїде на сервер, вона стосунку не має.
Запит до /api/collect/ivan може скласти будь-хто й будь-чим.
Терміналом, іншою сторінкою, скриптом на п'ять рядків. Сервер цих запитів
не розрізняє: він бачить метод, заголовки й тіло — і більше нічого.
Ця команда не відкриває твоєї сторінки взагалі. Подивись у
responses.csv: якщо там на один рядок більше, ти щойно довів
собі, що перевірка в браузері нікого не зупиняє.
curl — це не curl
У Windows PowerShell 5.1 слово curl — псевдонім до
Invoke-WebRequest, і ключі -X, -H,
-d він розуміє зовсім інакше. Пиши або curl.exe, або
рідну команду PowerShell.| Перевірка | Що ловить |
|---|---|
| обов'язкові поля не порожні | напівзаповнені анкети |
число справді число і в межах min/max | "sleep":"багато" і "sleep":900 |
значення radio/select є у списку варіантів | вигадані відповіді, яких у формі не було |
| довжина тексту обмежена | вставлений роман на 40 000 символів |
| невідомі поля відкидаються | колонки, яких немає у fields.json |
| обмеження частоти запитів | один «дотепник», який надіслав 300 рядків |
| екранування для CSV | кому, лапки й перенос рядка всередині відповіді |
Останній пункт — прямо з минулого уроку. Людина написала
читаю, коли не сплю. У файл має лягти
"читаю, коли не сплю", у лапках. Інакше рядок роз'їдеться
на дві колонки.
Наш сервіс ставить ці лапки сам. Але правило знати треба: чекер звіряє кількість колонок у кожному рядку.
Учитель, не відкриваючи твоєї сторінки, надіслав curl-ом POST
із порожнім sleep_hours і morning="!!!". У CSV
з'явився рядок — хоча у формі стоять required і
min="0" max="14". Що це доводить?
З цієї хвилини в тебе на диску лежать відповіді живих людей, які тобі повірили. Це не метафора — це файл, який можна відкрити.
Шкільна анкета збирає думки, звички й вподобання. Скільки спиш, який предмет любиш, скільки часу в телефоні. Цього досить для будь-якого дослідницького питання з минулого уроку.
Збирай рівно те, без чого не відповіси на своє питання. Це принцип мінімізації даних, і він записаний у законі. В українському — «Про захист персональних даних». І в європейському: його звуть GDPR, скорочення від General Data Protection Regulation. Українською — «загальний регламент захисту даних».
Формулювання там різні, суть одна. Дані мають бути адекватними й не надмірними щодо мети.
Практичний тест для кожного поля, який займає п'ять секунд:
Телефон і адреса не змінюють жодного підсумку в stats.json.
Середнє за годинами сну від них не зрушиться. Зате вони змінюють статус
усього файлу. Була анонімна табличка про сон — став список контактів
однокласників із їхніми звичками.
Перша група — те, що називає людину. Прізвище й повне ім'я, фото, повна дата народження. Варто додати одне таке поле — і файл перестає бути анонімним. Усі інші відповіді в рядку тепер прив'язані до конкретного однокласника, і твоя табличка про сон стала досьє.
Друга — те, чим людину знаходять у реальному світі. Номер телефону, нікнейм у месенджері, домашня адреса, маршрут до школи. Жодне з цих полів не змінить твоєї діаграми. Зате разом вони кажуть, де ця людина буває й о котрій виходить з дому.
Третя — те, чим відмикають чужі речі. Паролі, логіни, номер картки, серія паспорта, ІПН. Останнє — індивідуальний податковий номер: за ним людину впізнає держава. Такі номери не питають у жодній анкеті, і шкільна тут не виняток.
Четверта — те, за що однокласника почнуть цькувати. Здоров'я, релігія, політичні погляди, що коїться вдома. Закон називає це чутливими даними й вимагає для них особливого захисту. Діаграми ти з них не побудуєш, а от посварити клас вони здатні за один день.
Замість імені сервіс сам ставить session_id — випадкову мітку
на кожну відповідь. Її досить, щоб побачити «це різні люди». І замало, щоб
зрозуміти, хто саме.
required скрізь;
там, де питання чутливе, додай варіант «не хочу відповідати».responses.csv і видаляєш його рядок. Без обговорень.responses.csv живе в ~/data/Тека ~/www/ — це вітрина: усе, що поклав, віддає в інтернет
nginx. Файл звідти відкриє будь-хто, хто вгадає адресу. Його ще й може
проіндексувати пошуковик.
Тека ~/data/ — навпаки, шухляда. Вона має права
750 і лежить поза веб-корінням. Шляху до неї у веб-сервера
просто немає.
Чекер шукає копію файлу в ~/www не за іменем, а за
sha256. Тому перейменувати responses.csv на
data2.txt не вийде. Так зроблено навмисно: правило про приватні
дані не має обходитися хитрістю.
Однокласниця пропонує додати в анкету телефон — «раптом треба буде уточнити відповідь». Що відповісти?
Щоб швидше дивитися результати, учень скопіював responses.csv
у ~/www/form/. Що сталося?
stats.jsonОстанній крок теми — звести зібране в три числа на кожне поле. Саме цей файл на наступному уроці стане діаграмами.
6,75 порахувати з чотирьох показаних
рядків не можна — воно рахується з усіх дванадцяти.| Ключ | Що містить | Звідки береться |
|---|---|---|
count | скільки всього відповідей | кількість рядків даних, без заголовка |
by_field | mean, min, max на кожне числове поле | AVERAGE, MIN, MAX по колонці |
distribution | скільки разів трапився кожен варіант | COUNTIF по колонці radio/select |
responses.csv сам і звірить із твоїм файлом
із допуском 0,01. Округлив 6,7532 до 6,75 —
зарахує. А от 7, поставлене «бо приблизно так», у цей допуск
не влізе, і критерій лишиться червоним, поки не перерахуєш.stats.json скриптом і зберігаєш вивід у файл,
Windows PowerShell 5.1 запише його в UTF-16, і чекер (він читає JSON
як UTF-8) скаже «невалідний JSON» без жодного пояснення.
У stats.json ти написав "count": 15, бо стільки
відповідей планував зібрати, а в CSV їх 11. Що буде?
Кроки 1—5 — у себе на комп'ютері, далі — FTP і сервер. Галочки зберігаються, сторінку можна закрити.
index.html, stats.json і
fields.json — легкий текст. responses.csv пише сам
сервіс у ~/data/. Ти його не заливаєш, не копіюєш у
~/www/ і не відкриваєш «щоб показати другові».Чекер не читає відповідей однокласників. Він дивиться структуру
форми, звіряє stats.json із CSV і перевіряє, що приватне
лишилося приватним. Спроби не обмежені.
Демонстраційний режим: результат згенеровано для показу. На сервері
ця кнопка викликає POST /api/check, який запускає
check 04 від імені учня.
curl-ом повз форму, і ви
разом дивитесь, що відповів сервіс і чи виріс CSV;POST: метод,
Content-Type і тіло з тими самими name,
що у fields.json;responses.csv у терміналі й пояснюєш,
звідки взялися session_id і ts;Це те, що бачить учитель, коли виставляє оцінку.
| Складник | Вага | Результат |
|---|
name; id потрібен
підпису й скрипту і на сервер не їде;POST, а
Content-Type каже серверу, чим розбирати тіло;~/data/.action скопійовано в сусіда — відповіді всього класу
летять на чужий акаунт, а свій CSV лишається порожнім;required поставлено на всі поля — і половина анкет
не доходить;stats.json вписані руками — перевірка
cross ловить це першою ж спробою;responses.csv кладуть у ~/www/form/,
«щоб було зручно дивитися»;name пробіли й кирилиця — заголовок CSV розсипається;e.preventDefault() — сторінка перезавантажується,
і частина відповідей губиться.Довести кількість відповідей до 15 і перерахувати
stats.json. Потім дописати в form/README.md
три речення рефлексії.
Одне з них обов'язкове: яке поле ти хотів додати й не додав. І яке дослідницьке питання від цього не постраждало.
Усе, що на цій сторінці сказано про форми, HTTP і приватність, — зі специфікацій і офіційної документації. Перевіряти дозволено й корисно.
<form>, action, method,
enctype — стандарт WHATWG.
html.spec.whatwg.org/multipage/forms.html<input> і те, що браузер робить із кожним — MDN.
developer.mozilla.org/…/Element/input<label> і зв'язок for ↔ id — MDN.
developer.mozilla.org/…/Element/labelplaceholder не заміняє підпис — WCAG, критерій
«Labels or Instructions» і техніка про плейсхолдери.
w3.org/WAI/tutorials/forms/labelsrequired, min,
max, pattern — MDN.
developer.mozilla.org/…/Form_validationContent-Type і структура тіла запиту — RFC 9110, §8.3.
datatracker.ietf.org/doc/html/rfc9110#section-8.3multipart/form-data — RFC 7578.
datatracker.ietf.org/doc/html/rfc7578fetch(), FormData і подія submit — MDN.
developer.mozilla.org/…/API/FormDataYYYY-MM-DD (ISO 8601) — саме його віддає
<input type="date">.
iso.org/iso-8601-date-and-time-formatOut-File -Encoding.
learn.microsoft.com/…/out-fileПовний список із поясненнями, що звідки взято, —
у файлі urok-04-джерела.md поруч із цією сторінкою.