JavaScript Programming

From Elvanör's Technical Wiki
Revision as of 13:57, 27 October 2015 by Elvanor (talk | contribs) (→‎CORS)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The best documentation on JavaScript available on the Internet is the one on the Mozilla development center. All the other sources are just not worthy and full of errors.

Syntax and Concepts

  • Using the var keyword means that the declared variable is local to the function (and thus is discarded once the function exits). To bind a variable to the global scope (window), just declare the variable without using var.
  • There are no constants in JS. The const keyword is a Mozilla extension (supported by Opera); this does not allow class constants though. The best for creating a class constant is just to declare a MyClass.MY_CONSTANT property. But the fact that it's a constant is not actually enforced (eg, it is only a convention).
  • The for (property in obj) construct allows you to loop over the properties of a JS object (which is always a Hash). Note that the property variable gets assigned the key name, if you want to access the actual value, write obj[property] in the loop. Also, don't forget that it will loop over all the properties, including the methods! This statement should then only be used when dealing with plain data holding objects.

Scope

  • The equivalent of bind() in Prototype:
function() { myObject.pollElement.apply(myObject); }
  • Or, with an argument:
function(event) { myObject.eventCallback.call(myObject, event); }

CSS effects

  • Note that JS events can never trigger CSS effects. For instance, even if you fire the mouseover JS event manually for a DOM element that has a custom hover CSS style, it won't actually use that style. This is because JS events and CSS events are separate. Both of these events are actually generated by the internal events of the browser (I guess those must come from the underlying windowing system like Qt / GTK etc).

Single Threaded Model

  • Due to the single thread model of JavaScript, running some JavaScript code usually blocks all rendering on the browser until the code finished running. A simple way to force an update is to set a timeout (with a value of 0) on a function, and call that function via setTimeout. This will force the browser to update rendering before going on with the next JavaScript function.
  • Note that a long running code can even prevent delayed function executions to actually take place. For instance suppose you set a timeout of 100ms on a function, and at the end that function will again call itself via a timeout of 100ms. Then you call some intensive JavaScript code running for 1000ms. When the code finishes to run the timedout function will run but only once! It won't be called 10 times as expected during the computation of the intense JS code.
  • On Firefox, loading an external script then immediately reloading a next one (via the element onload event) seems to prevent graphical updates. Again, a workaround is to use window.setTimeout().

Running a function when (if) the DOM is ready

  • This is an infernal problem to solve cross-browser. Some remarks:
    • Most modern browsers have the DOMContentLoaded event. However, if you listen to this event too late (eg, it has already fired), you're stuck. So you have to consider document.readyState.
    • On Internet Explorer 9 (which has DOMContentLoaded), it is too early to launch your function if document.readyState == "interactive". Some methods such as document.getElementsByClassName() won't work. However, if document.readyState == "interactive", DOMContentLoaded has not yet fired on IE 9, so you can listen to that event.
    • Firefox 5.0 (and most probably all other browsers) do not have the previously mentionned IE 9 behavior. Eg, a document.readyState of "interactive" can happen after DOMContentLoaded has fired.
    • Firefox 3.5 (and earlier) don't have document.readyState at all. However you can simulate it by listening on DOMContentLoaded yourself and setting a variable somewhere that represents document.readyState.

Interactions with the browser or server

General

  • You can't access the HTTP headers of the host page in JavaScript. You can access headers of XMLHttp requests though.

Ajax

  • The Same Origin Policy (SOP) prevents the browser from receiving results (from an XMLHttp request for example) that came from another host. This means that you can use "normal" Ajax requests only if they are sent to the same host as the origin page.
  • There are multiple ways to work around this problem though. You can use dynamic scripts (eg, create a script element on the page, that will point to the target host). The target host will create this script file. You can use iframes.
  • Note: although in theory SOP should only prevent the browser from reading the results of a request, in practice it seems the browser does not even send the request.
  • Warning: Opera refuses to read external scripts coming from a different port on a different domain. All other major browsers are more permissive and allow this.

CORS

  • With the use of CORS, you can use an XMLHttp request and are not forced to use a call to a JS script. Be warned that the use of CORS can require some options set on the XMLHttp objet, and also on the response headers. For instance withCredentials should be set to true if you want cookies to be sent to the server using the normal cookie mechanism.
  • Chrome refuses to work with servers resolving to 127.0.0.1 with CORS (maybe this happen only with https). You can start with --disable-web-security to deactivate this, in development.

JSON

  • Be careful when outputting text that needs to be parsed as a JavaScript string, but in fact contains JSON data. You will need in that case to double escape each backslash, as the first JS parsing (by the interpreter) will remove this backslash and the special character it contained. For example:
var myJSONString = '{ "data" : "Some text.\nAgain some text."}';
var data = myJSONString.evalJSON(); // This won't work as there is in fact a newline in the string, whereas it should be encoded data.
var myJSONString = '{ "data" : "Some text.\\nAgain some text."}';
var data = myJSONString.evalJSON(); // This works fine.
  • You can send a true Number (Float) with JSON, which will be parsed and allocated as a Number. For example, encodeAsJSON() in Grails produces Numbers from Float instances.

Cookies

  • You can read, write and delete cookies in JavaScript. Regarding security restrictions, the policy is the same as if the cookie was set via an HTTP header.
  • Be careful, however, that some browsers won't set cookies for file:// protocol. This is the case of Chrome for instance.

Locale

  • In JavaScript, you can only read the browser or system language, and not the normal language mechanism that for example Firefox uses (via Accept HTTP headers). In addition, reading the locale in JavaScript does not seem to be a standard W3C feature (navigator.language or navigator.userLanguage in IE).

Tracking clicks

  • If you want to track clicks (and thus make a server call each time a click is performed on the browser), there is an issue with links (anchor HTML elements). This is because the browser will process the default action (following the link) faster / before handling the HTTP call that handles the link.
  • Possible solutions:
    • set a timeout on the links, and on the callback function set location.href to the link href attribute. This is very risky, as sometimes clicked links do have an href but should not follow it.
    • set a timeout and generate a new click event programmatically. This does not work well since you have to dispatch the event on a target; if this target is not the link itself (it may be a span inside the link), the default action of following the link won't take place.
    • listen on the "mousedown" event and not on the "click" event. This apparently gives enough time to the browser to record the tracking calls.

Objects

Document

  • You can obtain the character set (encoding) of a document in JavaScript via document.characterSet.
  • elementFromPosition() can be a very useful method to find the top-most element at a given coordinates. However, it seems buggy on Chrome:

Images

  • If you preload images, note that once an image is preloaded, you must attach it to the document somehow. Else it seems Firefox can somehow 'forget' it, and it will be fetched again, thus you lose the benefits of preloading.
  • Sample code for preloading (this uses Prototype):
function preloadNormalImages(url_array)
{
	url_array.each(function(url)
	{
		var normal_image = new Image();
		normal_image.onload = function() { $(image_preload).setAttribute("src", url); };
		normal_image.src = url;
	});
}
  • You can obtain in JS the width and height of an Image. Just create a new Image via new Image(), set the source. Then when the onload event is fired, you can obtain the width and height directly as property of the image.
  • new Image() is equivalent to document.createElement("img");
  • On Internet Explorer, if the image is already on the cache, the load event won't fire *if you set the image source before you register the event*. In order to avoid that, just register the event first. The event will then fire as soon as you set the source of the image.
  • Also note that on IE, when the event fires in this way, the code of the event is executed instantly. The behavior seems to be different from Firefox when the current running script finishes its execution before the event code runs.
  • On Firefox (and IE too I think) you can check if an image was correctly loaded was the naturalWidth property. If it is equal to 0, the image was not found / could not be loaded.

Arrays

  • There is a sort() method that takes a comparison function. Note that sortBy (Prototype) is usually easier, since it does not take a comparison function but only a direct "weight" function.
  • Be careful that many methods from the W3C DOM API return a NodeList and not an array, which can be problematic in many cases. You can (and should) easily convert from one to the other.

Events

General Notes

  • When submit() is called on a form programmatically, it won't be caught by an event set on the form.
  • Listening to key presses events needs to be done on the window object, not the document. Eg, use:
window.addEventListener("keydown", this.doSomething, true); // works
window.document.addEventListener("keydown", this.doSomething, true); // won't work

Note that in Prototype it seems OK to do document.observe("keydown").

  • Within an event, in Firefox (W3C API actually), you can test if the alt key, control key or such is currently pressed (active), via the property:
event.ctrlKey // boolean
  • The mouseout event on the document body is triggered everytime you enter a descendant element of the body (so any element actually). This is because the mouse leaves the body to enter a new element. This would also apply on a div that has descendant elements, so be careful with that. Many JS libraries implement a custom event to allow you to trigger a callback when the mouse actually leaves an element and its descendant as displayed on the screen.
  • The W3C DOM Level 3 Events API introduces for this reason the mouseleave event. It is similar to mouseout but won't have the behavior described just before.

Event Workflow

  • In standard W3C event handling, there are multiple phases. First the target of the event is determined by the browser. Then there is a capturing phase when the event goes down the node hierarchy, and a bubbling phase when it goes up.
  • This W3C reference explains the event model very well.
  • You cannot at all change the event target, unfortunately. This means that an element that appears over another one (like an overlay) blocks the underlying element from receiving any events, even if the overlay is transparent. There is no clean workaround over that - you can just recode manually a basic event handling module with the original coordinates of the event.
  • For a mousemove event for instance, only a single event is dispatched (not one for every element on the hierarchy). However, with the capturing and bubbling phases, this event will actually be sent to any listeners in the hierarchy, so it generally works as expected.

Removing event listeners

  • Unfortunately, there is no standard way to obtain the list of event listeners on a given DOM element. You also cannot remove an event listener if you didn't save explicitely the arguments when registering it (especially the callback). This is can really be problematic in some situations.
  • However, note that most JS frameworks (such as jQuery) actually save all this internally and have an API to later clear the event listeners / list all event listeners. So if the event was registered using a framework, generally you can remove it.

Focus / Blur

  • These events happen on form elements (input HTML elements with type = text or file for instance). focus is triggered when an element gains focus, blur is triggered when an element loses focus.
  • These elements do not bubble up (and I am not sure if capturing them is possible too, since their target can only be the form element).
  • There is no way to prevent the default action for a focus / blur event: these events are triggered once the elements actually have gained / lost focus. Note that they happen as soon as the mouse is clicked down (of course it is a separate event from mousedown).

Style

  • You cannot seem to use !important when changing a style property via JavaScript. This does not work on Firefox, but you can use the standard setProperty() W3C call (and put "important" as the third parameter). Note that if you use this call, the third argument must always be present (set it to null usually).

JavaScript Frameworks & Libraries

Standard Library (and crossbrowser methods)

  • Be careful about the replace() method of strings; it will only replace one occurrence at most. Use the gsub() method in Prototype.
  • toFixed() is called on a Number and produces a String.

Third party frameworks

  • Prototype is a fantastic JavaScript framework. It is so good that it's almost impossible to program in JavaScript without it.
  • Scriptaculous is a add-on to Prototype. It provides visual effects, drag and drop support, and more.
  • ExtJS is an advanced JS framework for managing page layouts, etc. It can be used as a good foundation to build a web application similar to a desktop one.

Rich Text Editor

  • FCKeditor: a rich text editor that can be integrated into any web page. Integration is really easy, at least for a PHP environment.
    • Warning: due to a strange encoding of the source files on the package, it seems some UTF BOM characters are present. This causes some weird character output when using PHP integration (at least, untested with other environments). Remove these strange characters from the PHP integration files (fckeditor.php and all).
    • By default, FCKeditor converts the latin characters directly to HTML entities (eg, é is converted to é). To disable that, set IncludeLatinEntities to false in the configuration (the file fckconfig.js).
FCKConfig.ProcessHTMLEntities = true;
FCKConfig.IncludeLatinEntities = false;
FCKConfig.IncludeGreekEntities = false;
    • Unfortunately, there seems to be no option to preserve the whole formatting of the source if you edit some content in source mode. For example, the newlines present are automatically removed. There seems to be options to automatically format the source but it will never format it exactly as you want...

Image viewer

  • Lightbox is not very flexible and buggy (for instance turning the animation off resulted in a bug on the latest version).
  • A core Prototype extension, Modal.Window, looks very interesting for this purpose.
  • FancyBox, based on jQuery, looks very good too.

Calendar Widget

  • There are a lots of calendar widgets for JS. The ideal calendar library should be:
    • available under an open-source licence;
    • light (under 50Kb if possible, ideally less than 20Kb), and fast;
    • skinnable;
    • offers nice features such as the display of more than one month;
    • and based on Prototype since this is what I mainly use.
  • Below is a list of some interesting libraries I have found.

NoGray Calendar

  • Currently the best all-around that I have found.
  • Advantages:
    • open-source, very well documented;
    • easily skinnable;
    • lots of options.
  • Disadvantages:
    • built on mootools;
    • somewhat big (100K if you count mootools).
  • When passing date objects to the API methods (especially when creating the calendar object), it's better to use new Date().fromString("yesterday") than directly passing the string, as it is not always able to correctly parse it, or it takes a wrong date as a reference.
  • Be careful not to have custom CSS properties set on tables, as it may conflict with the styles used by the calendar. The font also should be Arial for the calendar. The size can be adjusted based on the container (nice idea), you need at least something like 180px though.
  • Apparently the initial page of the calendar is based on the start date or the selected date. Setting the selected date just for that is one of the default of this calendar, however you can set via JS the underlying text field (so that it's empty at first).
  • The syntax used to specify the string pattern for the underlying text field is the same as PHP, so here is the documentation.

Improved jQuery Datepicker

  • Not yet available for production, but the look is nice and the fixed suggestions feature is really, really a good idea.

DateJS

  • Not really a calendar, but the concept is interesting.

YUI calendar

  • Not tested, should work though and supports multiple monthes.

Tools and Techniques

Profiling / optimization

  • The easiest way to obtain timing data is to use the following code:

var startTime = new Date().getTime();

... 

var elapsedTime = new Date().getTime() - startTime;

Debugging in production

  • It is absolutely necessary on a large scale web application to trap JS errors. This can be done via the window.onerror listener.
  • Firefox throws an exception if a page is reloaded or an URL is clicked while it is currently loading a script. So the window.onerror listener should be turned off as soon as we receive the beforeunload event.
  • On Chrome, window.error does not give any information if the error happened on a script in a cross-domain host. However, manually adding try / catch blocks give more information.

Obtaining the size of the viewport or whole page

  • This is a very complex topic. There are lots of different dimensions:
    • the size of the window (with or without scrollbars);
    • the size of the document. We call document here the whole canvas area of the browser (usually this is the same as the body, but if the body has margins this is no longer true);
    • the size of the body (can be very different to the size of the document due to margins or a low height).
  • document.documentElement.scrollWidth/Height gives the size of the document, without scrollbars. This works on Firefox 3.6.x. On Chromium, document.documentElement.scrollHeight only gives the height of the body element (so this can be less than window.innerHeight if the body is short).
  • document.documentElement.clientWidth/Height gives the size of the viewport, without scrollbars. This seems to work on Firefox and Chromium all the time.
  • window.innerWidth/Height gives the size of the viewport with scrollbars. This does not exist on Internet Explorer.
  • getBoundingClientRect() works relative to the start of the document, not the viewport. So basically document.body.getBoundingClientRect().top + window.pageYOffset is an invariant, not just document.body.getBoundingClientRect().top.

Obtaining the size of the document

  • Math.max(window.innerHeight, document.documentElement.scrollHeight);

Body height higher than viewport, no horizontal scrollbar

  • document.documentElement.scrollHeight will be more than window.innerHeight (and document.documentElement.clientHeight). To obtain the height of the page it is sufficient to read document.documentElement.scrollHeight.

Body height higher than viewport, horizontal scrollbar

  • Not tested.

Body height smaller than viewport, no horizontal scrollbar

  • To obtain the height of the page it is sufficient to read window.innerHeight.

Body height smaller than viewport, horizontal scrollbar

  • On Firefox, you have to read document.documentElement.scrollHeight. On Chromium you read document.documentElement.clientHeight.

Hints and Tips

  • document.write() should *never* be used. It is an extremely dangerous method. Instead rely on Prototype which has much safer methods.
  • Deleting options in a select element (in a form):
document.my_form.my_select.options[index] = null;

Note that this syntax probably also works for other arrays of objects (untested yet, though).

  • Outputting quickly (for debugging) an array:
alert(my_array);

will display all the elements of the array, separated by commas.

  • Clickable link, calling some JS code:
<a href="page_no_JS.html" onclick="javascript: doSomething(); return false;"> ?

This works very well if you have two versions of your site. This will hide the JavaScript code in the status bar in Firefox, which is much better.

  • There is NO default arguments for JavaScript functions. You can however easily "simulate" default arguments by testing if a given argument is undefined, and setting it to a default value in that case (Prototype uses this technique).
  • In a class, or object related code, you must use the this keyword explicitly all the time. Even when calling methods.
  • To create static methods, properties and constants for a class, don't use Object.extend(); this would return an object (which cause problems in Opera if you don't allocate the object to a variable), and with Prototype Object.extend() is not necessary anymore. Just declare the methods directly on the class:
ShopItem.createNewItem = function(referenceItemId)
{
	// doSomethingHere();	
};

Storing data in a CSS file and retrieving it via JavaScript

  • This can be done by using the quotes property on all browsers except WebKit, and font-family for WebKit.
  • The quotes property seems to cause silent fails in some cases on Internet Explorer 7. Thus using font-family for IE 7 seems to be safer.

JSON Encoding

  • The double quote character causes problems. The best is to encode it in HTML.

Old information

  • This is possible but tricky to achieve in a cross-browser way (while still having a valid CSS file). The basic idea is to find a CSS (2.1) property that allows arbitrary string content. These include:
    • font-family: impossible to use though as Opera 10.0 returns the computed value of the font, not the literal (this is not actually against current specifications).
    • voice-family: Opera and WebKit are unable to retrieve this via JavaScript.
    • content, quotes, counter-increment and counter-reset: these would be excellent candidates, but WebKit unfortunately does not allow retrieving the value (up to Safari version 4). Note that however the underlying CSS property is supported though...
  • Note that IE allows retrieving via the currentStyle property of a DOM element *any* CSS value (even non standard ones, if present on a CSS rule). This is stupid but here it would help us.
  • Another technique would be to use the data: scheme in an URI. Unfortunately, this is again not possible as Firefox does not retrieve the literal value correctly if it is not actually base64 encoded. I am not sure if others encoding are possible; the only times when I managed to retrieve the value was with a correct base64 content.
  • So the only current cross-browser technique is to use an URI with a fake URL. The best property is "list-style-image" (better than "background-image", since it is less intrusive). The URL must be fake but in a special way (again, tricky) for the following reasons:
    • A complete literal (like VERTICAL_FLEX) will return http://www.currentserver.com/VERTICAL_FLEX on Firefox.
    • A correct URL like http://www.google.com/image.png?data=VERTICAL_FLEX works, but will issue an spurious HTTP request. Note this is the case even if the element is hidden or has no lists; as soon as you call getComputeStyle(), the call is made (at least on Firefox).
  • So you should use a false domain, something like:
http://xxxvirtualyyy/VERTICAL_FLEX

This returns the correct value on all browsers. I also think it does never trigger an HTTP request. For me it also does not produce errors or warnings in any browser (although WebKit / Safari should be double checked). Note that there is no HTTP request made but there is probably a DNS request, unfortunately (if xxxvirtualyyy happens to return a valid IP, the HTTP request would be made I guess).

Future improvements

  • It would be much better to use counter-reset for this purpose. This could be possible as soon as Safari supports retrieving the counter-reset (or the quotes one). So WebKit is the main blocker right now (see this reference).
  • Several different properties could be used, but this is not ideal (data has then to be duplicated).