ארכיון פוסטים מהקטגוריה "JavaScript"

OhBehave – החלת התנהגות על אלמנט סטטי או דינמי בצורה מיידית

יום שישי, 18 בדצמבר, 2009

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

דוגמאות:

  • הפיכת תיבת select לפקד autocomplete
  • השמת אלמנט בתוך מסגרת מורכבת (פינות מעוגלות, שקיפות)
  • החלת אירועי לחיצה על אלמנטים מסוג מסויים
  • השמת תווית צפה מעל פקד טקסט עד ל-focus/שינוי ערכו

פיתרון סטנדרטי יהיה להמתין לעליית ה-dom (פונקציה אשר מצוייה בכל ספריית js אשר מכבדת את עצמה), לרוץ על אלמנטים עם class מסויים, ולהפעיל פונקציית js שתחיל את ההתנהגות.

שימו לב! במידה ומדובר בהצמדת אירועים בלבד כדאי מאוד להשתמש בשיטת ה-Event Delegation

חסרונות השיטה:

  • לפעמים, לוקח מעט זמן עד שה-dom עולה והאלמנט מופיע בגרסתו נטולת ההתנהגות עד לסיום הטעינה
  • כאשר מוסיפים דינמית אלמנטים לעמוד (innerHTML או createElement & appendChild) צריך לדאוג להחיל את ההתנהגות שוב, על כל האלמנטים שדורשים זאת
  • כפיתרון גנרי לאלמנטים דינמיים אפשר להפעיל טיימר על העמוד שיחקור את ה-dom בכל פעם מחדש. זהו תהליך איטי, וב-delay בקריאות ל-interval עלולות להיות קפיצות

דוגמה לעבודה בשיטה זו:

<script type=”text/javascript”>
function applyBehaviors() {
    var elements=$.select(“.behavior”);
    for (var i=0;i<elements.length;i++) applySpecificBehavior(elements[i]);
}

$.domready(function () {
    applyBehaviors();
});

$.ajax("url.html",
    onSuccess:function () {
        applyBehaviors();
    }
);
</script>

(הפונקציות $.select, $.domready, $.ajax הינן פונקציות פיקטיביות המקנות אפשרות לבחור אלמנטים לפי css selector, לבצע פעולה מייד בעליית ה-dom וביצוע בקשת ajax, בהתאמה. ניתן למצוא אותן בכל ספריית javascript המכבדת את עצמה).

הפיתרון הנכסף – OhBehave

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

איך זה עובד?

לכל דפדפן נאלצתי למצוא פיתרון שונה, מאחר וכל אחד מיישם את הפונקציונליות בה רציתי באופן אחר. מתוך כל מנגנון בחרתי להפעיל פונקציה בשם OhBehave.initialize שמקבלת אלמנט ותניע את החלת ההתנהגויות.
התנהגויות האלמנט מוגדרות אף הן ב-class שלו. לדוגמה – oh-behave-WrapWithFrame, oh-behave-Autocomplete (שימו לב לקידומת oh-behave-).
כל התנהגות היא פונקציה המאוחסנת באובייקט OhBehave.behaviors שמקבלת אלמנט עליו היא מחילה התנהגות בודדת.
לא רציתי לכבול לשימוש ב-OhBehave, כך שניתן להגדיר את הפונקציה גם כגלובאלית, ואף ניתן לשנות את קידומת ההתנהגות (ראו בקובץ המצורף).

דוגמה:

OhBehave.behaviors["AlertTheTime"]=function (element) {
    element.onclick=function () {
        alert(new Date());
    };
};

// or -- function AlertTheTime(element) { … }

Firefox ‏ (Gecko Engine) – שימוש ב-XBL

XBL או XML Binding Language היא שפת תגיות מבוססת XML המוגדרת בתקן W3C להחלת התנהגות עשירה לאלמנט. ה-XBL מאפשר להגדיר מאפיינים (כולל getters/setters), אירועים וירייתם, מתודות וקוד CSS/JS שיחול על האלמנט. ה-XBL מוצמד בעזרת css בתצורה:

.oh-behave { -moz-binding:url("oh-behave.xml#oh-behave"); }

ומבנה ה-xml:

<bindings xmlns="http://www.mozilla.org/xbl" xmlns:html="http://www.w3.org/1999/xhtml">
    <binding id="oh-behave">
        <implementation>
            <constructor>OhBehave.initialize(this);</constructor>
        </implementation>
    </binding>
</bindings>

כרגע רק מנוע Gecko תומך ב-XBL, על אף שהוא בתקן.
הצמדתי XBL ל-css class מסויים ומתוכו הפעלתי קוד גנרי המחיל התנהגות.

IE ‏ ‏ (Trident Engine) – שימוש ב-HTC

HTC, או HTML Component הינו יישום של Microsoft להחלת התנהגות עשירה לאלמנט. הוא מאפשר פחות או יותר את מה שה-XBL מאפשר.

Safari/Chrome ‏ (WebKit Engine) – האירוע DOMNodeInserted

מאחר וב-WebKit עוד לא הכניסו את XBL לשימוש (בקוד המקור יש אזכורים אך כנראה האופציה עדיין כבוייה), נאלצתי למצוא פיתרון אחר. האירוע DOMNodeInserted (אירוע המוגדר בתקן) נורה כאשר אלמנט מוכנס לעמוד דינמית (innerHTML / createElement & appendChild). דרך אירוע זה אני מחפש את האלמנטים עליהם יש להחיל את ההתנהגות. חיפוש האלמנטים מתבצע בעזרת querySelectorAll – מתודה של אלמנט או מסמך, המקבלת css selector ומגישה את כל האלמנטים העונים על סלקטור זה, תחת אותו אלמנט.
בעליית העמוד השתמשתי באירוע DOMContentLoaded שרץ מייד בעליית ה-dom. כאן עלולה להיות קפיצה קטנה, אבל המנוע כל-כך מהיר שאני לא חושב שיהיה אפשר להבחין בה.

דוגמה חיה

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

להדגמה של OhBehave

להורדת OhBehave כולל דוגמאות

המתנה אסינכרונית לפעולה ב-JavaScript

יום רביעי, 5 בנובמבר, 2008

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

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

לדוגמה:

  • טעינה בזמן ריצה של קובץ css והשמתו על העמוד לא אומרת שמיידית הוא ישפיע על האלמנטים.
  • קבלת html מתוך xmlhttp והשמתו על העמוד לא אומרת שמיידית כולו רונדר וניתן לגשת לרכיבים בו ללא בעיה
  • טעינת קובץ javascript לעמוד לא אומרת שכל האובייקטים בו מוכנים לשימוש

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

/// $waitUntil
///		waits until a certain function returns true and then executes a code. checks the function periodically
/// parameters
///		check - a function that should return false or true
///		onComplete - a function to execute when the check function returns true
///		delay - time in milliseconds, specifies the time period between each check. default value is 100
///		timeout - time in milliseconds, specifies how long to wait and check the check function before giving up
function $waitUntil(check,onComplete,delay,timeout) {
	// if the check returns true, execute onComplete immediately
	if (check()) {
		onComplete();
		return;
	}

	if (!delay) delay=100;

	var timeoutPointer;
	var intervalPointer=setInterval(function () {
		if (!check()) return; // if check didn't return true, means we need another check in the next interval

		// if the check returned true, means we're done here. clear the interval and the timeout and execute onComplete
		clearInterval(intervalPointer);
		if (timeoutPointer) clearTimeout(timeoutPointer);
		onComplete();
	},delay);
	// if after timeout milliseconds function doesn't return true, abort
	if (timeout) timeoutPointer=setTimeout(function () {
		clearInterval(intervalPointer);
	},timeout);
}

POC:

var globalVariable=0;

setTimeout(function () { globalVariable=1; },2000);

$waitUntil(
	function () {
		console.log("checking globalVariable="+globalVariable);
		return globalVariable==1;
	},
	function () {
		alert("done!");
	}
);

בדוגמה אנו יכולים לראות שיש משתנה בשם globalVariableשמאותחל ל-0, ולאחר 2 שניות ערכו הופך ל-1 ידנית. הפונקציה $waitUntil מקבלת שתי פונקציות. האחת תחזיר true/false לפי בדיקה שתבצע, והשניה תבצע קוד שירוץ רק אחרי שהראשונה החזירה true.

* הפונקציה console.log שייכת ל-firebug/webkit/ie8 ולא נתמכת ב-ie6/7.

דוגמאות לשימושים יותר נפוצים:

ajax("url.htm",function (source) {
	element.innerHTML=source;
	$waitUntil(
		function () { return document.getElementById("some-id-in-source")!=null; },
		function () { /* source is rendered and #some-id-in-source is available */ }
	);
});

loadCss("file.css",function () {
	$waitUntil(
		function () { return element.offsetHeight>0; },
		function () { /* css is applied on this element */ }
	);
});

loadJs("file.js",function () {
	$waitUntil(
		function () { return typeof(SomeTypeInFileJs)!="undefined"; },
		function () { /* SomeTypeInFileJs is ready to use */ }
	);
});

הפונקציות loadCss, ajax ו-loadJs טוענות אסינכרונית קבצים ומודיעות כשהם נטענו. ניתן למצוא מימוש שלהן בכל ספריית JavaScript המכבדת את עצמה.

תוסף Outline לקובצי JavaScript ב-Visual Studio 2005

יום חמישי, 6 במרץ, 2008

מיקרוסופט שכחו את מתכנתי הקליינט סייד הכבדים ב-visual studio ולא הוסיפו את האופציה של collapse ו-expand לפונקציות בקובצי javascript.

מצאתי add-in (אחרי חיפוש קדחתני) חביב ל-visual studio 2005

הורדה – SmartOutline – Javascript Outline for Visual Studio 2005

(לא בדקתי על visual studio 2008, אם זה עובד אשמח לשמוע בתגובות ולעדכן)

משמעות מילת המפתח this ב-javascript

יום רביעי, 9 במאי, 2007

הגדרה – this הוא האובייקט שקרא לפונקציה

מילת המפתח this שמורה לכל scope (כלומר, פונקציה או scope גלובאלי) ובכל scope יש לה ערך אחר. ערך ה-this, לפי ההגדרה בעצם נקבע לפי האובייקט שקרא לפונקציה.

מה זה בעצם scope? כל פונקציה מקבלת מרחב משלה להגדיר בו משתנים ומאפיינים. כשאנחנו רוצים לגשת למשתנה כלשהו אנחנו צריכים לעשות זאת מהמרחב בו הוא הוגדר. ב-javascript, כאשר איננו מגדירים פונקציה, הכל קורה ב-scope הגלובאלי, כלומר ה-window.

קוד

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};
o.method();

הגדרנו אובייקט חדש בשם o והגדרנו לו מאפיין מסוג פונקציה שמבצע alert ל-this.name.
אך מהיכן this מגיע?
בהפעלת הפונקציה, o.method(), נקבל את הערך "moon". התהליך שקורה כאן הוא כזה:

  1. יצרנו משתנה מסוג מחרוזת בעל הערך "moon" ושמנו אותו במאפיין name של האובייקט o.
  2. יצרנו פונקציה עם תוכן מסויים ושמנו אותה כמאפיין ל-o.
  3. הפעלנו את הפונקציה כאשר o הוא זה שקורא לה – הוא זה שמופיע לפני הנקודה.
  4. הפונקציה מופעלת כאשר this הוא בעצם רפרנס ל-o. נקבל o.name שערכו הוא "moon".

חשוב להבין! פונקציה היא משתנה לכל דבר. היא מכילה ביטויים שאנחנו יכולים להפעיל מאוחר יותר. אין באמת שיוך בין הפונקציה שלנו ל-o בכך שהגדרנו אותה כמאפיין של o. השיוך האמיתי יקרה רק בקריאה לפונקציה שאז this יתורגם ל-o.

מכך, אפשר להתחכם קצת:

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};

var x={};
x.name="sun";
x.method=o.method;
x.method();

מה עשינו כאן? יצרנו אובייקט חדש בשם x עם מאפיין name בעל הערך sun.

את הרפרנס לפונקציה שהגדרנו ושמנו ב-o שמנו גם ב-x.method. בהפעלה של x.method לא נקבל, חס וחלילה, את moon, כי אין שום שיוך ממשי ל-o. נקבל את sun כי x הוא זה שקרא לפונקציה, וערכו של this נקבע לפי מי שקרא לפונקציה.

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

function f() { alert(this.name); }

הופכת ל

function f(this) { alert(this.name); }

כלומר:

var o={}; // declaring new object
o.name="moon";
o.method=function (this) {
	alert(this.name);
};
o.method(o);
 
var x={};
x.name="sun";
x.method=o.method;
x.method(x);

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

class Foo {
	string name="foo";
	string GetName() { return this.name; }
}

Foo o=new o();
string s=o.GetName();

בעצם הופך להיות מעין:

class Foo {
	string name="foo";
	string GetName(Foo this) { return this.name; }
}

Foo o=new Foo();
string s=o.GetName(o);

שינוי ערכו של this

אפשר לשנות את ערכו של this, לא בעזרת השמה (לא ניתן להשים ערך ל-this) אלא בעזרת קריאה לפונקציה דרך אובייקט אחר, כפי שביצענו עם x – השמה של הפונקציה כמאפיין של אובייקט וקריאה אליה דרכו.

javascript נותנת לנו כלים לקרוא לפונקציה עם this אחר מבלי לשבור את הראש יותר מדי, בעזרת המתודות call/apply. שתי המתודות דומות: הן מתודות של פונקציה (יושבות ב-Function.prototype) שמקבלות אובייקט וארגומנטים, וקוראות לפונקציה עצמה עם הארגומנטים. השונה הוא דרך שליחת הארגומנטים – call מקבל אותם כארגומנטים רגילים (כמו params של C#) ואילו apply מקבל אותם כמערך.

את הקוד העליון, נשנה כדי שיפעל עם call:

var o={}; // declaring new object
o.name="moon";
o.method=function () {
	alert(this.name);
};

var x={};
x.name="sun";
o.method.call(x); 
מה שיקרה פה, מאחורי הקלעים:
x.temporaryMethod=o.method;
x.temporaryMethod();

האופרטור new ו-this

פונקציה ב-javascript היא לא רק מספר statements להפעלה מאוחר יותר, היא גם מתפקדת כמחלקה. כל פונקציה היא גם מחלקה. ממחלקה אפשר ליצור מופעים.

function Foo(name) {
	this.name=name;
}

הפונקציה הזו, כמו כל הפונקציות, משמשת גם כמחלקה, ומופע שלה אנו יוצרים כך:

var instance=new Foo("bar");
alert(instance.name); // "bar"

מה בעצם קרה כאן מאחורי הקלעים?בעת הפעלת new Foo("bar")

var instance={}; // declaring new object
Foo.call(instance,name); // calling the constructor with the new object as this, and the argument "name"

הפעלת הבנאי תפעיל את הביטוי:

this.name=name; // this is the "instance" object. assign name as the sent argument

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

this ו-prototype

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

ב-javascript מספר מחלקות מוגדרות מראש:
Object
Function
String
Number
Array
Date
Error
RegExp

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

ב-C#3.0 יש פיצ'ר חדש שנקרא extension method, שמרחיב בעצם פונקציונליות של מחלקות מוגדרות כמו string, int וכו'

ב-ruby גם כן הפיצ'ר קיים.
String.prototype.trim=function () {
	return this.replace(/^\s+|\s+$/g,"");
};
var s="    x        ";
alert("<"+s+">");
alert("<"+s.trim()+">");

לאחר הרחבת ה-prototype של המחלקה String במאפיין בשם trim שמכיל פונקציה שמקצצת רווחים מתחילת וסוף המחרוזת, נוכל לגשת מתוך כל מחרוזת למתודה הזו. this בתוך הפונקציה בעצם יכיל את הערך של המחרוזת, כי משתנה המחרוזת הוא זה שקרא לפונקציה.

DOM ו-this

<div id="div" onclick="alert(this.innerHTML);">div</div>

ה-DOM ידע לפרש את קוד ה-HTML לקוד הבא:

div.onclick=function () {
	alert(this.innerHTML);
};

כאן זה כבר נראה לנו מוכר, onclick מוגדר כמאפיין שערכו הוא פונקציה עם שימוש ב-this.

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

div.onclick();

כך, this יקבל רפרנס ל-div.

לסיכום

this הוא עולם ומלואו ב-javascript. הבנה מעמיקה שלו נותנת את האופציה לקצר בקוד ולכתוב בצורה יעילה ומתקדמת.