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

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

ארתור אוונס

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

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

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

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

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

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

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

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

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

  1. אתם מוסיפים לטופס שבו אתם רוצים לבצע אינטראקציה באמצעות event listener של 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>.
  • הרכיב יכול להגדיר ערך שיישלח באופן אוטומטי עם הטופס.
  • הרכיב יכול להגדיר סימון שמציין אם יש לו קלט חוקי או לא. אם אחד מפקדי הטופס מכיל קלט לא חוקי, אי אפשר לשלוח את הטופס.
  • הרכיב יכול לספק קריאות חוזרות לחלקים שונים במחזור החיים של הטופס - למשל, כאשר הטופס מושבת או מתאפס למצב ברירת המחדל שלו.
  • הרכיב תומך בפסאודו-סיווגים הרגילים של CSS עבור פקדי טפסים, כמו :disabled ו-:invalid.

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

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

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

  • מוסיפים מאפיין formAssociated סטטי למחלקה של הרכיב המותאם אישית. כך הדפדפן יתייחס לרכיב כאל פקד טופס.
  • צריך לקרוא לשיטה 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>

הגדרת ערך

השיטה 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);

אימות קלט

אמצעי הבקרה יכול גם להשתתף באימות טפסים על ידי קריאה לשיטה 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, בדיוק כמו אמצעי בקרה מובנה בטופס.

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

רכיב API בהתאמה אישית שמשויך לטופס כולל קבוצה של קריאות חוזרות נוספות במחזור החיים, שייקשרו אל מחזור החיים של הטופס. הקריאות החוזרות (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".

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

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

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

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

this.internals_.setFormValue(value, state);

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

הפרמטר state שימושי כשלא ניתן לשחזר מצב של בקרה על סמך הערך בלבד. לדוגמה, נניח שאתם יוצרים בוחר צבעים עם מספר מצבים: לוח צבעים או גלגל צבעים RGB. הערך של value שניתן לשליחה יהיה הצבע שנבחר בצורה קנונית, כמו "#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 ורכיבים מותאמים אישית שמשויכים לטופס זמינים. בשלב זה לא שוחררו Polyfills לאף אחת מהתכונות. בשני המקרים, תוכל לחזור ולהוסיף רכיב טופס מוסתר כדי להפיץ את הערך של הבקרה אל הטופס. סביר להניח שיהיה קשה או בלתי אפשרי למלא חלק גדול מהתכונות המתקדמות יותר של רכיבים מותאמים אישית המשויכים לטפסים.

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 ב-UnFlood.