פקדי טפסים משופרים

עם אירוע חדש וממשקי API של רכיבים מותאמים אישית, הרבה יותר קל עכשיו להשתתף בטפסים.

Arthur Evans

מפתחים רבים מפתחים פקדי טפסים מותאמים אישית, כדי לספק פקדים שאינם מובנים בדפדפן, או כדי להתאים אישית את המראה והתחושה מעבר למה שאפשר באמצעות פקדי הטפסים המובנים.

עם זאת, לפעמים קשה לשכפל את התכונות של פקדי טפסים מובנים ב-HTML. כדאי להביא בחשבון כמה תכונות שרכיב <input> מקבל באופן אוטומטי כשמוסיפים אותו לטופס:

  • הקלט יתווסף באופן אוטומטי לרשימת הפקדים של הטופס.
  • הערך של הקלט נשלח באופן אוטומטי עם הטופס.
  • הקלט משתתף באימות הטופס. אפשר לעצב את הקלט באמצעות המחלקות :valid ו-:invalid.
  • הקלט מקבל התראה כשהטופס מתאפס, כשהטופס נטען מחדש או כשהדפדפן מנסה למלא באופן אוטומטי את הרשומות של הטפסים.

בדרך כלל, פקדי טפסים מותאמים אישית כוללים כמה מהתכונות האלה. המפתחים יכולים לעקוף חלק מהמגבלות של JavaScript. למשל, הוספת <input> מוסתר לטופס כדי להשתתף בשליחת טופס. אבל יש תכונות אחרות שלא ניתן לשכפל ב-JavaScript בלבד.

שתי תכונות אינטרנט חדשות מאפשרות ליצור בקלות פקדים מותאמים אישית בטפסים, ולהסיר את המגבלות של אמצעי הבקרה המותאמים אישית הקיימים:

  • האירוע formdata מאפשר לאובייקט JavaScript שרירותי להשתתף בשליחת טופס. כך אפשר להוסיף נתוני טופס בלי להשתמש ב-<input> מוסתר.
  • ה-API של רכיבים מותאמים אישית המשויכים ל-Forms מאפשר לרכיבים מותאמים אישית לפעול יותר כמו פקדים מובנים בטופס.

ניתן להשתמש בשתי התכונות האלה כדי ליצור סוגים חדשים של אמצעי בקרה שעובדים טוב יותר.

API מבוסס-אירועים

האירוע formdata הוא API ברמה נמוכה שמאפשר לכל קוד JavaScript להשתתף בשליחת טופס. המנגנון פועל כך:

  1. אתם מוסיפים האזנה לאירועים של formdata לטופס שאיתו רוצים לבצע אינטראקציה.
  2. כשמשתמש לוחץ על לחצן השליחה, הטופס מפעיל אירוע formdata, שכולל אובייקט FormData שמכיל את כל הנתונים שנשלחים.
  3. כל אוזן formdata מקבל הזדמנות להוסיף לנתונים או לשנות אותם לפני שליחת הטופס.

דוגמה לשליחת ערך יחיד ב-event listener מסוג formdata:

const form = document.querySelector('form');
// FormData event is sent on <form> submission, before transmission.
// The event has a formData property
form.addEventListener('formdata', ({formData}) => {
  // https://developer.mozilla.org/docs/Web/API/FormData
  formData.append('my-input', myInputValue);
});

אפשר לנסות את זה באמצעות הדוגמה שלנו ב-Glitch. כדי לראות את ה-API בפעולה, מומלץ להפעיל אותו ב-Chrome 77 ואילך.

תאימות דפדפן

תמיכה בדפדפן

  • 5
  • 12
  • 4
  • 5

מקור

רכיבים מותאמים אישית שמשויכים לטופס

אפשר להשתמש ב-API מבוסס-אירועים עם כל סוג של רכיב, אבל הוא מאפשר רק לבצע אינטראקציה עם תהליך השליחה.

בקרות סטנדרטיות של טפסים משתתפות בחלקים רבים במחזור החיים של הטופס, בנוסף לשליחה. רכיבים מותאמים אישית המשויכים לטופס נועדו לגשר על הפער בין ווידג'טים מותאמים אישית לבין אמצעי בקרה מובנים. רכיבים מותאמים אישית שמשויכים לטופס תואמים לכמה מהתכונות של רכיבי טופס סטנדרטיים:

  • כשמציבים בתוך <form> רכיב מותאם אישית שמשויך לטופס, הוא משויך באופן אוטומטי לטופס, למשל פקד שסופק על ידי הדפדפן.
  • אפשר להוסיף תווית לרכיב באמצעות רכיב <label>.
  • הרכיב יכול להגדיר ערך שיישלח באופן אוטומטי עם הטופס.
  • הרכיב יכול להגדיר דגל שמציין אם יש לו קלט חוקי או לא. אם אחד מאמצעי הבקרה של הטופס מכיל קלט לא חוקי, לא ניתן לשלוח את הטופס.
  • הרכיב יכול לספק קריאות חוזרות (callback) לחלקים שונים במחזור החיים של הטופס – למשל, כשהטופס מושבת או איפוס למצב ברירת המחדל שלו.
  • הרכיב תומך בסיווגים רגילים של CSS עבור בקרות של טפסים, כמו :disabled ו-:invalid.

מדובר בהרבה תכונות! המאמר הזה לא ייגע בכולם, אבל נתאר את העקרונות הבסיסיים שדרושים כדי לשלב את הרכיב המותאם אישית בטופס.

הגדרת רכיב מותאם אישית שמשויך לטופס

כדי להפוך רכיב מותאם אישית לרכיב מותאם אישית המשויך לטופס, נדרשים מספר שלבים נוספים:

  • מוסיפים את המאפיין formAssociated הסטטי למחלקה של הרכיבים המותאמים אישית. זה מנחה את הדפדפן להתייחס לרכיב כמו לפקד טופס.
  • מפעילים את ה-method attachInternals() ברכיב כדי לקבל גישה לשיטות ולמאפיינים נוספים של פקדי טופס, כמו setFormValue() ו-setValidity().
  • מוסיפים את המאפיינים והשיטות הנפוצים שנתמכים על ידי פקדי הטופס, כמו name, value ו-validity.

כך הפריטים האלה משתלבים בהגדרה בסיסית של רכיב מותאם אישית:

// Form-associated custom elements must be autonomous custom elements--
// meaning they must extend HTMLElement, not one of its subclasses.
class MyCounter extends HTMLElement {

  // Identify the element as a form-associated custom element
  static formAssociated = true;

  constructor() {
    super();
    // Get access to the internal form control APIs
    this.internals_ = this.attachInternals();
    // internal value for this control
    this.value_ = 0;
  }

  // Form controls usually expose a "value" property
  get value() { return this.value_; }
  set value(v) { this.value_ = v; }

  // The following properties and methods aren't strictly required,
  // but browser-level form controls provide them. Providing them helps
  // ensure consistency with browser-provided controls.
  get form() { return this.internals_.form; }
  get name() { return this.getAttribute('name'); }
  get type() { return this.localName; }
  get validity() {return this.internals_.validity; }
  get validationMessage() {return this.internals_.validationMessage; }
  get willValidate() {return this.internals_.willValidate; }

  checkValidity() { return this.internals_.checkValidity(); }
  reportValidity() {return this.internals_.reportValidity(); }

  …
}
customElements.define('my-counter', MyCounter);

לאחר ההרשמה, אפשר להשתמש ברכיב הזה בכל מקום שבו משתמשים בפקד הטופס שמסופק על ידי הדפדפן:

<form>
  <label>Number of bunnies: <my-counter></my-counter></label>
  <button type="submit">Submit</button>
</form>

הגדרת ערך

ה-method attachInternals() מחזירה אובייקט ElementInternals שמספק גישה לממשקי API של בקרת טפסים. השיטה הבסיסית ביותר מביניהן היא setFormValue(), שקובעת את הערך הנוכחי של הבקרה.

השיטה setFormValue() יכולה לקבל אחד משלושת סוגי הערכים:

  • ערך למחרוזת.
  • אובייקט File.
  • אובייקט FormData. אפשר להשתמש באובייקט FormData כדי להעביר מספר ערכים (לדוגמה, אמצעי בקרה לקלט של כרטיס אשראי עשוי להעביר מספר כרטיס, תאריך תפוגה וקוד אימות).

כדי להגדיר ערך פשוט:

this.internals_.setFormValue(this.value_);

כדי להגדיר כמה ערכים, אפשר לבצע את הפעולה הבאה:

// Use the control's name as the base name for submitted data
const n = this.getAttribute('name');
const entries = new FormData();
entries.append(n + '-first-name', this.firstName_);
entries.append(n + '-last-name', this.lastName_);
this.internals_.setFormValue(entries);

אימות הקלט

אמצעי הבקרה יכול להשתתף גם באימות טפסים על ידי קריאה ל-method setValidity() באובייקט הפנימי.

// Assume this is called whenever the internal value is updated
onUpdateValue() {
  if (!this.matches(':disabled') && this.hasAttribute('required') &&
      this.value_ < 0) {
    this.internals_.setValidity({customError: true}, 'Value cannot be negative.');
  }
  else {
    this.internals_.setValidity({});
  }
  this.internals.setFormValue(this.value_);
}

אפשר לעצב רכיב מותאם אישית שמשויך לטופס באמצעות הסיווגים :valid ו-:invalid, בדיוק כמו פקד מובנה בטופס.

קריאה חוזרת (callback) במחזור החיים

ממשק API של רכיב מותאם אישית המשויך לטופס, כולל קבוצה של קריאות חוזרות (callback) נוספות של מחזור חיים שנקשרות למחזור החיים של הטופס. הקריאות החוזרות הן אופציונליות: אתם צריכים להטמיע קריאה חוזרת (callback) רק אם הרכיב שלכם צריך לבצע פעולה כלשהי בנקודה הזו של מחזור החיים.

void formAssociatedCallback(form)

הקריאה מופעלת כשהדפדפן משייך את הרכיב לרכיב טופס או מבטל את השיוך של הרכיב לרכיב טופס.

void formDisabledCallback(disabled)

נקראת אחרי שמצב disabled של הרכיב משתנה, כי המאפיין disabled של הרכיב הזה נוסף או הוסר, או כי המצב disabled השתנה ב-<fieldset> שהוא ישות האב של הרכיב הזה. הפרמטר disabled מייצג את המצב המושבת החדש של הרכיב. לדוגמה, הרכיב עשוי להשבית רכיבים ב-DOM של הצללית שלו כשהוא מושבת.

void formResetCallback()

תופעל שיחה אחרי איפוס הטופס. הרכיב אמור לאפס את עצמו למצב ברירת מחדל כלשהו. עבור רכיבי <input>, בדרך כלל צריך להגדיר את המאפיין value כך שיתאים למאפיין value בתגי העיצוב (או במקרה של תיבת סימון, הגדרת המאפיין checked כך שיתאים למאפיין checked.

void formStateRestoreCallback(state, mode)

מופעל באחת משתי הנסיבות:

  • כשהדפדפן משחזר את מצב הרכיב (לדוגמה, לאחר ניווט או כשהדפדפן מופעל מחדש). במקרה הזה, הארגומנט mode הוא "restore".
  • כאשר תכונות העזרה לקלט של הדפדפן, כמו מילוי אוטומטי של טפסים, מגדירות ערך. במקרה הזה, הארגומנט mode הוא "autocomplete".

סוג הארגומנט הראשון תלוי באופן הקריאה ל-method setFormValue(). מידע נוסף מופיע במאמר שחזור מצב הטופס.

מתבצע שחזור של מצב הטופס

בנסיבות מסוימות, כמו ניווט חזרה לדף או הפעלה מחדש של הדפדפן, הדפדפן עשוי לנסות לשחזר את הטופס למצב שבו המשתמש השאיר אותו.

של רכיב מותאם אישית המשויך לטופס, המצב המשוחזר מגיע מהערכים שאתה מעביר ל-method setFormValue(). אפשר להפעיל את השיטה באמצעות פרמטר של ערך יחיד, כמו שמוצג בדוגמאות הקודמות, או באמצעות שני פרמטרים:

this.internals_.setFormValue(value, state);

הערך value מייצג את הערך שניתן לשלוח לגבי הבקרה. הפרמטר האופציונלי state הוא ייצוג פנימי של מצב הבקרה, שיכול לכלול נתונים שלא נשלחים לשרת. הפרמטר state מקבל את אותם סוגים כמו הפרמטר value – הוא יכול להיות מחרוזת, אובייקט File או FormData.

הפרמטר state שימושי כאשר אי אפשר לשחזר מצב של אמצעי בקרה על סמך הערך בלבד. לדוגמה, נניח שאתם יוצרים בוחר צבעים עם מספר מצבים: לוח צבעים או גלגל צבעי RGB. הערך שניתן לשלוח הוא הצבע שנבחר בפורמט קנוני, למשל "#7fff00". עם זאת, כדי להחזיר את אמצעי הבקרה למצב ספציפי, חשוב לדעת גם באיזה מצב הוא היה, כך שהstate עשוי להיראות כמו "palette/#7fff00".

this.internals_.setFormValue(this.value_,
    this.mode_ + '/' + this.value_);

תצטרכו לשחזר את המצב של הקוד בהתאם לערך המצב המאוחסן.

formStateRestoreCallback(state, mode) {
  if (mode == 'restore') {
    // expects a state parameter in the form 'controlMode/value'
    [controlMode, value] = state.split('/');
    this.mode_ = controlMode;
    this.value_ = value;
  }
  // Chrome currently doesn't handle autofill for form-associated
  // custom elements. In the autofill case, you might need to handle
  // a raw value.
}

במקרה של פקד פשוט יותר (למשל קלט מספר), הערך כנראה מספיק כדי להחזיר את הבקרה למצב הקודם. אם משמיטים את הערך state בקריאה ל-setFormValue(), הערך מועבר אל formStateRestoreCallback().

formStateRestoreCallback(state, mode) {
  // Simple case, restore the saved value
  this.value_ = state;
}

דוגמה לעבודה

בדוגמה הבאה ריכזנו הרבה מהתכונות של רכיבים מותאמים אישית שמשויכים לטופס. כדי לראות את ה-API בפעולה, מומלץ להפעיל אותו ב-Chrome 77 ואילך.

זיהוי תכונות

אפשר להשתמש בזיהוי תכונות כדי לקבוע אם האירוע formdata והרכיבים המותאמים אישית שמשויכים לטופס זמינים. כרגע אין פוליגונים ליצירת פוליגונים לאף אחת מהתכונות. בשני המקרים, תוכלו לחזור להוספת רכיב טופס מוסתר כדי להפיץ את הערך של הבקרה לטופס. סביר להניח שיהיה קשה או בלתי אפשרי לשפר את רוב התכונות המתקדמות יותר של אלמנטים מותאמים אישית המשויכים לטופס.

if ('FormDataEvent' in window) {
  // formdata event is supported
}

if ('ElementInternals' in window &&
    'setFormValue' in window.ElementInternals.prototype) {
  // Form-associated custom elements are supported
}

סיכום

האירוע formdata והרכיבים המותאמים אישית שמשויכים לטופס מספקים כלים חדשים ליצירת פקדי טפסים בהתאמה אישית.

האירוע formdata לא מעניק לכם יכולות חדשות, אבל הוא מספק ממשק להוספת נתוני הטופס לתהליך השליחה, בלי שתצטרכו ליצור רכיב <input> מוסתר.

ה-API של רכיבים מותאמים אישית המשויך לטופס מספק קבוצה חדשה של יכולות ליצירת פקדי טפסים מותאמים אישית, שפועלים כמו פקדי טפסים מובנים.

תמונה ראשית (Hero) של Oudom Pravat ב-Unbounce.