RSS

Forms in AngularJS

16 אפר

Forms in AngularJS

הסיבה ליצירת השפה JavaScript היתה הרצון ליצר שפה שתוכל להכניס קצת לוגיקה לטפסים. מאז הטכנולוגיה מאוד התקדמה והיום AngularJS מאפשר לנו לכתוב לוגיקה לטפסים בארכיטקטורה של MVC ולבצע את המשימות של Data-Binding  ובדיקת תקינות הקלט בצורה פשוטה מאוד. בפוסט זה אני רוצה לצלול ולהסביר את היכולות המלאות שיש לנו ב-AngularJS בנושא טיפול בטפסים.

Data Binding

בחיבור בין התג input ל- scope מתבצע ע"י ng-model. ה- ng-model הוא data binding דו כיווני, כלומר שהמשתמש משנה את הערך של ה- value של התג input מתבצע עידכון ב-scope וכנ"ל גם ההפך. עד גירסא 1.3 כל לחיצה על המקשים (keydown)  גרמה לעדכון ה-scope. מגרסה 1.3 אפשר להגדיר את האירוע שיגרום לעדכון ע"י ng-model-option. האירוע שמעדכן את התג input זה $watch על השדה המתאים.  ראו תמונה.

clip_image002

ng-model Directive

ה- ng-model מחבר בין שדה ב-scope לתג input, בנוסף הוא מחבר את עצמו ל- form directive. ראו קוד, את DDO של ng-model.

{

    require: ['ngModel', '^?form'],

    controller: NgModelController,

    link: function(scope, element, attr, ctrls) {

      // notify others, especially parent forms

      var modelCtrl = ctrls[0],

          formCtrl = ctrls[1] || nullFormCtrl;

 

      formCtrl.$addControl(modelCtrl);

 

      scope.$on('$destroy', function() {

        formCtrl.$removeControl(modelCtrl);

      });

    }

  }

מהקוד אנחנו מבינים שב- NgModelController נמצאת כל הלוגיקה של החיבור בין scope לתג input. פונקצית ה-link מחברת את ng-model ל- form במידה והוא נמצא.

NgModelController

ל- NgModelController יש מספר משימות:

1. ביצוע data binding בין שדה ב-scope והתג input.

2. ביצוע תקינות קלט ע"פ סוג השדה ו- attributes שמוגדרים על התג של input ראה דוגמא:

<input type="number" name="age" ng-model="user.age"
           
min="0" max="120" required />

3. ניהול המצבים השונים שהתג input  יכול להיות בהם. המצבים הם:

A.     "נקי" ($dirty) או "מלוכלך" ($pristine), כלומר האם השדה שונה ע"י המשתמש.

B.     תקין ($valid) או לא תקין ($invalid) ע"פ הגדרות השדה וה- attributes שיש על התג.

C.     שגיאות ($error), פירוט השגיאות.

 

4.     הוספה או הורדה של class התואם את המצב שהתג נמצא בו.

5.     חשיפה של מתודות ל- directives אחרים שיוכלו להשתמש ב- NgModelController לביצוע פעולות אלו.

 

הארכיטקטורה של NgModelController

clip_image004

העברת המידע מהתג input ל- scope:

ה- ngModelController לא מחובר ישירות לשום תג. כדי שיהיה עידכון של השדה ב- scope צריך להפעיל את המתודה $setViewValue. התג input  מקבל את ngModelController (require: '?ngModel') ומפעיל את המתודה של ה- $setViewValue על כל keydown.

המתודה $setViewValue אחראית על כמה משימות:

1.     עידכון השדה $viewValue

2.     עידכון המצב ל- $dirty את התג input והתג form, כמו כן את ה- classes המתאימים. ראו קוד:

    // change to dirty

    if (this.$pristine) {

      this.$dirty = true;

      this.$pristine = false;

      $animate.removeClass($element, PRISTINE_CLASS);

      $animate.addClass($element, DIRTY_CLASS);

      parentForm.$setDirty();

    }

3.     הפעלת המתודות שרשומות ב- $parsers. מתודות אלו מקבלות את ה- value ומחזירות value חדש. למשל לקבל ערך של כסף עם פסיקים (1,800 ₪ ) ולהחזיר אותו כמספר תקין (1800). למתודות אלו אסור לזרוק שגיאות. ראו קוד:

forEach(this.$parsers, function(fn) {

      value = fn(value);

});

4.     הכנסת הערך האחרון שהתקבל מה- $parsers לתוך ה- $modelValue ועדכון השדה המתאים ב- scope. עכשיו רק נותר להפעיל את כל מי שנרשם ל- $viewChangeListeners. ראו קוד:

if (this.$modelValue !== value) {

  this.$modelValue = value;

  // Update scope field

  ngModelSet($scope, value);

  forEach(this.$viewChangeListeners, function(listener) {

        try {

          listener();

        } catch(e) {

          $exceptionHandler(e);

        }

      });

    }

העברת המידע מ scope לתג input:

ה- ngModelController מבצע $watch על השדה המתאים. כאשר ה- $watch מזהה שהשתנה הערך בשדה מעבירים את הערך החדש דרך כל $formatters ואת הערך האחרון שמים ב- $viewValue ובסוף מעדכנים את ה-UI ע"י הפעלת המתודה $render(). ראו קוד:

$scope.$watch(function ngModelWatch() {

    var value = ngModelGet($scope);

 

    // if scope model value and ngModel value
    // are out of sync

    if (ctrl.$modelValue !== value) {

 

      var formatters = ctrl.$formatters,

          idx = formatters.length;

 

      ctrl.$modelValue = value;

      while(idx–) {

        value = formatters[idx](value);

      }

 

      if (ctrl.$viewValue !== value) {

        ctrl.$viewValue = value;

        ctrl.$render();

      }

    }

    return value;

  });

נקודות חשובות שיש לשים לב אליהן:

1.     אין שינוי מצב ל- $dirty כמו שהיה במתודה $setViewValue. שדה נחשב "מלוכלך" רק אם המשתמש הקליד לתוכו.

2.     המתודה $render() לא עושה כלום !!! מי שמשתמש ב- ngModelController צריך לתת לה את המימוש. ראו את המימוש ש- directive input נותן לה:

ctrl.$render = function() {

    element.val(ctrl.$isEmpty(ctrl.$viewValue) ?
                                
" : ctrl.$viewValue);

  };

3.     הסדר של הפעלת ה- $formatters הפוך מהסדר של הפעלת ה- $parsers.

 

ngModelController Custom Validations

ngModelController מאפשר לנו להוסיף מתודות ל- $parssers ול- $formatters על פי הצורך. המתודות שאני מוסיף יכולות לשנות את הערך ולעשות בדיקת תקינות. דיווח על תקינות הערך מתבצע ע"י המתודה $setValidity. המתודה מקבלת שני ערכים, הראשון validationErrorKey והשני isValid. ראו קוד לדגומא של directive לבדיקת תקינות של מספר integer:

var INTEGER_REGEXP = /^\-?\d*$/;

app.directive('integer', function () {

  return {

    require: 'ngModel',

    link: function (scope, elm, attrs, ctrl) {

        ctrl.$parsers.unshift(function (viewValue) {

              if (INTEGER_REGEXP.test(viewValue)) {

                 // it is valid

                 ctrl.$setValidity('integer', true);

                 return viewValue;

               } else {

    // it is invalid, return undefined (no model update)

                  ctrl.$setValidity('integer', false);

                  return undefined;

               }

           });

         }

      };

  });

 

השימוש ב- $setValidity קובע אם הערך תקין או לא. ראו קוד של מתודה $setValidity.

 

var invalidCount = 0;

 

this.$setValidity = function(validationErrorKey, isValid) {

   // Purposeful use of ! here to cast isValid to boolean    
   // in case it is undefined

   if ($error[validationErrorKey] === !isValid) return;

   

   if (isValid) {

      if ($error[validationErrorKey]) invalidCount–;

      if (!invalidCount) {

        toggleValidCss(true);

        this.$valid = true;

        this.$invalid = false;

     }

   } else {

      toggleValidCss(false);

      this.$invalid = true;

      this.$valid = false;

      invalidCount++;

   }

 

   $error[validationErrorKey] = !isValid;

   toggleValidCss(isValid, validationErrorKey);

 

parentForm.$setValidity(
  validationErrorKey, isValid,
this );

};

דגשים:

המתודה מטפלת בשלוש משימות, ניהול המצב תקין או לא תקין וה-CSS המתאים. ניהול השגיאות וה- CSS המתאים, ועדכון האבא (form) את המצב שלו. שימו לב שה- $error מחזיק ערך הפוך מהמצב הנוכחי, זה די מבלבל.  ראו דוגמא.

 

Input Directive

התג input משמש גם כתג חוקי ב- HTML וגם כ- directive. כמו שראינו קודם ה- ngModelController לא יכול לעבוד לבד. ה- input directive מגדיר את האירועים שיפעילו את $setViewValue() , $render() ואת בדיקות התקינות שלו. בהסתכלות על הקוד של אנגולר אנחנו רואים שיש לכל type את הקוד שלו. ראו קוד:

var inputType = {

    'text': textInputType,

    'number': numberInputType,

    'url': urlInputType,

    'email': emailInputType,

    'radio': radioInputType,

    'checkbox': checkboxInputType,

 

    'hidden': noop,

    'button': noop,

    'submit': noop,

    'reset': noop,

    'file': noop

};

var inputDirective = function($browser, $sniffer) {

  return {

    restrict: 'E',

    require : '?ngModel',

    link: function(scope, element, attr, ctrl) {

      if (ctrl) {

        (inputType[lowercase(attr.type)] || inputType.text)
          (scope,element,attr,ctrl,$sniffer,$browser);

      }

    }

  };

};

מהקוד אנחנו מבינים שהקוד האמיתי נמצא ב- textInputType ( בהנחה שאנחנו מגדירים את ה- type="text" ) בהסתכלות על הקוד של המתודה textInputType אנחנו רואים שהיא מחולקת לשלושה חלקים עיקריים:

1.     ביצוע בדיקות תקינות, pattern, minLength ו- maxlength.

2.     הגדרת הפונקציה $render() ל- ngModelController כמו שכתבתי קודם.

3.     ההזנה לאירועים של keydown, paste, cut, change ו- input. כל האירועים אלו גורמים לעדכון השדה המתאים של ה- scope.

הקוד של המתודה טיפה ארוך וקצת נראה "מפחיד" אך אלו שלושת הדברים שהוא עושה.

המתודות של ה- types האחרים עושות אותו דבר רק עם הגדרות של בדיקות תקינות אחרות. למשל numberInputType מגדיר בנוסף לבדיקות שיש ב- text את הבדיקות של min,max ו- number.

 

HTML Validation vs. AngularJS Validation

יש הרבה בילבול בין תגים שהם חלק מהתקן של 5 HTML לבין directives של AngularJS. באיור אתם יכולים לראות את כל התגים החוקים ב- 5 HTML.

clip_image006

 

כמו שאתם רואים בציור יש כפילות בין 5 HTML לאנגולר. אנגולר דורס את ההגדרות של 5 HTML ונותן להם את המימוש שלו. הסיבה לעשות את זה היא, כדי לקבל תמיכה בכל הדפדפנים (ראו תמיכת דפדפנים של אנגולר) ולא רק באלו שתומכים ב- 5 HTML. במבט יותר מדויק בקוד של אנגולר אנחנו רואים שלחלק מהתגים ו- attributes הוא דורס אותם ע"י directives וחלק הוא משתמש בהם כ- attributes רגילים שהוא קורא את הערכים שלהם.

להלן רשימת ה- directives שיש לאנגולר בתחום הטפסים:

Requires

Directive

E

form

E

Input

E

textarea

E

Select

E

Option

A

required

A

ngModel

A

ngOptions

A

ngChange

A

ngRequired

A

ngValue

 

 

 

מהרשימה הנ"ל אנחנו מבינים שכל השאר הם attributes רגילים שאנגולר משתמש בהם כדי להבין מה אתם רוצים:

Ø  Max & Min

Ø  Maxlength & Minlength

Ø  Pattern

ה- Attribute של require כן מקבל directive, ראו קוד:

var requiredDirective = function() {

  return {

    require: '?ngModel',

    link: function(scope, elm, attr, ctrl) {

      if (!ctrl) return;

      attr.required = true; // force truthy in case we are on non input element

 

      var validator = function(value) {

        if (attr.required && ctrl.$isEmpty(value)) {

          ctrl.$setValidity('required', false);

          return;

        } else {

          ctrl.$setValidity('required', true);

          return value;

        }

      };

 

      ctrl.$formatters.push(validator);

      ctrl.$parsers.unshift(validator);

 

      attr.$observe('required', function() {

        validator(ctrl.$viewValue);

      });

    }

  };

};

ע"פ הקוד אנחנו רואים ש- require directive מקבל משמעות עם יש את ng-model.

על מנת למנוע התנגשויות בין אנגולר ל- 5 HTML בנושא בדיקות התקינות אנחנו שמים על התג form את novalidate וכך הדפדפן לא מבצע את הבדיקות אלא רק אנגולר עושה בדיקות תקינות.

 

Form Directive

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

FormController

המצבים שאותם מנהל ה- FormController לטופס הם:

Default Value

State

attrs.name || attrs.ngForm

$name

False

$dirty

True

$pristine

True

$valid

False

$invalid

 

המתודות שאותם חושף ה- FormController הם:

תיאור

מתודות

הוספה של תג input

$addControl

הורדה של תג input

$removeControl

מנהל את המצבים של $valid, $invalid ו- $error. בנוסף מעדכן את ה- class המתאים בהתאם למצב.

$setValidity

מנהל את המצבים $dirty ו- $pristine.

$setDirty

מנהל את המצבים $dirty ו- $pristine.

$setPristine

 

בהסתכלות בקוד של ה- FormController אנחנו רואים שיש תמיכה ב- form מקונן.

 

סיכום

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

 

מאמר זה הוא חלק מהקורס Angular Deep Dive. אתם מוזמנים לבוא לקורס שיפתח ב-14.5.2014. מלאו את הפרטים ונחזור אליכם בהקדם.

clip_image008

מודעות פרסומת
 
2 תגובות

פורסם ע"י ב- אפריל 16, 2014 ב- AngularJS

 

2 תגובות ל-“Forms in AngularJS

  1. חן נעם

    מאי 24, 2014 at 8:09 pm

    הי אייל,
    הסבר מצויין ומפורט מאוד.
    תודה,
    חן

     

כתיבת תגובה

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

הלוגו של WordPress.com

אתה מגיב באמצעות חשבון WordPress.com שלך. לצאת מהמערכת / לשנות )

תמונת Twitter

אתה מגיב באמצעות חשבון Twitter שלך. לצאת מהמערכת / לשנות )

תמונת Facebook

אתה מגיב באמצעות חשבון Facebook שלך. לצאת מהמערכת / לשנות )

תמונת גוגל פלוס

אתה מגיב באמצעות חשבון Google+ שלך. לצאת מהמערכת / לשנות )

מתחבר ל-%s

 
%d בלוגרים אהבו את זה: