Angular 2.0 Forms Deep Dive
אנגולר 2.0 שיכתב מחדש את הקוד שמטפל בטפסים. בפוסט זה אני רוצה לצלול לתוך הקוד של אנגולר 2.0 ולהסביר את השינויים שנעשו בתחום הטפסים. ממליץ בחום, לפני שאתם קוראים את פוסט זה לקרוא קודם את המאמרים הבאים:
1. המאמר שבאתר הרשמי של אנגולר. (Developer Guides – Forms)
2. את הפרק The Ultimate Guide to Forms in Angular 2 בספר ng-book 2.0, הפרק חינם.
מאמרים אלו ילמדו אתכםאותכם איך עובדים עם Angular Forms, אך לא איך זה עובד "מתחת למכסה המנוע".
מטרת פוסט זה להסביר איך הדברים עובדים.
חלוקה לשכבות:
ציור 1
השכבה העליונה מיצגת את ה- Form Directives. באנגולר 2.0 יש כ- 15 Directives Form שתפקידם לתת לנו גישה לשיכבה של ה- Control’s ששם יש את הלוגיקה האמתית.
אני אתחיל באבא ("הלב") של כל המחלקות והוא AbstractControl.
הלב של התשתית הוא AbstractControl, שכבת ה- Control’s:
אפשר להקביל את AbstractControl ל- NgModelController מגירסת אנגולר 1.0.
ה- AbstractControl אחראי על מספר יכולות: ( הקוד מוצג רק בצורה חלקית למען התמקדות בעיקר )
1. מעקב אחרי שינויים ועידכון ה- class המתאים על ה-DOM. כמו שהיה באנגולר 1.0.
get value(): any {…}
get status(): string {…}
get valid(): boolean {…}
get pristine(): boolean {…}
get dirty(): boolean {…}
get touched(): boolean {…}
get untouched(): boolean {…}
2. בדיקת תקינות ורשימת טעויות:
private _runValidator() {...}
private _runAsyncValidator(emitEvent: boolean): void {...}
get errors(): {[key: string]: any} {...}
get pending(): boolean {...}
setErrors(...): void {...}
getError(errorCode: string, path: string[] = null): any {...}
hasError(errorCode: string, path: string[] = null): boolean {...}
3. מעקב אחרי שינויים של ערכים וסטאטוס, מצריך שימוש בספריה Rx:
get valueChanges(): Observable<any> {...}
get statusChanges(): Observable<any> {...}
4. חיפוש (Find) פקדים אחרים בטופס.
מה אין כאן לעומת ה- NgModelController מגירסת אנגולר 1.0:
1. אין תמיכה ב- formatters ו- parsers, לא צריך. ברגע שעוברים לעבוד עם properties (object.defineproperty) אפשר להכניס את הלוגיקה לתוך ה- get ו-set ולא מערבים בכך את שכבת ה-UI, כלמור יותר קל לביצוע Unit Testing.
2. אין תמיכה ב- debounce. נוצר בעיקר בגלל בעיות ביצועים. אין בעיות ביצועים באנגולר 2.0 אז לא צריך את זה J. עכשיו ברצינות… ע"י שימוש ב- valueChanges והטכנולוגיה Rx ניתן לקבל את אותו אפקט וגם יותר קל לביצוע Unit Testing.
איך המחלקה AbstractControl קשורה לטופס שלי?
ציור 2
השימוש ב-directives Form בתבנית יוצר מופעים של המחלקה AbstractControl. המחלקה זו משמשת "כצינור" בין המחלקה שלי לתבנית, שדות ה-inputs השונים.
אנחנו יכולים לפתח טופס בשני שיטות:
1. Model Driven
2. Template Driven
Model Driven:
בגישה זו אנחנו בונים את עץ הפקדים בעצמנו ע"י קוד ולא ע"י Template. אנחנו יכולים להשתמש במחלקה FormBuilder שתעזור לנו. ראו קוד:
Figure 3
עכשיו אתם בטח מקללים… שיטה זו נועדה לחובבי ה- Unit Testing. פיתוח ב- Model Driven מאפשרת לי הפרדה יותר טובה בין שכבת ה-UI לשיכבת הלוגיקה של הטופס. שימו לב שה- Directives שנמצאים על ה-Template רק עוטפים (הכלה) את המחלקות של השכבה של הפקדים, במילים פשוטות את AbstractControl.
יכולת זו היא חדשה לאנגולר 2.0.
Template Driven:
גישה זו היא הגישה המוכרת מאנגולר 1.0, אך גם בה יש הרבה חידושים. להל"ן ה-directives שעובדים לרשותינו:
כמו שאתם רואים, העץ מתחלק לשתים, קובצת השדה (input) וקובצת הטופס (form). אני אתחיל מהאבא, AbstractControlDirective.
ה- AbstractControlDirective עוטף את AbstractControl ( מכיל אותו ) וחושף property אחד חדש path. לסכום לא עושה הרבה. גם ה- NgControl לא עושה הרבה (abstract class), הוא רק מוסיף לנו את הגישה ל- validators.
קחו אוויר מתחילים לצלול J
NgModel Directive:
בפעם הראשונה שמפעילים את המתודה ngOnChanges בדרקטיב NgModel מתבצעת קריאה למתודה setUpControl (נמצאת בקובץ shared).
מתודה זו מגדירה את הקשר הדו כיווני. להל"ן הקוד של המתודה.
export function setUpControl(control: Control, dir: NgControl): void {
…
control.validator = Validators.compose([control.validator, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator,
dir.asyncValidator]);
dir.valueAccessor.writeValue(control.value);
// view -> model
dir.valueAccessor.registerOnChange(newValue => {
dir.viewToModelUpdate(newValue);
control.updateValue(newValue, {emitModelToViewChange: false});
control.markAsDirty();
});
// model -> view
control.registerOnChange(newValue => dir.valueAccessor.writeValue(newValue));
// touched
dir.valueAccessor.registerOnTouched(() => control.markAsTouched());
}
המתודה הזו מאוד חשובה להבנה. למה היא לא בתוך המחלקה AbstractControlDirective ?
מה זה valueAccessor? הוא הדבק בין ה- DOM ל- Directive. להל"ן הקוד.
@Directive({
selector:
'input:not([type=checkbox])[ngControl],
input:not([type=checkbox])[ngFormControl],
input:not([type=checkbox])[ngModel],
textarea[ngControl],
textarea[ngFormControl],
textarea[ngModel],
[ngDefaultControl]',
host: {'(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()'},
bindings: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
var normalizedValue = isBlank(value) ? " : value;
this._renderer.setElementProperty(this._elementRef, 'value', normalizedValue);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
כמו שאתם רואים, הוא מאזין לשני אירועים blur ו- onChange. יש כמה מחלקות כאלו למקרים שונים של UI.
אם ננסה להשוות את זה לאנגולר 1.0, זה הכלאה של ng-model-option והיכולת להגדיר render ב- ngModelController, ראו פוסט נחמד בנושא.
אז מה היה לנו כאן? <input type="text" [(ngModel)]="name">
כאשר onChange של input נורה מתחיל ה-value של input לעבור את התהליכים הבאים:
1. עידכון הערך ב-Directive (viewModel).
2. ה-Directive יורה את האירוע ngModelChange.
3. ה-control מעדכן את שדה ה- _value.
4. ה- controlמריץ את הבדיקות (validators)
5. ה- controlמעדכן את ה-status
6. ה- controlיורה את האירועים valueChanges ו-statusChanges
7. הטופס חוזר על סעיפים 4-6
8. השדה שלנו (name) מתעדכן בעקבות סעיף 2.
לכיון השני, כלומר שהשדה שלנו משתנה:
1. ngOnChange מופעל על ה-Directive (NgModel).
2. מעדכנים את ה-DOM.
3. ה- controlמריץ את הבדיקות (validators)
4. ה- controlמעדכן את ה-status
5. ה- controlיורה את האירועים valueChanges ו-statusChanges
6. הטופס חוזר על סעיפים 4-6
7. הערך ב-model מעודכן ל-viewModel
טבלת השוואה לאנגולר 1.0
ngModelController |
NgModel |
Control |
My Component |
$viewValue |
viewModel |
|
|
$modelValue |
Model |
value |
|
$formatters |
אין |
אין |
Get property |
$parsers |
אין |
אין |
Set property |
$validators |
validators |
validator |
|
$asyncValidators |
asyncValidators |
asyncValidator |
|
$render |
valueAccessor |
|
|
$rollbackViewValue |
אין |
אין |
|
debounce |
אין |
אין |
|
$error |
|
errors |
|
$pristine |
|
pristine |
|
$dirty |
|
dirty |
|
$valid |
|
valid |
|
$invalid |
|
אין |
|
$touched |
|
touched |
|
$untouched |
|
untouched |
|
$pending |
|
pending |
|
אין |
|
status |
|
אין |
|
valueChanges |
|
אין |
|
statusChanges |
|
שאלה: מה ההבדל בין NgModel ל- NgControl ול- NgFormControl?
תשובה: סתכלו בקוד על ה- selector של כל דרקטיב.
1. כאשר עובדים רק עם NgModel הוא יוצר בעצמו את האוביקט Control. אין לנו גישה לאוביקט זה דרך ה-Template. כלומר אין לנו צורך לעבוד עם כל המידע שיש ב-control בצד ה-template.
2. כאשר עובדים עם NgControl מקבלים בצד ה-template גישה ל-control ואוביקט זה הוא גם NgModel, כלומר NgModel משמש כ-attribute של הדרקטיב NgControl.
3. כאשר עובדים עם NgFormControl מקבלים את שני העולמות. אנחנו יוצרים את המופע של control בקוד וגם מקבלים עליו גישה ב-template. אפשר לומר שזו הכלאה של סעיפים 1 ו-2.
שאלה: מה ההבדל בין NgForm ל- NgFormModel?
תשובה:
שאלה: ExpertAs מה זה?
תשובה: כאשר רוצים referance ל-Form Directive צריך להגדיר משתנה ולהתחל אותו בערך שמוגדר בתוכנה ExprtAs על ה- DirectiveMetadata. ראו טבלה.
How to use in Template |
ExportAs value |
Directive |
<input [(ngModel)] =”name” #c=”ngForm”/> |
ngForm |
NgModel |
<input ngControl =”name” #c=”ngForm”/> |
ngForm |
NgControl |
<input ngFormControl=”name” #c=”ngForm”/> |
ngForm |
NgFormControl |
<form #f=”ngForm”/> |
ngForm |
NgForm |
<form ngFormModel =”myForm” #f=”ngForm”/> |
ngForm |
NgFormModel |
<form ngControlGroup=”myForm” #f=”ngForm”/> |
ngForm |
NgControlGroup |
השורות שצבועות בורוד לא יוצרות control אוביקט אלה מתחברות לאוביקט קיים שנמצא על ה-component שלנו, זה סגנון ה- model Driven. כל השאר יוצרים את ה-control בעצמם ואין לנו גישה עליו מה- component שלנו, זה סגנון ה- Template Driven.
שאלה: מה חסר? מה לא גיליתי לכם? איפה מעדכנים את ה- Classes של ה-DOM? (ng-valid, ng-dirty…)
תשובה: ב- NgControlStatus Directive. לקח לי זמן למצוא את הממזר הקטן J
מהסתכלות על הקוד אנחנו מבינים שאנחנו יכולים לבנות בקלות NgControlBootstrapStatus שבמקום ה- CSS Classes של אנגולר אפשר לשים שמות של מחלקות של Bootstrap, כמו למשל, במקום ng-valid לשנות ל- success.
סיכום:
להיכנס לקוד של אנגולר זה קשה… אין מספיק תיעוד, מסמכים של ארכיטקטורה ולכן חייבים להשקיע המון זמן עד שמוצאים את כל הנקודות החשובות כדי להבין איך הדברים עובדים. אני מקווה שמפתחים שיש להם את הסקרנות לחקור את הדברים, קיבלו כאן בצורה מרוכזת את הדגשים והמקומות החשובים בקוד של אנגולר כדי להבין איך אנגולר טפסים עובד.
אשמח לקבל פידבקים… או תיקונים.
בקורס הקורב שלי על אנגולר אני מכניס פרק חדש על אנגולר 2.0 ואיך מכינים את הקוד של אנגולר 1.0 למעבר לגירסה 2.0.
תאריכים: ב-25,27,31 לינואר ו-3 לפברואר. לפרטים נוספים הירשמו באתר הקורס ng-course.org.
יעקב
ינואר 8, 2016 at 3:42 pm
Is there english translation of this article
?