সরাসরি পর্বে যান
পর্ব ৩৯ · ৫৯-এর মধ্যে · মডিউল ৬

form আর validation — ফর্ম থেকে মান পড়া, যাচাই আর নিরাপদে পাঠানো

Forms and Validation in JavaScript — FormData, the Submit Event, Constraint Validation API, Accessible Errors and Server-side Checks

Taught in Bangla, this lesson shows how to build forms users can actually complete: form structure, labels and input types, reading values with FormData, the submit event, requestSubmit and input versus change, built-in HTML validation and ValidityState, the Constraint Validation API with custom Bangla messages, accessible error display, async checks, double-submit prevention and server-side validation.

এই পর্বে যা শিখবেন

  • একটি ফর্মের গঠন — label, input-এর ধরন, name, autocomplete — আর কেন প্রতিটি ঘরের একটি label দরকার
  • ঘর থেকে সঠিক টাইপে মান পড়া, FormData আর Object.fromEntries, আর submit, input, change event-এর পার্থক্য
  • HTML-এর নিজস্ব যাচাই কীভাবে চলে, ValidityState কী বলে, আর setCustomValidity দিয়ে বাংলা বার্তা
  • ব্যবহারকারীকে বিরক্ত না করে চলতি যাচাই, screen reader-বান্ধব error, পাসওয়ার্ড মেলানো, ইউজারনেম-যাচাই আর দুবার জমা ঠেকানো
  • fetch দিয়ে পাঠানো, server-এর 422 error ঠিক ঘরে ফিরিয়ে দেখানো, আর কেন server-এ আবার যাচাই বাধ্যতামূলক

১ · ফর্ম কেন — ব্যবহারকারীর কথা সার্ভারে পৌঁছানোর দরজা

অনলাইন দোকানে অর্ডার, চাকরির আবেদন, ভর্তির ফরম, বিল পরিশোধ, সার্চ, লগইন — ওয়েবের প্রায় সব গুরুত্বপূর্ণ কাজই ফর্ম। পাতা পড়া একমুখী; ফর্মে ব্যবহারকারী উল্টো দিকে কথা বলেন। সেই কথাটি ঠিকমতো, নিরাপদে, বিরক্তি ছাড়া পৌঁছে দেওয়াই এই পর্বের বিষয়। কাজটি তিন স্তরে ভাগ করা, আর প্রতিটি স্তরের দায়িত্ব আলাদা:

১. HTML — বিনামূল্যের ভিত্তি

type="email" দিলে মোবাইলে @ চিহ্নসহ কিবোর্ড আসে, required দিলে ফাঁকা ঘরে ব্রাউজার নিজেই আটকায়, autocomplete="tel" দিলে ফোন নম্বর নিজে থেকে বসে। JavaScript বন্ধ থাকলেও এগুলো কাজ করে।

২. JavaScript — অভিজ্ঞতা ভালো করা

বাংলায় স্পষ্ট বার্তা, ঘর ছাড়ার সাথে সাথে feedback, পাসওয়ার্ড মেলানো, ইউজারনেম খালি আছে কি না দেখা, পাতা reload না করে পাঠানো।

৩. Server — আসল পাহারাদার

ব্রাউজারে যা-ই যাচাই হোক, server প্রতিটি নিয়ম আবার যাচাই করে। কারণ ব্রাউজার ব্যবহারকারীর যন্ত্র — সেখানে চলা কোড তিনি বদলাতে বা এড়াতে পারেন।

ব্যাংকের কাউন্টারে টাকা জমার স্লিপের কথা ভাবুন। স্লিপে আগে থেকেই ঘর ছাপা থাকে — হিসাব নম্বর, টাকার অঙ্ক কথায় ও সংখ্যায়, তারিখ (এটি HTML). পাশে দাঁড়ানো সহকারী আপনাকে বলে দেন, “অঙ্কটা কথায়ও লিখুন” (এটি JavaScript-এর বন্ধুসুলভ বার্তা)। কিন্তু টাকা নেওয়ার আগে ক্যাশিয়ার নিজে আবার মিলিয়ে দেখেন — সহকারী কী বলেছেন তাতে তাঁর কিছু যায় আসে না (এটি server). সহকারী না থাকলেও কাজ চলে; ক্যাশিয়ার না থাকলে ব্যাংক চলে না।

মূল ধারণা

Client-side validation ব্যবহারকারীর সুবিধার জন্য: ভুলটা সাথে সাথে দেখানো, সার্ভারে অপ্রয়োজনীয় অনুরোধ কমানো। Server-side validation নিরাপত্তা ও ডেটার সঠিকতার জন্য। প্রথমটি বাদ দিলে ফর্ম বিরক্তিকর হয়; দ্বিতীয়টি বাদ দিলে সিস্টেম অনিরাপদ হয়। এই পর্বের প্রতিটি কৌশল এই দুই ভাগের কোনো একটিতে পড়ে।

এটিই পর্ব ০২-এর progressive enhancement: আগে এমন HTML, যা একাই কাজ করে; তার ওপর JavaScript. পর্ব ৩৭-এর DOM আর পর্ব ৩৮-এর event এখানে একসাথে কাজে লাগবে।

২ · ফর্মের গঠন — form, label, input-এর ধরন আর name

একটি অনলাইন দোকানের checkout ফর্ম দিয়ে শুরু করি। এই পর্বের বেশিরভাগ উদাহরণ এই ফর্মকে ঘিরে:

<form id="checkout" action="/api/orders" method="post">
  <label for="phone">মোবাইল নম্বর</label>
  <input id="phone" name="phone" type="tel" required
         pattern="01[3-9]\d{8}" autocomplete="tel" inputmode="numeric"
         aria-describedby="phone-hint">
  <small id="phone-hint">যেমন: 01712345678</small>

  <label for="district">জেলা</label>
  <select id="district" name="district" required>
    <option value="">বাছাই করুন</option>
    <option value="dhaka">ঢাকা</option>
  </select>

  <fieldset>
    <legend>পেমেন্ট</legend>
    <label><input type="radio" name="pay" value="cod" required> ক্যাশ অন ডেলিভারি</label>
    <label><input type="radio" name="pay" value="mobile"> মোবাইল ব্যাংকিং</label>
  </fieldset>

  <label><input type="checkbox" name="terms" required> শর্তাবলিতে রাজি</label>
  <button>অর্ডার করুন</button>
</form>

এই কোডটি দেখানোর জন্য। চারটি জিনিস খেয়াল করুন। এক, প্রতিটি ঘরের একটি <label> — হয় for/id দিয়ে জোড়া, নয়তো ঘরটিকে ভেতরে মুড়ে। দুই, প্রতিটি ঘরের name — পাঠানোর সময় ডেটার “চাবি” এটিই; name ছাড়া ঘরের মান সার্ভারে যায় না। তিন, radio-গুলোর name একই — তাই এরা একটি দল, একটির বেশি বাছাই করা যায় না। চার, <fieldset> আর <legend> দলটিকে একটি নাম দেয়, যা screen reader প্রতিটি বিকল্পের আগে পড়ে।

label কেন বাধ্যতামূলক — placeholder কেন যথেষ্ট নয়

অনেক ডিজাইনে ঘরের ভেতরের ধূসর লেখা (placeholder) দিয়েই label-এর কাজ চালানো হয়। সমস্যা তিনটি: টাইপ শুরু করলেই লেখাটি উধাও, ধূসর লেখা অনেকে পড়তে পারেন না, আর screen reader সবসময় placeholder-কে নাম হিসেবে পড়ে না। আসল <label> এসব মেটায়, আর label-এ চাপলে ঘরটি focus পায় — ছোট checkbox-এ আঙুল দিয়ে চাপার জন্য বড় সুবিধা। placeholder শুধু উদাহরণের জন্য (“যেমন: 01712345678”). এটি accessibility-র সবচেয়ে সস্তা জয়।

input-এর ধরন — প্রতিটি একটি ছোট চুক্তি

type attribute শুধু দেখতে কেমন হবে তা ঠিক করে না; এটি ব্রাউজারকে বলে মানটি কী ধরনের। তাতে তিনটি জিনিস বিনামূল্যে আসে: মোবাইলে মানানসই কিবোর্ড, নিজস্ব যাচাই, আর কখনো কখনো আলাদা নিয়ন্ত্রণ (যেমন তারিখ বাছাইয়ের ক্যালেন্ডার)।

ধরনকী পানমনে রাখুন
email@ সহ কিবোর্ড, format যাচাই (typeMismatch)multiple দিলে কমা দিয়ে একাধিক ঠিকানা
telসংখ্যার কিবোর্ডকোনো format যাচাই করে না — দেশে দেশে নিয়ম আলাদা; pattern দিন
numberসংখ্যার কিবোর্ড, min/max/stepপরিমাণের জন্য; ফোন নম্বর, OTP বা পোস্ট কোডের জন্য নয় (শুরুর 0 আর মাউস-চাকায় মান বদলানোর ঝামেলা)
dateক্যালেন্ডার, মান সবসময় YYYY-MM-DDদেখায় স্থানীয় format-এ, কিন্তু value ISO (পর্ব ২০)
passwordলেখা লুকানো, পাসওয়ার্ড ম্যানেজারের সহায়তাautocomplete="new-password" বা "current-password"
checkbox / radioহ্যাঁ-না / দলের একটিপড়তে হয় checked, value নয়
<select>, <textarea>তালিকা থেকে বাছাই / বহু লাইনের লেখাselect-এ multiple দিলে একাধিক বাছাই
fileফাইল বাছাই, মোবাইলে ক্যামেরাaccept শুধু পরামর্শ, যাচাই নয় (অংশ ৭)

দুটি সহযোগী attribute: inputmode শুধু মোবাইল কিবোর্ড বেছে দেয়, যাচাই ছাড়া (OTP-র ঘরে inputmode="numeric"). আর autocomplete বলে ঘরটি কোন তথ্যের — name, tel, street-address, new-password, one-time-code — যাতে ব্যবহারকারী এক চাপে পুরো ঠিকানা বসাতে পারেন।

৩ · মান পড়া — value সবসময় string, আর FormData

ফর্মের ঘরও DOM-এর object (পর্ব ৩৭), আর তাদের সবচেয়ে ব্যবহৃত property value. প্রায় সবাইকে একবার ভোগানো নিয়ম: value সবসময় একটি string, ঘরটি type="number" হলেও।

const qty = document.querySelector('#qty');      // <input type="number">
qty.value;            // "2"  — string!
qty.value + 1;        // "21" — যোগ নয়, জোড়া লাগানো
qty.valueAsNumber;    // 2    — ফাঁকা বা অবৈধ হলে NaN
Number(qty.value);    // 2    — কিন্তু ফাঁকা ঘরে 0! (Number("") === 0)

document.querySelector('#dob').valueAsDate;    // Date (UTC মধ্যরাত) বা null
document.querySelector('[name=terms]').checked; // true / false
document.querySelector('[name=pay]:checked')?.value;   // বাছাই করা radio, নইলে undefined
[...document.querySelector('#tags').selectedOptions].map((o) => o.value);  // select multiple

এই কোডটি দেখানোর জন্য। সবচেয়ে বিপজ্জনক লাইন Number(qty.value): ফাঁকা ঘর 0 হয়ে যায়, আর “পরিমাণ দেননি” আর “শূন্য দিয়েছেন” আলাদা করা যায় না। পর্ব ০৬ আর পর্ব ১০-এ দেখা coercion-এর ফাঁদ এখানে বাস্তব রূপ নেয়। valueAsNumber ফাঁকা ঘরে NaN দেয়, তাই Number.isNaN() দিয়ে ধরা যায়। আমরা Edge-এ যাচাই করেছি: type="number"-এর ঘরে “abc” বসালে ব্রাউজার value-কে "" করে দেয়, আর valueAsNumber হয় NaN; “1e1” বসালে valueAsNumber 10. তারিখের valueAsDate UTC মধ্যরাতের Date দেয় — বাংলাদেশ সময়ে সকাল ৬টা (পর্ব ২০)।

FormData — পুরো ফর্ম একবারে

দশটি ঘরে দশবার querySelector না লিখে FormData সব ঘর name ধরে এক লাইনে তুলে আনে — ঠিক যে ডেটা ব্রাউজার সাধারণ submit-এ পাঠাত (পর্ব ৩৫):

const fd = new FormData(form);
fd.get('phone');              // "01712345678"
fd.getAll('tags');            // [ "gift", "fragile" ] — একই name-এর সব মান
Object.fromEntries(fd);       // { phone: "...", district: "dhaka", pay: "cod", terms: "on", tags: "fragile" }
                              //  ↑ একই name বারবার থাকলে শুধু শেষটি থাকে!
fd.has('terms');              // টিক না দেওয়া checkbox হলে false — একেবারেই অনুপস্থিত

এই কোডটি দেখানোর জন্য। নিয়মগুলো এসেছে HTML-এর পুরোনো ফর্ম-জমার নিয়ম থেকে, আর সবগুলো Edge-এ যাচাই করা: (১) name ছাড়া ঘর বাদ। (২) disabled ঘর বাদ — দেখাতে চান কিন্তু বদলাতে দিতে চান না, অথচ পাঠাতে চান? তাহলে readonly. (৩) টিক না দেওয়া checkbox আর কোনো radio বাছাই না হলে সেই name অনুপস্থিত — false নয়। (৪) টিক দেওয়া checkbox-এর value না দিলে মান হয় "on". (৫) Object.fromEntries একই name-এর একাধিক মানের শেষটি রাখে; select multiple বা একাধিক checkbox-এর জন্য getAll ব্যবহার করুন। মানগুলো সবই string (ফাইল হলে File object) — টাইপ বদলানো আপনার কাজ।

৪ · submit event — preventDefault, requestSubmit আর input বনাম change

বোতাম চাপলে বা কোনো ঘরে Enter দিলে <form>-এর ওপর একটি submit event আসে — তাই listener বসান ফর্মে, বোতামের click-এ নয় (নইলে Enter দিয়ে জমা দেওয়া ব্যবহারকারীরা বাদ পড়েন)। ডিফল্টভাবে ব্রাউজার ডেটা নিয়ে action-এর ঠিকানায় নতুন পাতা খোলে; নিজে পাঠাতে চাইলে সেটি থামান preventDefault() দিয়ে (পর্ব ৩৮):

form.addEventListener('submit', (e) => {
  e.preventDefault();                       // পাতা বদলাবে না
  const data = Object.fromEntries(new FormData(form));
  // … যাচাই, তারপর fetch (অংশ ৮)
});

form.requestSubmit();   // ✓ যাচাই চলে, submit event আসে — বোতাম চাপার মতো
form.submit();          // ✗ যাচাই নেই, submit event নেই — সরাসরি পাঠিয়ে দেয়

এই কোডটি দেখানোর জন্য। কোড থেকে ফর্ম জমা দেওয়ার দুটি method-এর পার্থক্য বড়। পুরোনো form.submit() সোজা পাঠিয়ে দেয় — required ঘর ফাঁকা থাকলেও, আর আপনার submit listener না চালিয়েই। আমরা Edge-এ যাচাই করেছি: required ঘর ফাঁকা রেখে submit() ডাকলে submit event আর invalid event দুটোই শূন্যবার আসে, আর পাতা চলে যায়। নতুন requestSubmit() (Chrome ৭৬, Safari ১৬ থেকে) ঠিক বোতাম চাপার মতো আচরণ করে: আগে যাচাই, তারপর submit event. কোনো নির্দিষ্ট বোতাম দিয়ে জমার ভান করতে requestSubmit(button).

input বনাম change — কখন কোনটি

input আসে প্রতিটি পরিবর্তনে — অক্ষর টাইপ, মুছে ফেলা, পেস্ট। change আসে মান “চূড়ান্ত” হলে: লেখার ঘরে focus সরে যাওয়ার সময় (মান বদলে থাকলে), আর checkbox, radio, select-এ সাথে সাথে। আমরা Edge-এ “0171” টাইপ করে তারপর অন্য ঘরে গিয়ে দেখেছি: চারটি input (0, 01, 017, 0171), তারপর একটিমাত্র change. কোডের মাধ্যমে el.value = … বসালে এ দুটির কোনোটিই আসে না — দরকার হলে নিজে el.dispatchEvent(new Event('input', { bubbles: true })) পাঠাতে হয়, যা এই পর্বের ডেমোগুলো করে।

কাজকোন eventকেন
অক্ষর গোনা (“১৬০-এর মধ্যে ৪২”)inputপ্রতিটি অক্ষরে হালনাগাদ চাই
সার্চের পরামর্শ, ইউজারনেম-যাচাইinput + debounceপ্রতিটি অক্ষরে নয়, থামার পর একবার (পর্ব ৩৬)
জেলা বাছাই করলে থানার তালিকা বদলানোchangeselect-এ বাছাই শেষ হলেই একবার
ঘর ছাড়ার পর প্রথম error দেখানোblur বা focusoutটাইপের মাঝখানে বিরক্ত না করা (অংশ ৭)

input আর change bubble করে, তাই ফর্মে একটি listener-ই যথেষ্ট (পর্ব ৩৮-এর event delegation; e.target বলে কোন ঘর)। focus/blur bubble করে না — ফর্ম-স্তরে focusin/focusout ব্যবহার করুন।

৫ · HTML-এর নিজস্ব যাচাই — required, pattern, min/max আর ValidityState

এক লাইন JavaScript না লিখেই ব্রাউজার অনেক যাচাই করে দেয়। এর নাম constraint validation — প্রতিটি ঘরে কিছু “শর্ত” (constraint) attribute দিয়ে লেখা থাকে, আর জমা দেওয়ার আগে ব্রাউজার সেগুলো মিলিয়ে দেখে:

Attributeশর্তভাঙলে কোন flag
requiredফাঁকা চলবে না (checkbox-এ টিক, radio দলে একটি বাছাই)valueMissing
type="email", "url"সেই ধরনের formattypeMismatch
patternregex-এর সাথে পুরো মান মেলাpatternMismatch
minlength / maxlengthঅক্ষরের সংখ্যাtooShort / tooLong
min / maxসংখ্যা বা তারিখের সীমাrangeUnderflow / rangeOverflow
stepনির্দিষ্ট ধাপে (যেমন পূর্ণসংখ্যা)stepMismatch
(number-এ অবোধ্য লেখা)ব্রাউজার মানটি বুঝতে পারেনিbadInput
setCustomValidity('…')আপনার নিজের নিয়ম (অংশ ৬)customError

pattern — নিজে থেকেই anchor করা

পর্ব ৩০-এ দেখেছেন, যাচাইয়ের regex-এ ^…$ ভুলে গেলে আগে-পরে যা খুশি জুড়ে দেওয়া যায়। pattern-এ এই ঝুঁকি নেই: ব্রাউজার নিজেই নকশাকে ^(?:…)$ দিয়ে মুড়ে নেয়। আমরা যাচাই করেছি, pattern="01[3-9]\d{8}"-এ “x01712345678” আর “ 01712345678” (শুরুতে space) দুটিই patternMismatch. একটি নতুন ফাঁদ: HTML মানদণ্ড এখন pattern-কে regex-এর v flag দিয়ে compile করতে বলে, আর নতুন Edge তা-ই করে। ফলে character class-এ খালি - বা ( থাকলে নকশাটি অবৈধ, আর অবৈধ pattern ব্রাউজার চুপচাপ উপেক্ষা করে — আমাদের পরীক্ষায় pattern="[a-z-]+" “!!”-কেও বৈধ বলেছে। escape করুন: [a-z\-].

type="email"-এর নিয়ম ইচ্ছা করেই ঢিলেঢালা: Edge-এ “a@b” বৈধ (ডট বাধ্যতামূলক নয়), “a@b.” অবৈধ, “আমি@x.com” অবৈধ। ঠিকানাটি সত্যিই কারও কি না, জানার একমাত্র উপায় confirmation ইমেইল। জমা দেওয়ার মুহূর্তে ব্রাউজার কী কী করে, দেখুন। “পরে” চাপুন:

  1. জমার চেষ্টাclick / Enterবা requestSubmit()
  2. প্রতিটি ঘরvalidityDOM-এর ক্রমে
  3. invalid eventপ্রতিটি ভুল ঘরেbubble করে না
  4. প্রথম ভুল ঘরfocus + বার্তাজমা বাতিল
  5. submit eventসব ঠিক হলেআপনার handler
ধাপ ১যাচাই কখন চলে, কখন চলে না
বোতাম চাপা, Enter, form.requestSubmit()  → যাচাই চলবে
<form novalidate> বা <button formnovalidate> → যাচাই বাদ
form.submit()                              → যাচাই বাদ, submit event-ও বাদ
ধাপ ২ফাঁকা নাম, ভুল ফোন, বাকি সব ঠিক
name     ""             → valueMissing     ✗
phone    "01212345678"  → patternMismatch  ✗
district "dhaka"        → ✓
terms    checked        → ✓
disabled, readonly আর type="button" ঘর যাচাইয়ে পড়ে না (willValidate = false)
ধাপ ৩বার্তা বদলানোর শেষ সুযোগ
form.addEventListener('invalid', (e) => {   // capture: true দরকার
  e.target.validity.valueMissing && e.target.setCustomValidity('এই ঘরটি পূরণ করুন');
}, true);
name আর phone — দুটি ঘরেই invalid আসে, শুধু প্রথমটিতে নয়
ধাপ ৪ব্রাউজারের বুদবুদ
focus → name ঘর
বুদবুদ: "Please fill out this field."   (Edge, ইংরেজি সেটিংসে)
submit event: আসেনি  → আপনার handler চলেনি
ধাপ ৫এবার JavaScript-এর পালা
form.addEventListener('submit', (e) => {
  e.preventDefault();
  const data = Object.fromEntries(new FormData(form));   // → fetch
});
  1. জমার চেষ্টা। ব্যবহারকারী বোতাম চাপলেন, বা কোনো ঘরে Enter দিলেন, বা কোড requestSubmit() ডাকল। ব্রাউজার এখনই পাঠায় না — আগে “interactive validation” চালায়, যদি না ফর্মে novalidate বা বোতামে formnovalidate থাকে।
  2. প্রতিটি ঘর যাচাই। ফর্মের ঘরগুলো DOM-এর ক্রমে একে একে দেখা হয়। প্রতিটির validity object-এ কোন শর্ত ভেঙেছে তার flag থাকে। disabled ঘর, readonly ঘর আর সাধারণ বোতাম এই হিসাবে পড়ে না — তাদের willValidate হয় false.
  3. প্রতিটি ভুল ঘরে invalid event. শুধু প্রথমটিতে নয় — আমাদের পরীক্ষায় ফাঁকা নাম আর ভুল ফোন — দুটি ঘরেই এসেছে। Event-টি bubble করে না, তাই ফর্মে শুনতে চাইলে capture দিয়ে (addEventListener(…, true)). বার্তা বদলানোর এটি একটি সুযোগ।
  4. প্রথম ভুল ঘরে focus আর বার্তা। ব্রাউজার প্রথম ভুল ঘরে নিয়ে যায় আর তার পাশে একটি ছোট বুদবুদে validationMessage দেখায় — ব্রাউজারের ভাষায় লেখা। submit event আসেই না, তাই আপনার handler-এ ভুল ডেটা কখনো পৌঁছায় না।
  5. সব ঠিক হলে submit event. এবার আপনার listener চলে। preventDefault() ডাকলে আপনি নিজে fetch করবেন (অংশ ৮); না ডাকলে ব্রাউজার action-এ পাঠিয়ে নতুন পাতা খুলবে — JavaScript ছাড়া পুরোনো পথ।

ValidityState — একটি ঘরের রিপোর্ট কার্ড

প্রতিটি ঘরের el.validity একটি ValidityState object — ওপরের টেবিলের প্রতিটি flag-এর জন্য একটি true/false, আর সবশেষে valid, যা সত্য শুধু তখন, যখন বাকি সব মিথ্যা। একটি ফোন ঘর ধরুন: <input type="tel" required minlength="11" pattern="01[3-9]\d{8}">. বিভিন্ন মানে এর রিপোর্ট কার্ড কেমন দাঁড়ায়, দেখুন (সব মান Edge-এ যাচাই করা):

input type="tel" required minlength="11" pattern="01[3-9]\d{8}"
value
valueMissing
patternMismatch
tooShort
valid
validationMessage (Edge)
  1. ফাঁকা ঘর। শুধু valueMissing. খেয়াল করুন, ফাঁকা ঘরে pattern বা minlength পরীক্ষাই হয় না — ফাঁকা থাকা চলবে কি না, সেটি একমাত্র required-এর সিদ্ধান্ত। required না থাকলে ফাঁকা ঘর পুরোপুরি বৈধ।
  2. “0171”, কোড দিয়ে বসানো। patternMismatch সত্য, কিন্তু অবাক করা ব্যাপার: tooShort মিথ্যা! মানদণ্ড অনুযায়ী minlength/maxlength পরীক্ষা হয় শুধু ব্যবহারকারী নিজে মান বদলালে। তাই কোড দিয়ে মান বসিয়ে টেস্ট করলে এই flag কখনো সত্য হবে না — অনেক ডেভেলপার এখানে বিভ্রান্ত হন।
  3. “0171”, হাতে টাইপ করা। এবার tooShort-ও সত্য। একটি ঘরে একসাথে একাধিক flag সত্য হতে পারে; ব্রাউজার তখন একটি বার্তা বেছে দেখায় — এখানে pattern-এর বার্তা। তাই নিজের বার্তা লেখার সময় flag-গুলো কোন ক্রমে দেখবেন, তা আপনাকে ঠিক করতে হবে।
  4. “01212345678”. দৈর্ঘ্য ঠিক, কিন্তু তৃতীয় অঙ্ক 2 — patternMismatch. “Please match the requested format.” ব্যবহারকারীকে কিছুই শেখায় না: কোন format? এখানেই নিজের বার্তা দরকার।
  5. “01712345678” — সব ঠিক। সব flag মিথ্যা, valid সত্য, বার্তা খালি। ব্রাউজার শুধু আকৃতি যাচাই করেছে — নম্বরটি আসলে কারও কি না, জানতে OTP লাগে।

৬ · Constraint Validation API — বাংলা বার্তা আর নিজের নিয়ম

ব্রাউজারের বুদবুদের দুর্বলতা: বার্তা ব্রাউজারের ভাষায় আর সাধারণ, ডিজাইন বদলানো যায় না, আর কিছুক্ষণ পর মিলিয়ে যায়। সমাধান Constraint Validation API: HTML-এর শর্তগুলো রেখে দেওয়া, কিন্তু বার্তা দেখানোর কাজ নিজের হাতে নেওয়া।

সদস্যকী করে
el.validityValidityState — কোন শর্ত ভেঙেছে
el.validationMessageব্রাউজারের (বা আপনার custom) বার্তা
el.willValidateঘরটি আদৌ যাচাইয়ে পড়ে কি না
checkValidity()true/false দেয়; ভুল হলে invalid event পাঠায়, কিন্তু কিছু দেখায় না
reportValidity()checkValidity-র মতোই, সাথে প্রথম ভুল ঘরে focus আর বুদবুদ দেখায়
setCustomValidity(msg)খালি নয় এমন বার্তা দিলে ঘরটি অবৈধ (customError); '' দিলে আবার বৈধ

setCustomValidity-র সবচেয়ে প্রচলিত বাগ: বার্তা বসানো হলো, কিন্তু ভুল সারানোর পর মোছা হলো না — ঘরটি চিরকাল অবৈধ, সঠিক মান লিখেও ফর্ম জমা হয় না। নিয়ম: প্রতিটি input event-এ আগে setCustomValidity(''), তারপর দরকার হলে নতুন বার্তা।

চালিয়ে দেখুন — বাংলা বার্তাসহ checkout ফর্ম

নিচের কোড novalidate বসায় JavaScript থেকে (form.noValidate = true) — JavaScript না চললে ব্রাউজারের যাচাই থাকে, চললে আমাদের বাংলা বার্তা। HTML-এর শর্ত বদলায়নি; আমরা শুধু validity পড়ছি। কোডটি নিজে তিনবার জমার চেষ্টা করে, যাতে প্রতিবার একই আউটপুট আসে। তারপর ওপরের ফর্মে নিজে লিখে “অর্ডার করুন” চেপে দেখুন।

const form = document.querySelector('#checkout');
const MSG = { valueMissing: 'এই ঘরটি পূরণ করুন', patternMismatch: '01XXXXXXXXX আকারে ১১ অঙ্ক লিখুন', tooShort: 'আরও একটু বিস্তারিত লিখুন' };
const problemOf = (el) => Object.keys(MSG).find((flag) => el.validity[flag]);
form.noValidate = true;                       // JavaScript চললে বার্তা আমরাই দেখাব
function showError(el) {
  const flag = el.checkValidity() ? null : problemOf(el);
  el.setAttribute('aria-invalid', flag ? 'true' : 'false');
  form.querySelector(`#${el.name}-err`).textContent = flag ? MSG[flag] : '';
  return flag;
}
form.addEventListener('submit', (e) => {
  e.preventDefault();                         // পাতা reload হবে না
  const bad = [...form.elements].filter((el) => el.willValidate && el.name && showError(el));
  if (bad.length) {
    console.log('ভুল:', [...new Set(bad.map((el) => `${el.name}=${problemOf(el)}`))].join(', '));
    return bad[0].focus();                    // প্রথম ভুল ঘরে নিয়ে যাওয়া
  }
  console.log('পাঠানো হবে:', JSON.stringify(Object.fromEntries(new FormData(form))));
});
const fill = (key, value) => { form.elements[key].value = value; };
form.requestSubmit();                                         // ১) সব ফাঁকা
fill('name', 'রহিম উদ্দিন'); fill('phone', '01212345678'); fill('district', 'rajshahi');
fill('address', 'বাড়ি ১২, সাহেব বাজার'); fill('pay', 'cod');
form.requestSubmit();                                         // ২) দুটি ভুল বাকি
fill('phone', '01712345678'); form.elements.terms.checked = true;
form.requestSubmit();                                         // ৩) সব ঠিক

Edge-এ চালানো আউটপুট:

ভুল: name=valueMissing, phone=valueMissing, district=valueMissing, address=valueMissing, pay=valueMissing, terms=valueMissing
ভুল: phone=patternMismatch, terms=valueMissing
পাঠানো হবে: {"name":"রহিম উদ্দিন","phone":"01712345678","district":"rajshahi","address":"বাড়ি ১২, সাহেব বাজার","pay":"cod","terms":"on"}

problemOf শুধু সেই flag খোঁজে, যার বাংলা বার্তা আমরা লিখেছি; MSG-এর key-এর ক্রমই বলে দেয় একাধিক flag সত্য হলে কোনটি আগে দেখানো হবে। showError তিনটি কাজ একসাথে করে — বার্তা লেখে, aria-invalid বসায়, আর ভুল সারানো হলে দুটোই মোছে। radio দলের প্রতিটি বোতাম একই valueMissing দেয়, তাই Set দিয়ে নাম একবার করে দেখানো হয়েছে। কোড দিয়ে বসানো মানে tooShort কখনো আসে না (আগের stepper-এর ধাপ ২), তাই “আরও একটু বিস্তারিত লিখুন” বার্তাটি দেখতে ওপরের ফর্মে নিজে পাঁচ-ছয় অক্ষরের একটি ঠিকানা টাইপ করে জমা দিন। শেষ লাইনে দেখুন: checkbox-এর মান "on", আর JSON.stringify দিয়ে লগ করা হয়েছে যাতে লম্বা object এক লাইনে আসে।

CSS দিয়ে দেখানো — :invalid বনাম :user-invalid

CSS-এর :invalid পাতা খোলার মুহূর্তেই সব ফাঁকা required ঘরের সাথে মেলে — কিছু লেখার আগেই পুরো ফর্ম লাল! সমাধান CSS Selectors Level 4-এর :user-invalid: এটি মেলে শুধু ব্যবহারকারী ঘরটির সাথে কাজ করার পর — মান বদলে ঘর ছেড়ে গেলে, বা জমার চেষ্টা করলে। আমরা Edge-এ যাচাই করেছি: “0171” টাইপ করার সময় :user-invalid মেলেনি, অন্য ঘরে যাওয়ার পর মিলেছে। Firefox ৮৮ (২০২১), Safari ১৬.৫ আর Chrome/Edge ১১৯ (২০২৩) থেকে এটি সমর্থন করে।

input:user-invalid { border-color: #b00020; }
input:user-valid   { border-color: #1b7f3b; }
/* শুধু রঙ যথেষ্ট নয় — পাশে লেখায় বার্তাও দিন (অংশ ৭) */

৭ · ভালো অভিজ্ঞতা — কখন বলবেন, কীভাবে বলবেন, কাকে বলবেন

কখন ভুল দেখাচ্ছেন, সেটিও নিয়মের মতোই জরুরি। এক চরম: শুধু জমার সময় সব ভুল একসাথে — দশটি ঘর পূরণের পর জানা গেল প্রথম ঘরেই ভুল। অন্য চরম: প্রথম অক্ষর টাইপ করতেই লাল বার্তা — “চিৎকার করা ফর্ম”। মাঝামাঝি পথটি inline validation-এর সবচেয়ে প্রচলিত নিয়ম:

1focus      ঘরে ঢুকলেন2input      "01"3input      "0171"4blur       অন্য ঘরে গেলেন5input      "0171234"6input      "01712345678"
ফোন ঘরের অবস্থা
value
valid?
touched
aria-invalid
  1. ঘরে ঢোকা। ঘরটি ফাঁকা, তাই প্রযুক্তিগতভাবে অবৈধ। কিন্তু ব্যবহারকারী এখনো কিছু করেননি — এখন বার্তা দেখানো মানে তাঁকে দোষ দেওয়া। আমরা একটি পতাকা রাখি: touched = false.
  2. টাইপ শুরু। “01” — অবৈধ, কিন্তু অসম্পূর্ণ। input event এসেছে, তবু touched মিথ্যা বলে আমরা চুপ। ব্যবহারকারী নিজের গতিতে লিখুন।
  3. এখনো টাইপ চলছে। “0171” — এখনো চুপ। এই নীরবতাই :user-invalid-এর আচরণের সাথে মেলে।
  4. ঘর ছাড়া — এবার বলার সময়। blur (ফর্ম-স্তরে focusout) এল, অথচ মান “0171”. ব্যবহারকারী মনে করছেন তিনি শেষ করেছেন, তাই এখন বার্তা দেখানো সঠিক। touched = true, aria-invalid="true", আর ঘরের নিচে লেখায় বার্তা।
  5. ফিরে এসে সারানো — এবার প্রতিটি অক্ষরে। একবার ভুল দেখানোর পর নিয়ম উল্টে যায়: এখন প্রতিটি input-এ যাচাই, যাতে ব্যবহারকারী সারানোর সাথে সাথে বুঝতে পারেন কখন ঠিক হলো। “0171234” — এখনো অবৈধ, বার্তা থাকে।
  6. ঠিক হতেই বার্তা উধাও। শেষ অঙ্কটি লেখার মুহূর্তে বার্তা মুছে যায়, aria-invalid="false". ব্যবহারকারীকে ঘর ছেড়ে যেতে হয়নি। সংক্ষেপে: ভুল দেখাতে ধীর, ঠিক স্বীকার করতে দ্রুত।

সবার জন্য error — রঙ নয়, লেখা

শুধু লাল বর্ডার বর্ণান্ধ আর screen reader ব্যবহারকারীদের কাছে অদৃশ্য। তাই প্রতিটি error-এর চারটি অংশ:

<div id="errors" role="alert" tabindex="-1">
  <h2>২টি ঘর ঠিক করুন</h2>
  <ul>
    <li><a href="#phone">মোবাইল নম্বর: 01XXXXXXXXX আকারে ১১ অঙ্ক লিখুন</a></li>
    <li><a href="#terms">শর্তাবলিতে রাজি হতে টিক দিন</a></li>
  </ul>
</div>

এটি alert()-এর চেয়ে অনেক ভালো: alert বন্ধ করলেই বার্তা উধাও, আর কোন ঘরে ভুল তা বোঝা যায় না। এই নিয়মগুলো শুধু ভদ্রতা নয় — WCAG-এর “Error Identification” (3.3.1) নিয়ম বলে, স্বয়ংক্রিয়ভাবে ধরা ভুল লেখায় বর্ণনা করতে হবে (ভাবনার প্রশ্ন ০৫)।

OTP আর ফাইল — দুটি বিশেষ ঘর

OTP-র ছয়টি আলাদা বাক্সে পেস্ট, মুছে ফেলা, autofill সব নিজে সামলাতে হয়; একটি ঘরই সাধারণত ভালো:

<label for="otp">SMS-এ পাওয়া ৬ অঙ্কের কোড</label>
<input id="otp" name="otp" type="text" inputmode="numeric"
       autocomplete="one-time-code" pattern="\d{6}" maxlength="6" required>

type="number" নয়, কারণ OTP সংখ্যা নয় — শুরুর 0 হারিয়ে যেতে পারে। autocomplete="one-time-code" দিলে অনেক মোবাইল ব্রাউজার SMS থেকে কোডটি কিবোর্ডের ওপর পরামর্শ হিসেবে দেখায়।

ফাইলের ঘরে (type="file") accept="image/*" শুধু বাছাইয়ের জানালায় ফিল্টার করে — ব্যবহারকারী চাইলে “সব ফাইল” বেছে যেকোনো কিছু দিতে পারেন। আকার আর ধরন JavaScript-এ যাচাই করুন, আর server-এ আবার:

photo.addEventListener('change', () => {
  const file = photo.files[0];
  if (!file) return;
  if (file.size > 2 * 1024 * 1024) return photo.setCustomValidity('ছবি ২ MB-এর বেশি হতে পারবে না');
  photo.setCustomValidity('');
  const url = URL.createObjectURL(file);           // ফাইলটি না পাঠিয়েই দেখানো
  preview.src = url;
  preview.onload = () => URL.revokeObjectURL(url); // মেমরি ছেড়ে দেওয়া
});

এই কোডটি দেখানোর জন্য (চালাতে আসল ফাইল বাছাই লাগে)। URL.createObjectURL মেমরিতে থাকা ফাইলের জন্য একটি অস্থায়ী blob: ঠিকানা বানায়, যা <img>-এ বসানো যায় — আপলোডের আগেই ছবি দেখা যায়। revokeObjectURL না ডাকলে পাতা খোলা থাকা পর্যন্ত ফাইলটি মেমরিতে থাকে। file.type আসে মূলত ফাইলের নাম থেকে, তাই server-কে ধরন নিজে যাচাই করতে হয়।

৮ · পাঠানো আর server-এর উত্তর — async যাচাই, দুবার জমা, 422 আর নিরাপত্তা

ইউজারনেম খালি আছে কি না — debounce করা async যাচাই

“এই ইউজারনেম কি আগেই নেওয়া?” — এর উত্তর আছে শুধু server-এর ডেটাবেসে। প্রতিটি অক্ষরে অনুরোধ পাঠালে অর্থহীন অনুরোধের বন্যা, আর উত্তর উল্টো ক্রমে এলে ভুল ফল (পর্ব ৩৬-এর race condition). তাই async validation-এ লাগে debounce আর আগের অনুরোধ বাতিল:

let timer, controller;
username.addEventListener('input', () => {
  username.setCustomValidity('');                       // পুরোনো ফল মুছুন
  clearTimeout(timer);
  timer = setTimeout(async () => {                      // ৪০০ ms থামলে তবেই
    controller?.abort();                                // আগের অনুরোধ বাতিল
    controller = new AbortController();
    const q = encodeURIComponent(username.value);
    const res = await fetch(`/api/username-available?u=${q}`, { signal: controller.signal });
    const { available } = await res.json();
    username.setCustomValidity(available ? '' : 'এই নামটি আগেই নেওয়া হয়েছে');
    hint.textContent = available ? '✓ নামটি খালি আছে' : username.validationMessage;
  }, 400);
});

এই কোডটি দেখানোর জন্য (আসল server লাগে; বাতিল হওয়া অনুরোধের AbortError সামলানো সংক্ষেপের জন্য বাদ রাখা হয়েছে — পর্ব ৩৬ দেখুন)। এই যাচাই শুধু সুবিধা: দুজন একই মুহূর্তে একই নাম চাইলে দুজনেই “খালি আছে” দেখতে পারেন। আসল সিদ্ধান্ত হয় অ্যাকাউন্ট খোলার মুহূর্তে, ডেটাবেসের unique শর্তে — তখন server 422 দিয়ে জানায়।

জমা দেওয়ার পুরো পথ

জমা দেওয়া থেকে ফল দেখানো পর্যন্ত প্রতিটি ধাপে একটি করে ভুলের সুযোগ। পুরো পথটি দেখুন:

  1. থামানোpreventDefaultreload নয়
  2. client যাচাইreportValidityভুল হলে এখানেই শেষ
  3. পাঠানোfetch + FormDataবোতাম disabled
  4. server যাচাইআবার সব নিয়ম422 বা 201
  5. ফল দেখানোঘরে ঘরে বার্তাবা সফলতা
ধাপ ১দুবার জমা ঠেকানো এখান থেকেই
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  if (sending) return;          // আগের অনুরোধ এখনো চলছে
ধাপ ২ব্যবহারকারীর সুবিধা, নিরাপত্তা নয়
if (!form.reportValidity()) return;   // প্রথম ভুল ঘরে focus
এই ধাপ কেউ DevTools বা curl দিয়ে এড়াতে পারে — তাতে সমস্যা নেই,
কারণ ধাপ ৪ আছে
ধাপ ৩JSON বা FormData
btn.disabled = true; sending = true;
const res = await fetch('/api/orders', {
  method: 'POST',
  body: new FormData(form),    // ফাইল থাকলে এটিই; নইলে JSON-ও চলে
});
ধাপ ৪server-এর উত্তর
HTTP/1.1 422 Unprocessable Content
Content-Type: application/json

{ "errors": { "phone": "এই নম্বরে আগেই অ্যাকাউন্ট আছে",
              "district": "এই জেলায় এখনো ডেলিভারি নেই" } }
ধাপ ৫error ফিরিয়ে ঠিক ঘরে
for (const [key, msg] of Object.entries(body.errors)) {
  form.elements[key].setCustomValidity(msg);
  form.elements[key].setAttribute('aria-invalid', 'true');
}
form.reportValidity();            // প্রথম ভুল ঘরে focus
finally { btn.disabled = false; sending = false; }
  1. থামানো আর পাহারা। preventDefault(), তারপর একটি পতাকা: আগের অনুরোধ এখনো চলছে কি না। ধীর মোবাইল নেটওয়ার্কে ব্যবহারকারী “কিছু হচ্ছে না” ভেবে আবার চাপেন — এই লাইন না থাকলে একই অর্ডার বা পেমেন্ট দুবার। একে বলে double submit.
  2. Client-এ যাচাই। reportValidity() সব HTML শর্ত আর setCustomValidity বার্তা একসাথে দেখে, ভুল হলে প্রথম ভুল ঘরে নিয়ে যায়। এখানে থামলে server-এ অনুরোধই যায় না।
  3. পাঠানো। বোতাম disabled, লেখা “পাঠানো হচ্ছে…”। ফাইল থাকলে body: new FormData(form) (ব্রাউজার নিজেই multipart/form-data লেখে, পর্ব ৩৫); নইলে JSON-ও চলে।
  4. Server আবার সব যাচাই করে — format, তালিকা, শর্ত — আর এমন নিয়মও, যা শুধু server জানে: নম্বরটি আগেই নিবন্ধিত, এই জেলায় ডেলিভারি নেই। ভুল পেলে ফেরত দেয় 422 আর প্রতিটি ঘরের বার্তা। 400-ও চলে; 422 status code বলে “গঠন ঠিক, বিষয়বস্তু গ্রহণযোগ্য নয়”।
  5. ফল দেখানো। 422-এর প্রতিটি বার্তা ঠিক ঘরে — একই setCustomValidity, একই aria-invalid. ব্যবহারকারীর কাছে client আর server-এর ভুলে কোনো পার্থক্য থাকা উচিত নয়। সফল হলে (201) ধন্যবাদ বার্তা; আর finally-তে বোতাম আবার চালু, নেটওয়ার্ক ব্যর্থ হলেও।

চালিয়ে দেখুন — sign-up: পাসওয়ার্ড মেলানো, দুবার জমা আর server-এর 422

এখানে server-এর জায়গায় একটি নকল function, যা ১০০ ms পরে আসল Response object ফেরত দেয় — “rahim” নামটি “নেওয়া”। তাই res.ok, res.status, res.json() হুবহু fetch-এর মতো। চালানোর পর ওপরের ফর্মে নিজে পাসওয়ার্ড দুটি আলাদা লিখে বা নাম “rahim” দিয়ে চেষ্টা করুন।

const form = document.querySelector('#signup'), btn = form.querySelector('button');
const { username, password, password2 } = form.elements;
const fakeServer = (data) => new Promise((r) => setTimeout(r, 100)).then(() => data.username === 'rahim'
  ? Response.json({ errors: { username: 'এই নামটি আগেই নেওয়া হয়েছে' } }, { status: 422 })
  : Response.json({ id: 7 }, { status: 201 }));       // আসল অ্যাপে: fetch('/api/signup', …)
form.noValidate = true;                                 // যাচাই চলবে submit handler-এ
form.addEventListener('input', (e) => {
  if (e.target === username) username.setCustomValidity('');   // পুরোনো server-বার্তা মুছুন
  password2.setCustomValidity(password2.value === password.value ? '' : 'দুটি পাসওয়ার্ড মেলেনি');
});
form.addEventListener('submit', async (e) => {
  e.preventDefault();
  if (btn.disabled) return console.log('পাঠানো চলছে — দ্বিতীয় submit উপেক্ষা');
  if (!form.reportValidity()) return console.log('client:', password2.validationMessage);
  btn.disabled = true;
  try {
    const res = await fakeServer(Object.fromEntries(new FormData(form))), body = await res.json();
    if (res.ok) return console.log('সফল:', res.status, body);
    for (const [key, msg] of Object.entries(body.errors)) form.elements[key].setCustomValidity(msg);
    form.reportValidity(); console.log('server', res.status, '→', username.validationMessage);
  } finally { btn.disabled = false; }
});
const type = (el, v) => { el.value = v; el.dispatchEvent(new Event('input', { bubbles: true })); };
type(username, 'rahim'); type(password, 'dhaka2026'); type(password2, 'dhaka2025');
form.requestSubmit();                                           // ১) পাসওয়ার্ড মেলেনি
type(password2, 'dhaka2026'); form.requestSubmit(); form.requestSubmit();  // ২) দ্রুত দুবার
setTimeout(() => { type(username, 'rahim_bd'); form.requestSubmit(); }, 400);   // ৩) নাম বদলে

Edge-এ চালানো আউটপুট:

client: দুটি পাসওয়ার্ড মেলেনি
পাঠানো চলছে — দ্বিতীয় submit উপেক্ষা
server 422 → এই নামটি আগেই নেওয়া হয়েছে
সফল: 201 { id: 7 }

প্রথম চেষ্টা client-এই থেমেছে, server পর্যন্ত যায়নি। দ্বিতীয়বার দুটি requestSubmit() পরপর ডাকা হয়েছে — যেন ব্যবহারকারী দ্রুত দুবার চেপেছেন। প্রথমটি await-এ পৌঁছানোর আগেই btn.disabled = true করে দিয়েছিল, তাই দ্বিতীয়টি উপেক্ষিত। Server-এর 422 বার্তা ইউজারনেম ঘরে setCustomValidity দিয়ে বসেছে — আর ঠিক এই কারণে input listener-এর প্রথম লাইন জরুরি: নাম বদলাতেই পুরোনো server-বার্তা মুছে যায়। ওই লাইনটি মুছে আবার চালিয়ে দেখুন — তৃতীয় চেষ্টা “client:” দিয়ে আটকে যাবে, কারণ “rahim_bd” লিখলেও ঘরটি পুরোনো বার্তার জন্য অবৈধ থেকে যায়।

নিরাপত্তা — client-কে কখনো বিশ্বাস নয়

DevTools-এ required মোছা যায়, JavaScript বন্ধ করা যায়, ফর্ম ছাড়াই সরাসরি HTTP অনুরোধ পাঠানো যায়। তাই server-এর অলঙ্ঘনীয় নিয়ম:

C/C++/Python/Java জানলে

Server-এর দিকে যাচাই সব ভাষায় একই নীতিতে চলে, শুধু টুল আলাদা। Java-র Spring Boot-এ DTO-র field-এ @NotBlank, @Pattern(regexp = "01[3-9]\\d{8}") লিখে controller-এ @Valid দিলে ভুল ডেটা method পর্যন্ত পৌঁছায়ই না — ডিফল্টে 400 ফেরত যায় (Spring Boot দিয়ে REST API). Python-এর FastAPI-তে pydantic model একই কাজ করে আর ডিফল্টে 422 দেয়, প্রতিটি ভুল field-এর তালিকাসহ (FastAPI-র শুরু)। C বা C++-এ এমন ফ্রেমওয়ার্ক কম, তাই প্রতিটি ইনপুটের দৈর্ঘ্য আর ধরন হাতে যাচাই করতে হয় — না করলে buffer overflow-এর মতো বিপদ।

পরীক্ষা ও ইন্টারভিউতে
  • Client-side validation থাকলে server-side কেন লাগে? — client-এর কোড ব্যবহারকারী বদলাতে বা এড়াতে পারেন; client-এর যাচাই শুধু সুবিধার জন্য, নিরাপত্তার জন্য নয়।
  • form.submit() আর form.requestSubmit()-এর পার্থক্য? — submit() যাচাই আর submit event দুটোই এড়ায়; requestSubmit() বোতাম চাপার মতো দুটোই চালায়।
  • input আর change event-এর পার্থক্য? — input প্রতিটি পরিবর্তনে; change মান চূড়ান্ত হলে (লেখার ঘরে focus সরলে, checkbox/select-এ সাথে সাথে)।
  • type="number" ঘরের value-র টাইপ কী? — string; সংখ্যা চাইলে valueAsNumber (ফাঁকা হলে NaN).
  • setCustomValidity দিয়ে বার্তা দিলে কীভাবে ঘরটি আবার বৈধ হয়? — setCustomValidity('') ডেকে; না ডাকলে ঘরটি চিরকাল অবৈধ থাকে।

৯ · AI-এর যুগে — AI-এর লেখা ফর্ম যাচাই করা

AI কয়েক সেকেন্ডে সুন্দর একটি ফর্ম দেয়, যা ডেমোতে কাজও করে। কিন্তু ফর্মের আসল সমস্যা ধরা পড়ে ধীর নেটওয়ার্কে, screen reader-এ, ভুল ইনপুটে বা কেউ ইচ্ছা করে ভাঙতে চাইলে। বারবার দেখা ভুলগুলো:

AI যা লেখেকী হয়যা চাইবেন
শুধু JavaScript-এ যাচাই, server-এর কথা নেইcurl দিয়ে যেকোনো ডেটা ঢোকেserver-এ একই নিয়ম, 422 দিয়ে field error
ইমেইল বা ফোনের জন্য বিশাল regex, type="text"ভুল কিবোর্ড, autofill নেই, regex-এ নতুন বাগtype="email"/"tel" + ছোট pattern
label-এর বদলে শুধু placeholderটাইপ করলে নাম উধাও; screen reader-এ নামহীন ঘরপ্রতিটি ঘরে <label for>
alert('Invalid input')কোন ঘরে, কী ভুল — বোঝা যায় নাঘরের পাশে লেখা, aria-invalid, aria-describedby, focus
setCustomValidity(msg) আছে, '' দিয়ে মোছা নেইঠিক করার পরও ফর্ম জমা হয় নাপ্রতিটি input-এ আগে মোছা
বোতামের click শোনা, form.submit()Enter-এ কাজ করে না, বা যাচাই এড়িয়ে যায়ফর্মের submit event, requestSubmit()
পাঠানোর সময় বোতাম চালু থাকেদুবার অর্ডার, দুবার পেমেন্টdisabled + পতাকা, finally-তে ফেরানো
hidden ঘরে দাম, client-এ মোট হিসাবদাম বদলে কম টাকায় অর্ডারserver ডেটাবেস থেকে দাম

AI-কে ফর্ম বানাতে বলার সময় এই নিয়মগুলো শুরুতেই দিয়ে দিন — তাহলে উত্তর অনেক কাছাকাছি আসে:

আমার জন্য একটি checkout ফর্ম বানাও — HTML, CSS আর আধুনিক JavaScript (কোনো লাইব্রেরি ছাড়া)। ঘর: নাম, বাংলাদেশি মোবাইল নম্বর (01[3-9] তারপর ৮ অঙ্ক), জেলা (select), ঠিকানা, পেমেন্ট (radio), শর্তে রাজি (checkbox). নিয়ম: ১. আগে HTML দিয়ে যত যাচাই সম্ভব: সঠিক type, required, pattern, minlength; প্রতিটি ঘরে <label for>, প্রতিটিতে name আর সঠিক autocomplete. placeholder শুধু উদাহরণের জন্য। ২. JavaScript না চললেও ফর্ম যেন ব্রাউজারের নিজের যাচাইসহ কাজ করে; JavaScript চললে form.noValidate = true করে validity flag দেখে বাংলা বার্তা দেখাবে। ৩. error দেখাবে ঘরের নিচে লেখায়, aria-invalid আর aria-describedby দিয়ে; জমা ব্যর্থ হলে প্রথম ভুল ঘরে focus. alert() ব্যবহার করবে না। ৪. প্রথম error দেখাবে ঘর ছাড়ার (blur) পর, তারপর প্রতিটি input-এ; ঠিক হলে সাথে সাথে মুছবে। setCustomValidity ব্যবহার করলে প্রতিবার '' দিয়ে মোছার জায়গা দেখাবে। ৫. submit event-এ preventDefault, পাঠানোর সময় বোতাম disabled আর দুবার জমা ঠেকানো, fetch-এ পাঠিয়ে 422 উত্তরের {errors: {field: msg}} ঠিক ঘরে দেখাবে। ৬. আলাদা করে server-এর (Node.js/Express) যাচাই-কোড দাও, যা একই নিয়ম আবার মানে; দাম ফর্ম থেকে নেবে না। ৭. শেষে বলো কোন অংশ আমার নিজে screen reader আর ধীর নেটওয়ার্কে পরীক্ষা করা উচিত।

AI-এর ফর্ম হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১JavaScript আসার আগেই ওয়েবে ফর্ম ছিল। তখন একটি ফর্ম জমা দিলে কী ঘটত, আর সেই নকশার কোন অংশ আজও প্রতিটি ফর্মে বেঁচে আছে?

প্রথম দিকের ওয়েব ছিল শুধু পড়ার জন্য — লেখা আর লিংক। ১৯৯৩ সালের দিকে NCSA Mosaic ব্রাউজার আর পরীক্ষামূলক HTML প্রস্তাবগুলোতে ফর্ম আসে, আর ১৯৯৫ সালের নভেম্বরে HTML 2.0 (RFC 1866) সেটিকে মানদণ্ডে লিখে দেয়: <form>, <input> (text, password, checkbox, radio, hidden, submit, reset…), <select> আর <textarea>. JavaScript তখনো জন্মায়নি বা সবে জন্মেছে (পর্ব ০১)। তাহলে যাচাই হতো কোথায়?

উত্তর: শুধু server-এ। ব্যবহারকারী জমা দিলে ব্রাউজার প্রতিটি ঘরের name=value জোড়া জুড়ে একটি লেখা বানাত — phone=01712345678&district=dhaka — আর সেটি action-এর ঠিকানায় পাঠাত, GET হলে URL-এর শেষে, POST হলে অনুরোধের body-তে। Server-এ এই অনুরোধ নিত একটি প্রোগ্রাম, যা চলত CGI (Common Gateway Interface) নামের একটি সরল নিয়মে: web server প্রতিটি অনুরোধে একটি নতুন প্রোগ্রাম চালাত (প্রায়ই Perl বা C-তে লেখা), ফর্মের ডেটা দিত environment variable আর standard input দিয়ে, আর প্রোগ্রাম যা print করত তা-ই হতো নতুন পাতা। ভুল থাকলে নতুন পাতায় লেখা আসত “ফোন নম্বর ভুল, Back চেপে আবার চেষ্টা করুন” — আর Back চাপলে অনেক সময় পুরো ফর্ম ফাঁকা!

এই অভিজ্ঞতা কষ্টকর ছিল, আর ঠিক এই কারণেই ১৯৯৫-৯৬ সালে JavaScript-এর প্রথম জনপ্রিয় ব্যবহারগুলোর একটি ছিল ফর্ম যাচাই: জমার আগেই “ইমেইল ঘর ফাঁকা” বলে দেওয়া, যাতে একটি ধীর dial-up অনুরোধ বাঁচে।

আজও সেই নকশার মূল অংশ অটুট। name attribute-ই ডেটার চাবি; FormData ঠিক সেই নিয়মে ঘর বাছাই করে (disabled বাদ, টিক না দেওয়া checkbox বাদ), কারণ এই নিয়মগুলো তখন থেকেই। application/x-www-form-urlencoded format আর URLSearchParams (পর্ব ৩৫) সেই a=1&b=2 লেখা। আর সবচেয়ে বড় উত্তরাধিকার: JavaScript ছাড়াও একটি ফর্ম পুরোপুরি কাজ করতে পারে, যদি server তৈরি থাকে। আধুনিক ফ্রেমওয়ার্কের “progressive enhancement” বা “server actions”-এর ধারণা আসলে এই পুরোনো নকশায় ফিরে যাওয়া।

মূল উপলব্ধি: ফর্মের জন্ম server-কেন্দ্রিক যাচাই নিয়ে; JavaScript এসেছে পরে, সুবিধা যোগ করতে। তাই দায়িত্বের ক্রমটি ইতিহাসেই লেখা — server বাধ্যতামূলক, client ঐচ্ছিক উন্নতি।

প্র ০২HTML5 কীভাবে ব্রাউজারকে নিজেই ফর্ম যাচাই করতে শেখাল? type="email", required, pattern আসার আগে ডেভেলপাররা কী করতেন?

২০০০-এর দশকের শুরুতে প্রতিটি ওয়েবসাইট নিজের যাচাই-কোড লিখত, বা কোনো jQuery plugin ব্যবহার করত। একই “ইমেইল ঠিক আছে কি না” কোড লক্ষ লক্ষ সাইটে লক্ষ রকমভাবে লেখা হতো — অনেকগুলোতে বাগ, প্রায় কোনোটিই screen reader-বান্ধব নয়। তারিখ বাছাইয়ের জন্য প্রতিটি সাইট নিজের ক্যালেন্ডার widget বানাত। মোবাইলে সংখ্যার কিবোর্ড আনার কোনো উপায়ই ছিল না, কারণ ব্রাউজার জানতই না ঘরটিতে কী আসবে।

২০০৪ সালে Opera, Mozilla আর Apple-এর কয়েকজন মিলে WHATWG গড়ে তোলেন, আর Ian Hickson-এর সম্পাদনায় “Web Forms 2.0” নামে একটি প্রস্তাব আসে। মূল ধারণা: যাচাইয়ের প্রচলিত কাজগুলো ঘোষণামূলক (declarative) করা — “এই ঘর বাধ্যতামূলক”, “এটি ইমেইল”, “এটি ১ থেকে ১০-এর সংখ্যা” — আর বাকিটা ব্রাউজারের দায়িত্ব। Opera ব্রাউজার এর অনেকটা আগেভাগেই বাস্তবায়ন করে। পরে এই কাজ HTML5-এর মূল মানদণ্ডে মিশে যায়; W3C-তে HTML5-এর প্রথম public working draft আসে ২০০৮ সালের জানুয়ারিতে, আর চূড়ান্ত Recommendation ২০১৪ সালের অক্টোবরে।

নতুন input type (email, url, tel, number, date, range, color…), constraint attribute (required, pattern, min, max, step, minlength), আর তাদের পেছনের JavaScript API — validity, checkValidity, setCustomValidity — এই সময়েরই ফল। সবচেয়ে চতুর নকশাগত সিদ্ধান্ত ছিল পুরোনো ব্রাউজারের সাথে মানিয়ে চলা: যে ব্রাউজার type="email" চেনে না, সে এটিকে সাধারণ type="text" ধরে নেয় — কিছু ভাঙে না, শুধু বাড়তি সুবিধাটুকু পাওয়া যায় না।

তবে সব ব্রাউজার সমান গতিতে এগোয়নি। বহু বছর Safari ফর্ম যাচাই করত, কিন্তু ভুল থাকলেও জমা আটকাত না — Safari ১০.১ (২০১৭) পর্যন্ত। বুদবুদের চেহারা কোনো CSS দিয়ে বদলানো যায় না, আর :invalid পাতা খোলার মুহূর্তেই লাল দেখাত। ফলে অনেক দল HTML-এর শর্ত রেখে, novalidate দিয়ে, নিজস্ব বার্তা দেখানোর পথ নেয় — ঠিক যা এই পর্বের প্রথম কোডটি করে। :user-invalid-এর মতো সমাধান সব বড় ব্রাউজারে পৌঁছাতে লেগেছে ২০২৩ সাল পর্যন্ত।

মূল উপলব্ধি: মানদণ্ড সাধারণ কাজগুলো ব্রাউজারের ভেতরে নিয়ে যায়, যাতে প্রতিটি সাইটকে একই চাকা নতুন করে বানাতে না হয়। ভালো ডেভেলপার আগে দেখেন প্ল্যাটফর্ম কী দেয়, তারপর শুধু বাকিটুকু নিজে লেখেন।

প্র ০৩“Never trust the client” — নিরাপত্তার এই পুরোনো নীতিটি ফর্মের ক্ষেত্রে বাস্তবে কেমন দেখায়? hidden ঘরে দাম রাখলে কী হতে পারে?

Client-server সিস্টেমের একটি মৌলিক সত্য: server জানে না অনুরোধটি কে বা কী পাঠিয়েছে। আপনার সুন্দর ফর্ম, আপনার JavaScript, আপনার ব্রাউজার — server-এর কাছে এসবের কোনো প্রমাণ পৌঁছায় না। সে শুধু কিছু byte পায়। সেই byte একটি আসল ফর্ম থেকে আসতে পারে, DevTools-এ বদলানো ফর্ম থেকে, curl বা Postman থেকে, বা কারও লেখা একটি script থেকে।

এই সত্য না বোঝার সবচেয়ে পুরোনো আর বারবার দেখা উদাহরণ হলো hidden ঘরে দাম। ১৯৯০-এর শেষ আর ২০০০-এর শুরুর অনেক অনলাইন দোকান “কার্টে যোগ করুন” ফর্মে <input type="hidden" name="price" value="..."> রাখত, আর checkout-এর সময় সেই দামই বিশ্বাস করত। সাধারণ ব্যবহারকারী ঘরটি দেখতেও পেতেন না। কিন্তু পাতাটি সেভ করে মান বদলে আবার জমা দেওয়া, বা পরে একটি proxy টুল দিয়ে অনুরোধ মাঝপথে বদলানো খুবই সহজ ছিল। নিরাপত্তা-গবেষকরা বছরের পর বছর এমন ত্রুটি দেখিয়েছেন — শুধু দাম নয়, পরিমাণ ঋণাত্মক করা (যাতে মোট কমে যায়), ছাড়ের হার, শিপিং খরচ, এমনকি “এই অর্ডার কার” — সেই id-ও। এর নাম parameter tampering, আর OWASP-এর মতো সংগঠনের নির্দেশিকায় এটি বহু বছর ধরে একটি স্থায়ী বিষয়।

আজও একই ভুল নতুন রূপে ফিরে আসে। Single-page অ্যাপে client JSON-এ { "total": 1200 } পাঠায়; মোবাইল অ্যাপ ভাবে “অ্যাপটি তো আমাদের, কেউ বদলাবে না”; একটি API-তে "role": "admin" field পাঠালে সেটিও সেভ হয়ে যায়, কারণ server সব field চোখ বুজে ডেটাবেসে লিখেছে (একে বলে mass assignment). প্রতিটির মূলে একই ভুল ধারণা — “client-এর পাঠানো ডেটা আমাদের কোডেরই বানানো”।

সমাধানের নীতি সহজ: client থেকে নিন শুধু ব্যবহারকারীর ইচ্ছা (কোন পণ্য, কয়টি, কোন ঠিকানায়), আর সব তথ্য (দাম, ছাড়, কার অ্যাকাউন্ট, কী অনুমতি) server নিজের উৎস থেকে বের করুক। প্রতিটি field-এর জন্য একটি allow-list রাখুন — যা প্রত্যাশিত নয়, তা ফেলে দিন। আর client-side যাচাইকে কখনো “নিরাপত্তা স্তর” হিসেবে গুনবেন না।

মূল উপলব্ধি: ব্রাউজার ব্যবহারকারীর এলাকা, server আপনার। সীমানার ওপারে যা যায়, তা যেকোনো রূপে ফিরে আসতে পারে — তাই সীমানায় প্রতিটি জিনিস নতুন করে পরীক্ষা করুন।

প্র ০৪ভুল দেখানোর “সঠিক সময়” নিয়ে কি গবেষণা আছে? Luke Wroblewski-র কাজ ফর্ম ডিজাইনে কী বদলেছে?

Luke Wroblewski ছিলেন Yahoo-র মতো বড় প্রতিষ্ঠানে কাজ করা একজন product designer. ২০০৮ সালে Rosenfeld Media থেকে প্রকাশিত হয় তাঁর বই “Web Form Design: Filling in the Blanks”. বইটির মূল কথা ছিল: ফর্ম হলো ব্যবহারকারী আর প্রতিষ্ঠানের মধ্যে একটি কথোপকথন, আর প্রতিটি অপ্রয়োজনীয় ঘর, অস্পষ্ট label বা দেরিতে দেখানো ভুল সেই কথোপকথনে বাধা। তিনি eye-tracking আর usability গবেষণার ফল দিয়ে দেখান, label ঘরের ওপরে থাকলে চোখ সবচেয়ে কম নড়াচড়া করে, “ঐচ্ছিক” আর “বাধ্যতামূলক” চিহ্ন স্পষ্ট হওয়া জরুরি, আর প্রাথমিক বোতাম (জমা) আর গৌণ বোতাম (বাতিল) দেখতে আলাদা হওয়া উচিত।

২০০৯ সালে তিনি A List Apart-এ “Inline Validation in Web Forms” নামে একটি প্রবন্ধে একটি usability গবেষণার ফল প্রকাশ করেন, যা পরীক্ষা-প্রতিষ্ঠান Etre-র সাথে করা হয়েছিল। একই ফর্মের কয়েকটি সংস্করণ তুলনা করা হয়: শুধু জমার পরে ভুল দেখানো, আর বিভিন্ন সময়ে inline (ঘরের পাশে, সাথে সাথে) ভুল দেখানো। Inline সংস্করণে সফলভাবে ফর্ম শেষ করার হার বেড়েছিল, ভুল কমেছিল, সময় কমেছিল আর ব্যবহারকারীর সন্তুষ্টি বেড়েছিল। কিন্তু সবচেয়ে কাজের ফলটি ছিল সূক্ষ্ম: ঘরে টাইপ শুরু করতেই ভুল দেখালে (অর্থাৎ ব্যবহারকারী শেষ করার আগেই) মানুষ বিরক্ত ও বিভ্রান্ত হয়েছেন; ঘর ছেড়ে যাওয়ার পর দেখানো সবচেয়ে ভালো কাজ করেছে। এই পর্বের “আগে blur, তারপর input” নিয়মটি এই পর্যবেক্ষণেরই প্রচলিত রূপ।

গবেষণাটি আরও একটি সূক্ষ্ম কথা বলেছিল: পাসওয়ার্ড বা ইউজারনেমের মতো ঘর, যেখানে উত্তর আগে থেকে জানা যায় না, সেখানে চলতি feedback (যেমন “নামটি খালি আছে”) সবচেয়ে বেশি সাহায্য করে; কিন্তু নাম বা ঠিকানার মতো ঘর, যার উত্তর ব্যবহারকারী নিশ্চিতভাবে জানেন, সেখানে প্রতিটি ঘরে সবুজ টিক তেমন কিছু যোগ করে না।

মূল উপলব্ধি: যাচাইয়ের প্রযুক্তি (Constraint API, setCustomValidity) আর যাচাইয়ের অভিজ্ঞতা দুটি আলাদা দক্ষতা। প্রথমটি ঠিক করে কী ভুল; দ্বিতীয়টি ঠিক করে ব্যবহারকারী সেটি কখন ও কীভাবে জানবেন — আর অর্ডার শেষ হবে কি না, তা প্রায়ই নির্ভর করে দ্বিতীয়টির ওপর।

প্র ০৫শুধু লাল বর্ডার দিয়ে ভুল দেখালে কেন তা accessibility-র নিয়ম ভাঙে? WCAG কী বলে, আর এর পেছনের যুক্তি কী?

WCAG (Web Content Accessibility Guidelines) প্রকাশ করে W3C-র Web Accessibility Initiative. প্রথম সংস্করণ ১৯৯৯ সালে, WCAG 2.0 আসে ২০০৮ সালের ডিসেম্বরে, 2.1 আসে ২০১৮ সালে, আর 2.2 আসে ২০২৩ সালের অক্টোবরে। WCAG 2.0 পরে ISO/IEC 40500 আন্তর্জাতিক মানদণ্ড হিসেবেও গৃহীত হয়েছে। বহু দেশের আইন ও সরকারি নিয়ম সরাসরি বা পরোক্ষভাবে WCAG-এর দিকে ইঙ্গিত করে — যেমন ইউরোপীয় ইউনিয়নের সরকারি ওয়েবসাইটের নিয়ম আর যুক্তরাষ্ট্রের ফেডারেল সংস্থার Section 508.

ফর্মের ভুলের জন্য সবচেয়ে সরাসরি নিয়ম 3.3.1 Error Identification (Level A — অর্থাৎ সবচেয়ে মৌলিক স্তর): কোনো ইনপুটের ভুল স্বয়ংক্রিয়ভাবে ধরা পড়লে, কোন ঘরে ভুল তা চিহ্নিত করতে হবে, আর ভুলটি ব্যবহারকারীকে লেখায় বর্ণনা করতে হবে। সাথে আছে 1.4.1 Use of Color: শুধু রঙ দিয়ে কোনো তথ্য বোঝানো যাবে না। আর Level AA-তে 3.3.3 Error Suggestion: সংশোধনের উপায় জানা থাকলে সেটিও বলতে হবে (“01 দিয়ে শুরু করে ১১ অঙ্ক”), যদি না তাতে নিরাপত্তা ক্ষুণ্ন হয়। WCAG 2.2 আরও যোগ করেছে 3.3.7 Redundant Entry — একই প্রক্রিয়ায় আগে দেওয়া তথ্য আবার টাইপ করতে বাধ্য না করা।

যুক্তিটি বোঝা সহজ, যদি কয়েকজন ব্যবহারকারীর কথা ভাবেন। একজন বর্ণান্ধ মানুষ লাল আর সবুজ বর্ডার প্রায় একই দেখেন। একজন দৃষ্টিপ্রতিবন্ধী মানুষ screen reader-এ শোনেন: বর্ডারের রঙ শব্দে রূপান্তরিত হয় না, কিন্তু aria-invalid="true" হয় (“invalid entry”), আর aria-describedby দিয়ে জোড়া বার্তাটি ঘরের নামের পরেই পড়া হয়। কম দৃষ্টিশক্তির একজন মানুষ পর্দা অনেক বড় করে দেখেন — পাতার ওপরে একটি লাল বাক্স হয়তো তাঁর দৃষ্টির বাইরে; তাই প্রথম ভুল ঘরে focus নিয়ে যাওয়া জরুরি। রোদে মোবাইলে ফর্ম পূরণ করা যে কারও জন্যও হালকা লাল বর্ডার চোখে পড়ে না।

এখানেই ব্রাউজারের নিজস্ব বুদবুদের একটি দুর্বলতা: কয়েক সেকেন্ড পর মিলিয়ে যায়, আর চেহারা বদলানো যায় না। তাই অনেক দল স্থায়ী, লেখায় লেখা বার্তা বেছে নেয়। Accessibility-র জন্য করা এই কাজ প্রায় সবসময় সবার অভিজ্ঞতা ভালো করে — একে বলে “curb-cut effect”, ফুটপাতের ঢালু অংশের মতো, যা হুইলচেয়ারের জন্য বানানো হলেও ট্রলি-ব্যাগ আর সাইকেল নিয়েও সবাই ব্যবহার করেন।

মূল উপলব্ধি: ভুল দেখানো মানে তথ্য পৌঁছে দেওয়া, আর তথ্যকে একাধিক পথে পাঠাতে হয় — লেখা, প্রোগ্রামের পড়ার মতো attribute, আর focus. যে ভুলটি একজন ব্যবহারকারী জানতে পারেন না, সেটি তাঁর কাছে ফর্মের একটি বাগ।

অনুশীলন

বেশিরভাগ কাজ ব্রাউজারের DevTools-এ দেখা আর ভাবা — কোড লিখতে হবে শুধু শেষটিতে।

  1. আগে অনুমান, পরে যাচাই: একটি <input type="number" id="q"> ফাঁকা আছে। খাতায় লিখুন প্রতিটির ফল, তারপর console-এ মিলিয়ে নিন: (ক) q.value (খ) Number(q.value) (গ) q.valueAsNumber (ঘ) ঘরে 5 লিখে q.value + 1

    (ক) "" — ফাঁকা string. (খ) 0 — Number("") শূন্য দেয়, তাই “দেননি” আর “শূন্য দিয়েছেন” আলাদা করা যায় না; এটিই ফাঁদ। (গ) NaN — ফাঁকা বোঝানোর সৎ উপায়, Number.isNaN() দিয়ে ধরুন। (ঘ) "51" — value string, তাই + জোড়া লাগায় (পর্ব ০৬)। ঠিক উপায়: q.valueAsNumber + 1 দেয় 6.

  2. নিরীক্ষা: যেকোনো ওয়েবসাইটের একটি ফর্ম খুলুন (বা এই পর্বের প্রথম কোডের ফর্ম)। DevTools-এর Elements প্যানেলে একটি ঘরের required মুছে দিন, তারপর ঘরটি ফাঁকা রেখে জমা দিন। কী ঘটল? এ থেকে server-এর দায়িত্ব সম্পর্কে কী শিখলেন?

    ব্রাউজার আর আটকায় না — ফাঁকা ঘর নিয়েই জমা হয় (বা আপনার JavaScript যদি নিজে নিয়ম না রাখে, ফাঁকা মানই পাঠানো হয়)। এক মিনিটে যে কেউ এটি করতে পারেন, কোনো বিশেষ দক্ষতা ছাড়া। এর মানে HTML বা JavaScript-এর প্রতিটি নিয়ম শুধু সৎ ব্যবহারকারীর সুবিধার জন্য। ডেটাবেসে ফাঁকা ফোন নম্বর যেন না ঢোকে, তা নিশ্চিত করতে পারে শুধু server-এর যাচাই। আরও এক ধাপ এগিয়ে দেখুন: Network প্যানেলে অনুরোধটি ডান-ক্লিক করে “Copy as cURL” — terminal-এ সেটি বদলে চালালে ফর্মই লাগে না।

  3. বাগ খুঁজুন: একটি AI এই কোড দিয়েছে: email.addEventListener('blur', () => { if (!email.value.endsWith('.bd')) email.setCustomValidity('শুধু .bd ঠিকানা'); }); একজন ব্যবহারকারী প্রথমে “a@x.com” লিখলেন, ঘর ছাড়লেন, তারপর ঠিক করে “a@x.com.bd” লিখে জমা দিলেন। কী ঘটবে, কেন, আর কীভাবে সারাবেন?

    ফর্ম জমা হবে না — ঠিক ঠিকানা লেখার পরও। প্রথম blur-এ setCustomValidity বার্তা বসিয়েছে, কিন্তু শর্ত পূরণ হলে বার্তা মোছার কোনো লাইন নেই; তাই customError চিরকাল সত্য। তার ওপর ব্যবহারকারী জমা দেওয়ার আগে আবার ঘর ছাড়েননি, তাই blur-ও আসেনি। সমাধান: প্রতিবার শর্তটি দুই দিকেই লিখুন — email.setCustomValidity(email.value.endsWith('.bd') ? '' : 'শুধু .bd ঠিকানা') — আর এটি input event-এ চালান, শুধু blur-এ নয়। দেখানোর সময় blur-এর পরে, কিন্তু validity-র হিসাব প্রতিটি পরিবর্তনে।

  4. ছোট কোড লিখুন: server-এর জন্য একটি pure function validateOrder(data) লিখুন, যা checkout ফর্মের object নেয় (name, phone, district, pay, terms) আর ভুলগুলোর একটি object ফেরত দেয় — যেমন { phone: '…' }; সব ঠিক হলে {}. জেলা হতে হবে একটি নির্দিষ্ট তালিকার মধ্যে। Node.js-এ { phone: '01212345678', district: 'mars' } দিয়ে পরীক্ষা করুন।

    একটি সমাধান:

    const DISTRICTS = new Set(['dhaka', 'rajshahi', 'khulna', 'chattogram']);
    const PAY = new Set(['cod', 'mobile']);
    
    function validateOrder(data) {
      const errors = {};
      const s = (v) => (typeof v === 'string' ? v.trim() : '');
      if (s(data.name).length < 3) errors.name = 'নাম অন্তত ৩ অক্ষরের হতে হবে';
      if (!/^01[3-9]\d{8}$/.test(s(data.phone))) errors.phone = '01XXXXXXXXX আকারে ১১ অঙ্ক লিখুন';
      if (!DISTRICTS.has(data.district)) errors.district = 'তালিকা থেকে একটি জেলা বাছাই করুন';
      if (!PAY.has(data.pay)) errors.pay = 'পেমেন্টের ধরন বাছাই করুন';
      if (data.terms !== 'on') errors.terms = 'শর্তাবলিতে রাজি হতে হবে';
      return errors;
    }

    পরীক্ষার ইনপুটে ফল আসবে পাঁচটি key-সহ object — name, phone, district, pay, terms — কারণ নাম, পেমেন্ট আর শর্ত পাঠানোই হয়নি। তিনটি বিষয় খেয়াল করুন। এক, typeof পরীক্ষা: server-এ JSON আসলে string-এর বদলে array বা object আসতে পারে, তাই অনুমান করা চলে না। দুই, regex-এ এখানে ^…$ নিজে লিখতে হয়েছে — HTML-এর pattern-এর মতো নিজে থেকে anchor হয় না (পর্ব ৩০)। তিন, জেলা আর পেমেন্ট যাচাই হয়েছে allow-list দিয়ে — select-এ যা ছিল না, এমন মানও কেউ পাঠাতে পারে। Express-এ (পর্ব ৫০) ফলের object খালি না হলে res.status(422).json({ errors }).

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমJavaScript দিয়ে একটি ফর্ম জমা দেওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✋ submit event-এ preventDefault()
  2. 🛂 reportValidity() দিয়ে client-এ যাচাই
  3. 🔒 বোতাম disabled করে fetch দিয়ে পাঠানো
  4. 🏦 server-এ সব নিয়ম আবার যাচাই
  5. 🎯 422 হলে প্রতিটি বার্তা ঠিক ঘরে দেখানো

একটি ফর্মে required ঘর ফাঁকা। JavaScript থেকে কোন method ডাকলে ব্রাউজারের যাচাই চলবে আর submit event-ও আসবে?

  • form.submit()
  • form.checkValidity()
  • form.requestSubmit()
  • form.reset()

<input type="number"> ঘরে ব্যবহারকারী 5 লিখলেন। typeof input.value কী দেয়?

  • "number"
  • "string"
  • "object"
  • "undefined"

<input pattern="01[3-9]\d{8}"> ঘরে মান "x01712345678". validity.patternMismatch কী হবে?

  • true, কারণ pattern পুরো মানের সাথে মিলতে হয়
  • false, কারণ ভেতরে বৈধ নম্বর আছে
  • pattern অবৈধ, তাই উপেক্ষিত
  • শুধু valueMissing true হবে

একটি ঘরে el.setCustomValidity('ভুল') ডাকা হলো। পরে ব্যবহারকারী সঠিক মান লিখলেন, কিন্তু কোড আর কিছু ডাকেনি। ঘরটির অবস্থা কী?

  • সঠিক মান লেখায় নিজে থেকেই বৈধ হয়ে গেছে
  • ঘর ছাড়লে (blur) বৈধ হবে
  • জমা দেওয়ার চেষ্টা করলে বৈধ হবে
  • অবৈধই থাকবে, যতক্ষণ না setCustomValidity('') ডাকা হয়

একটি ফর্মে <input type="checkbox" name="terms"> টিক দেওয়া নেই। new FormData(form).get('terms') কী দেয়?

  • false
  • null
  • "off"
  • ""

একটি অনলাইন দোকানের checkout-এ পণ্যের দাম কোথা থেকে নেওয়া উচিত?

  • ফর্মের একটি hidden ঘর থেকে
  • JavaScript-এ হিসাব করা মোট টাকা, যা JSON-এ পাঠানো হয়
  • server-এর ডেটাবেস থেকে, পণ্যের id দেখে
  • ফর্ম থেকে, তবে pattern দিয়ে যাচাই করে

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. HTML আগে: প্রতিটি ঘরে <label>, name, সঠিক type আর autocomplete; placeholder label নয়।
  2. value সবসময় string — valueAsNumber, checked, আর পুরো ফর্মের জন্য FormData (একাধিক মানে getAll).
  3. ফর্মের submit event শুনুন, preventDefault() দিন; কোড থেকে requestSubmit(), কখনো submit() নয়।
  4. validity-র flag দেখে বাংলা বার্তা; setCustomValidity বসালে '' দিয়ে মোছার জায়গাও লিখুন; ভুল দেখান blur-এর পরে, লেখায়, aria-invalid আর focus-সহ।
  5. পাঠানোর সময় বোতাম disabled, server-এ প্রতিটি নিয়ম আবার, 422-এর বার্তা ঠিক ঘরে — আর দাম বা অনুমতি কখনো client থেকে নয়।

পরের পর্বে: ফর্মের অর্ধেক লেখা খসড়া, পছন্দের থিম, লগইনের অবস্থা — পাতা বন্ধ করলেও এগুলো মনে রাখবেন কীভাবে? localStorage, sessionStorage, cookie আর IndexedDB — কোনটি কখন, আর কোনটিতে কী কখনো রাখবেন না।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান