Now.js Framework Documentation

Now.js Framework Documentation

Text-based Elements (อิลิเมนต์ข้อความ)

TH 04 Sep 2026 01:40

Text-based Elements (อิลิเมนต์ข้อความ)

เอกสารสำหรับ TextElementFactory, PasswordElementFactory และ SearchElementFactory - components สำหรับ text input พร้อม autocomplete, validation และฟีเจอร์พิเศษ

📋 สารบัญ

  1. ภาพรวม
  2. TextElementFactory
  3. PasswordElementFactory
  4. SearchElementFactory
  5. Events
  6. แนวทางปฏิบัติที่ดี

ภาพรวม

Text-based elements จัดการ text input พร้อมฟีเจอร์ต่างๆ เช่น autocomplete, validation และ formatting

Element data-element คำอธิบาย
TextElementFactory text Text input พร้อม autocomplete, hidden input
PasswordElementFactory password Password พร้อม strength meter, toggle visibility
SearchElementFactory search Search input พร้อม debounce, results container

TextElementFactory

TextElementFactory จัดการ text inputs พร้อม autocomplete และรองรับ hidden input เมื่อเปิด autocomplete จะสร้าง hidden input เพื่อเก็บค่า (key) ขณะที่แสดงข้อความ (label) ให้ผู้ใช้

การใช้งานพื้นฐาน

<!-- Simple text input -->
<input type="text"
       data-element="text"
       name="username"
       placeholder="กรอก username">

<!-- Text พร้อม validation -->
<input type="text"
       data-element="text"
       data-validate="email"
       name="email"
       placeholder="กรอก email">

ฟีเจอร์ Autocomplete

Static Source (Array)

<input type="text"
       data-element="text"
       data-autocomplete="true"
       data-source='[{"value":"1","text":"กรุงเทพ"},{"value":"2","text":"เชียงใหม่"}]'
       name="city"
       placeholder="เลือกจังหวัด">

Datalist Source

<input type="text"
       data-element="text"
       list="cities"
       name="city">
<datalist id="cities">
  <option value="1" label="Bangkok">
  <option value="2" label="Chiang Mai">
  <option value="3" label="Phuket">
</datalist>

💡 หมายเหตุ: เมื่อใช้ datalist อีลิเมนต์ <datalist> จะถูกถอดออกและแปลงเป็น autocomplete dropdown ให้เอง

Ajax Source

<input type="text"
       data-element="text"
       data-autocomplete="true"
       data-source="/api/cities"
       data-min-length="2"
       name="city"
       placeholder="ค้นหาจังหวัด...">

Expected API Response:

{
  "success": true,
  "data": [
    {"value": "1", "text": "กรุงเทพ"},
    {"value": "2", "text": "เชียงใหม่"}
  ]
}

เติมช่องอื่นจากรายการที่เลือก

ข้อมูลอื่นที่ API ส่งกลับมาพร้อมรายการ จะถูกเติมลงช่องที่ชื่อตรงกันให้เอง
การเลือกครั้งเดียวจึงเติมข้อมูลได้ทั้งชุด ไม่ต้องประกาศอะไรเพิ่มใน HTML
เพราะฝั่ง API เป็นผู้กำหนดว่าการเลือกหนึ่งครั้งจะเติมอะไรบ้าง

{
  "success": true,
  "data": [
    {"value": "SN-001", "text": "SN-001 : Notebook", "topic": "Notebook", "price": "25000"}
  ]
}
<input type="text" id="product_no" name="product_no"
       data-autocomplete="true" data-source="api/repair/autocomplete/find">

<!-- ได้ค่าจาก topic และ price ของรายการที่เลือก -->
<input type="text" id="topic" name="topic" readonly>
<input type="text" id="price" name="price">

การหาช่องปลายทางจะดูจาก id ก่อน แล้วจึงดูจาก name และหาเฉพาะภายในฟอร์มเดียวกัน
ฟอร์มคนละใบบนหน้าเดียวกันจึงไม่เติมข้ามกัน รองรับ element ทุกชนิด: input ปกติและ
textarea รับค่าโดยตรง, checkbox ติ๊กตามค่าที่ส่งมา, radio เลือกตัวที่ value ตรงกัน,
select แบบ multiple เลือกได้หลายค่าจาก array หรือข้อความคั่นด้วยจุลภาค และ element
ที่ไม่ใช่ช่องกรอก (<span>, <div>, ช่องในตาราง) จะได้ข้อความแทน ส่วนช่องปลายทาง
ที่เป็น autocomplete เองจะถูกเขียนผ่าน instance ของมัน ข้อความที่แสดงกับค่าจริง
ใน hidden input จึงตรงกันเสมอ

ค่าที่ไม่ถูกเติมให้: value text label key และ options เป็นคีย์ของตัวรายการเอง
ส่วน id เกือบทุกครั้งหมายถึง id ของข้อมูลที่ API ส่งมา ไม่ใช่ id ของข้อมูลที่ฟอร์มกำลังแก้ไข
การเติมจึงจะไปทับ id ของฟอร์ม และช่อง autocomplete ตัวมันเองก็ถูกข้ามเช่นกัน
หากต้องการใช้คีย์เหล่านี้จริงๆ ให้ระบุด้วย data-fill

กันข้อมูลค้าง: ถ้ารายการที่เลือกใหม่ไม่มีค่านั้น ช่องที่เคยถูกเติมจะถูกล้าง
และเมื่อผู้ใช้พิมพ์ต่อหลังเลือกไปแล้ว ทุกช่องที่การเลือกครั้งนั้นเติมไว้จะถูกล้างทันที
ไม่มีช่องไหนแสดงข้อมูลของรายการที่ไม่ได้เลือกแล้ว

แมปชื่อที่ไม่ตรงกัน หรือปิดการเติม — data-fill รับรายการคู่ ช่อง:คีย์
(ถ้าใส่ชื่อเดียวหมายถึงช่องกับคีย์ชื่อเดียวกัน) หรือรับเป็น JSON ก็ได้
เมื่อระบุ data-fill แล้ว จะเติมเฉพาะที่ระบุไว้เท่านั้น

<!-- คีย์ topic ของรายการ เติมลงช่องชื่อ equipment และไม่เติมช่องอื่น -->
<input type="text" name="product_no" data-autocomplete="true" data-source="/api/products"
       data-fill="equipment:topic">

<!-- แบบ JSON -->
<input type="text" name="product_no" data-autocomplete="true" data-source="/api/products"
       data-fill='{"equipment":"topic","price":"unit_price"}'>

<!-- ไม่เติมช่องใดเลย -->
<input type="text" name="product_no" data-autocomplete="true" data-source="/api/products"
       data-fill="false">

ค้นหาแบบลำดับชั้น

สำหรับช่องที่อยู่ (จังหวัด → อำเภอ → ตำบล → รหัสไปรษณีย์)

<input type="text"
       data-element="text"
       data-autocomplete="true"
       data-search-api="/api/address/search"
       data-search-field="subdistrict"
       name="subdistrict"
       placeholder="Search subdistrict...">

เมื่อผู้ใช้เลือกรายการ ช่องที่อยู่ที่เกี่ยวข้องทั้งหมดจะถูกเติมให้เอง

รองรับ Hidden Input

เมื่อเปิด autocomplete TextElementFactory จะสร้าง hidden input ไว้เก็บค่าคีย์

<!-- Before enhancement -->
<input type="text" name="city" data-element="text" data-autocomplete="true" ...>

<!-- After enhancement -->
<input type="text" name="city_text" ...>  <!-- Shows display text -->
<input type="hidden" name="city">          <!-- Stores key value -->

HTML Attributes

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-element string - ตั้งค่าเป็น text
data-autocomplete boolean false เปิด autocomplete
data-source string/JSON - แหล่งข้อมูล autocomplete (URL, array)
data-min-length number 2 ตัวอักษรขั้นต่ำก่อนค้นหา
data-max-results number 10 จำนวนผลลัพธ์สูงสุด
data-delay number 300 Debounce delay (ms)
data-hierarchy string - ระดับของลำดับชั้น (province/district/subdistrict)
data-dependent string - id ของช่องแม่สำหรับ dropdown ที่ขึ้นต่อกัน
data-callback string - ชื่อฟังก์ชันที่ใช้ render รายการเอง
data-fill string/JSON - ช่องที่รายการที่เลือกจะเติมให้: รายการ ช่อง:คีย์, JSON หรือ false เพื่อปิด ถ้าไม่ระบุจะเติมตาม id/name ที่ตรงกัน
data-search-api string - endpoint สำหรับค้นหาแบบลำดับชั้น
data-search-field string - ชื่อฟิลด์สำหรับค้นหาแบบลำดับชั้น
data-validate string - Validation rules (email, url, alpha, ฯลฯ)
data-formatter string - ชื่อฟังก์ชัน formatter

Built-in Validators

Validator Pattern คำอธิบาย
email ^[^\s@]+@[^\s@]+\.[^\s@]+$ รูปแบบ email
url HTTP/HTTPS URL pattern URL ที่ถูกต้อง
number ^-?\d*\.?\d+$ ค่าตัวเลข
integer ^-?\d+$ จำนวนเต็ม
alpha ^[a-zA-Z]+$ ตัวอักษรเท่านั้น
alphanumeric ^[a-zA-Z0-9]+$ ตัวอักษรและตัวเลข
usernameOrEmail Combined pattern ชื่อผู้ใช้หรืออีเมล

JavaScript API

// Get instance
const cityInput = document.querySelector('[name="city"]');
const instance = ElementManager.getInstance(cityInput);

// Access values
console.log(instance.element.value);      // Display text
console.log(instance.hiddenInput.value);  // Key value
console.log(instance.selectedValue);      // Currently selected key
console.log(instance.selectedItem);       // ข้อมูลทั้งรายการที่เลือก รวมคีย์อื่นๆ ที่ API ส่งมาด้วย

// Programmatically update autocomplete options
instance.populate([
  {value: '1', text: 'Option 1'},
  {value: '2', text: 'Option 2'}
]);

// Show/hide dropdown
instance.show();
instance.hide();

Custom Render Callback

// Define callback function
window.renderCityItem = function({key, value, search, level}) {
  const div = document.createElement('div');
  div.className = 'city-item';
  div.innerHTML = `
    <strong>${value}</strong>
    <span class="code">${key}</span>
  `;
  return div;
};
<input type="text"
       data-element="text"
       data-autocomplete="true"
       data-source="/api/cities"
       data-callback="renderCityItem"
       name="city">

PasswordElementFactory

PasswordElementFactory ขยาย TextElementFactory พร้อมฟีเจอร์เฉพาะ password: strength meter, toggle visibility, criteria list และ match validation

การใช้งานพื้นฐาน

<!-- Password พร้อม toggle visibility -->
<input type="password"
       data-element="password"
       name="password"
       minlength="8"
       required>

<!-- Password พร้อม strength meter -->
<input type="password"
       data-element="password"
       data-password-strength="true"
       name="password"
       required>

Password Strength Meter

<input type="password"
       data-element="password"
       data-password-strength="true"
       id="password"
       name="password">
<span id="result_password" class="comment">อย่างน้อย 8 ตัวอักษร</span>
<!-- Strength bar จะแทรกหลัง comment -->

CSS Classes บน Strength Bar:

  • .weak - สีแดง (< 40% เกณฑ์ผ่าน)
  • .medium - สีเหลือง (40-80% เกณฑ์ผ่าน)
  • .strong - สีเขียว (> 80% เกณฑ์ผ่าน)

รายการเงื่อนไขรหัสผ่าน

<input type="password"
       data-element="password"
       data-password-criteria-list="true"
       name="password">

เงื่อนไขเริ่มต้น:

  • ✅ อย่างน้อย 8 ตัวอักษร
  • ✅ มีตัวพิมพ์ใหญ่อย่างน้อยหนึ่งตัว
  • ✅ มีตัวพิมพ์เล็กอย่างน้อยหนึ่งตัว
  • ⬜ มีตัวเลขอย่างน้อยหนึ่งตัว (ปิดไว้เป็นค่าเริ่มต้น)
  • ⬜ มีอักขระพิเศษอย่างน้อยหนึ่งตัว (ปิดไว้เป็นค่าเริ่มต้น)

Password Match Validation

<!-- Password หลัก -->
<input type="password"
       data-element="password"
       id="password"
       name="password"
       required>

<!-- ยืนยัน password -->
<input type="password"
       data-element="password"
       data-target-password="password"
       name="password_confirm"
       required>

HTML Attributes

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-element string - ตั้งค่าเป็น password
data-password-strength boolean false แสดง strength meter
data-password-criteria-list boolean false แสดง checklist เกณฑ์
data-target-password string - ID ของช่อง password หลัก
minlength number 8 ความยาว password ขั้นต่ำ
maxlength number 50 ความยาว password สูงสุด

Toggle Visibility

เปิดใช้งานโดยค่าเริ่มต้น ผู้ใช้สามารถ:

  • คลิกไอคอนตาเพื่อแสดง/ซ่อน password
  • กด Alt + S keyboard shortcut

JavaScript API

const instance = ElementManager.getInstance(passwordInput);

// Check if passwords match
const confirmInput = document.getElementById('password_confirm');
const matches = instance.checkMatch(confirmInput);

// Check specific criterion
instance.passesCriterion('minLength', 'myPassword123'); // true/false
instance.passesCriterion('uppercase', 'myPassword123'); // true
instance.passesCriterion('numbers', 'myPassword123');   // true

// Get all failed criteria
const failed = instance.getFailedCriteria('weak');
// ['At least 8 characters', 'At least one uppercase letter']

// Update UI manually
instance.updateStrengthBar();
instance.updateCriteriaList();
instance.updateMatchStatus();

ปรับแต่งเงื่อนไขเอง

// Override default criteria in create/enhance
PasswordElementFactory.enhance(element, {
  passwordCriteria: {
    minLength: {enabled: true, value: 12, label: 'At least {value} characters'},
    uppercase: {enabled: true, label: 'At least one uppercase letter'},
    lowercase: {enabled: true, label: 'At least one lowercase letter'},
    numbers: {enabled: true, label: 'At least one number'},
    special: {enabled: true, label: 'At least one special character (!@#$%^&*)'}
  }
});

SearchElementFactory

SearchElementFactory ขยาย TextElementFactory พร้อมฟีเจอร์เฉพาะการค้นหา: debounced input, clear button และ results container

การใช้งานพื้นฐาน

<!-- Simple search input -->
<input type="search"
       data-element="search"
       name="search"
       placeholder="ค้นหา...">

ค้นหาพร้อมแสดงผลลัพธ์

// Create search with custom onSearch handler
SearchElementFactory.enhance(searchInput, {
  onSearch: async (query) => {
    const response = await fetch(`/api/products?q=${query}`);
    const data = await response.json();
    return data.results; // [{value, text}, ...]
  }
});

HTML Attributes

Attribute ประเภท ค่าเริ่มต้น คำอธิบาย
data-element string - ตั้งค่าเป็น search
data-search-delay number 300 Debounce delay (ms)
data-min-length number 2 ตัวอักษรขั้นต่ำก่อนค้นหา
data-max-length number 100 ความยาวข้อความสูงสุด
data-max-results number 10 จำนวนผลลัพธ์สูงสุด
data-highlight-matches boolean true ไฮไลท์ข้อความที่ตรงกัน

ตัวเลือกการตั้งค่า

SearchElementFactory.config = {
  type: 'search',
  inputMode: 'search',
  searchDelay: 300,
  minLength: 2,
  maxLength: 100,
  placeholder: 'Search',
  wrapperClass: 'search',
  inputClass: 'search-input',
  clearClass: 'search-clear',
  loadingClass: 'search-loading',
  resultsClass: 'search-results',
  activeClass: 'active',
  maxResults: 10,
  highlightMatches: true,
  formIntegration: true,
  keyboard: {
    selectKeys: ['Enter', ' '],
    closeKeys: ['Escape'],
    navigateUpKeys: ['ArrowUp'],
    navigateDownKeys: ['ArrowDown']
  }
};

ปุ่มล้างค่า

ปุ่มล้างค่าถูกเพิ่มให้เองและแสดงเมื่อช่องมีค่าอยู่

.search-clear {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
}

สถานะกำลังโหลด

.search-loading {
  position: relative;
}

.search-loading::after {
  content: '';
  position: absolute;
  right: 40px;
  top: 50%;
  width: 16px;
  height: 16px;
  border: 2px solid #ccc;
  border-top-color: #333;
  border-radius: 50%;
  animation: spin 0.6s linear infinite;
}

JavaScript API

const instance = ElementManager.getInstance(searchInput);

// Show results programmatically
instance.createSuggestionList([
  {value: '1', text: 'Result 1'},
  {value: '2', text: 'Result 2'}
]);

// Access clear button
searchInput.clearButton.click(); // Clear input

เหตุการณ์

Common Events

Event เมื่อเกิด Detail
element:change ค่าเปลี่ยน {value, valid}
element:valid Validation ผ่าน {value}
element:invalid Validation ไม่ผ่าน {value, error}
input ผู้ใช้พิมพ์ Native event
change ค่าถูกยืนยัน Native event
focus element ได้โฟกัส Native event
blur element เสียโฟกัส Native event

เหตุการณ์ของ TextElementFactory

element.addEventListener('change', (e) => {
  const instance = ElementManager.getInstance(e.target);
  console.log('Display value:', e.target.value);
  console.log('Selected key:', instance.hiddenInput?.value);
});

เหตุการณ์ของ PasswordElementFactory

// Listen to password changes with strength info
passwordInput.addEventListener('input', (e) => {
  const instance = ElementManager.getInstance(e.target);
  const failed = instance.getFailedCriteria(e.target.value);
  console.log('Failed criteria:', failed);
});

เหตุการณ์ของ SearchElementFactory

// Now.js event system
Now.on('search:performed', ({elementId, query}) => {
  console.log(`Search performed on ${elementId}: ${query}`);
});

แนวทางปฏิบัติที่ดี

1. ✅ ใช้ Hidden Input สำหรับ Key-Value Pairs

<!-- ✅ ดี: Autocomplete เก็บ key ใน hidden input -->
<input type="text"
       data-element="text"
       data-autocomplete="true"
       data-source="/api/cities"
       name="city">

<!-- Form ส่ง: city=123 (hidden input value ไม่ใช่ display text) -->

2. ✅ ตั้ง Min Length ของ Autocomplete ให้เหมาะสม

<!-- ✅ ดี: รออย่างน้อย 2 ตัวอักษร -->
<input type="text"
       data-element="text"
       data-autocomplete="true"
       data-source="/api/products"
       data-min-length="2">

<!-- ❌ ไม่ดี: ยิงทุกครั้งที่กดปุ่ม -->
<input type="text"
       data-autocomplete="true"
       data-source="/api/products"
       data-min-length="0">

3. ✅ ใช้ Password Strength สำหรับการลงทะเบียน

<!-- ✅ ดี: แสดง strength meter ระหว่างลงทะเบียน -->
<form data-form="register">
  <input type="password"
         data-element="password"
         data-password-strength="true"
         data-password-criteria-list="true"
         name="password">
</form>

4. ✅ Match Passwords ด้วย data-target-password

<!-- ✅ ดี: ใช้ built-in match validation -->
<input type="password" id="password" name="password">
<input type="password"
       data-target-password="password"
       name="password_confirm">

5. ✅ หน่วงการค้นหา

<!-- ✅ ดี: SearchElementFactory มี debounce ในตัว -->
<input type="search"
       data-element="search"
       data-search-delay="300">

<!-- ❌ ไม่ดี: ยิงทุกครั้งที่กดปุ่ม -->
<input type="text" oninput="search(this.value)">

6. ✅ ให้ข้อความผิดพลาดที่ชัดเจน

<input type="text"
       data-element="text"
       data-validate="email"
       data-error-email="Please enter a valid email address like example@domain.com"
       name="email">

เอกสารที่เกี่ยวข้อง