The Sandbox
The FBJS sand box is a protective layer. Your raw javascript gets passed through some translators over at Facebook and is spit out to the browser. You get to see that translated Javascript (which we'll now refer to as Face Book Java Script FBJS) when you access a part of your application that contains script elements, you'll notice that all of the variables you have declared in your scripting blocks are rewritten to run within the Sandbox.
Code you wrote
<script> var ds = { results: ['apple', 'orange', 'banana', 'peach', 'grape', 'pineapple', 'plum'] }; var tagEntry = new TagEntry(document.getElementById('tagEntry'), ds, 'tags[]', 15); tagEntry.renderTags(); </script> |
Ends up being
<script type="text/javascript"> var a12345678_ds = { results: ['apple', 'orange', 'banana', 'peach', 'grape', 'pineapple', 'plum'] }; var a12345678_tagEntry = new a12345678_TagEntry(a12345678_document.getElementById('tagEntry'), a12345678_ds, 'tags[]', 15); a12345678_tagEntry.renderTags(); </script> |
In a nutshell Facebook is making sure that you are only instantiating objects for which you wrote and you are using variables only you have defined. They don't want you using Javascript to start poking around a person's profile, grabbing elements you aren't allowed to see!
For instance, the TagEntry class listed above is a class I made, and it resides in a script file I include in my application's web page. Using a tool like Firebug, I can check to find that sure enough, all the TagEntry class functions within my included .js file have been been rewritten as "a12345678_TagEntry*".
So essentially they are forcing you into your own namespace. You cannot reach out of your namespace unless they have blessed it.
FBJS
FBJS itself is a set of objects and functions which abstract away the underlying browser your application is running in. This is a nice feature--I repeat--this is a nice feature. Although, perhaps one which you've already figured out on your own by using frameworks such as prototype. Nonetheless, it's a nice feature to have.
What the fine folks at Facebook have done is used Javascript's ability to morph existing classes and objects, via prototyping, just like how javascript frameworks such as prototype do it. For instance, in FBJS the DOM elements have an entirely new set of functions and the ones you are used to from the W3C specification are either crippled or non existent. You can see the differences by exploring this table (reproduced here from the one found on the Facebook Developers Wiki)
So as you can see, porting an existing javascript widget to FBJS would not work so well, lots of manual labour and debugging. In FBJS:
Instead of
var elem = document.getElementById('someDiv'); elem.innerHTML = "<h1> here's some html!</h1>"; |
You write
var elem = document.getElementById('someDiv'); elem.setInnerXHTML("<h1> here's some html!</h1"); |
Essentially properties don't exist, only their setters and getters. It takes some getting used to, and from what I can gleam from FBJS2 they have listened to the bitching and moaning of the Facebook developer community and have made writing FBJS more like writing normal javascript inside a browser. This post is not about FBJS2 though, it's about plain jane FBJS and so we continue...
Firebug
Do you have the Firebug addon installed? If the answer is no, do not pass go, do not collect $200. Seriously, writing any type of rich Javascript widget without the aid of Firefox is an excercise in futility. Not only does it provide some pretty good Javascript debugging options, it also has a network monitoring and a DOM inspector. You can edit both the HTML or CSS of a page on the fly to experiment with changes in real time. Luckily FBJS isolates you from the browser so I only had to debug the code in Firefox with the aid of Firebug. For Internet Explorer I was SOL when it came to debugging the stylesheets, it was all hunt and peck.
The Bad about FBJS
Aside from not really being able to use external libraries like Prototype and jQuery which override a lot of the default classes in Javascript is the fact that Facebook has created a lot of really kickass widgets that you cannot use for your own application. For instance, the <fb:multi-friend-input> renders a really nice little widget that allows you to pick some friends and add them to (what looks like) an input box much like you would see in the To: field of Apple's Mail.app. You, however cannot extend this widget and use it for purposes other than how Facebook indended it to be used. You could not, for example, use it to create a list of fruit, or motorcycles even though if you inspect the widget at runtime you can see that Facebook definitely designed it to be reused for othe purposes.
That's ok, you think, I'll just "procure" the source code for the widget through Firebug and use it on my page as I see fit. Bzzzzzt, wrong! The widget is written to work outside of your application's Sandbox, so it has access to the actual Javascript classes and functions. If you tried to use it within your sandbox, it just plain wouldn't work.
So really the best you can do is find out how Facebook made something look by inspecting the CSS and HTML for the rendered widget, then you go to town creating your own version of the behaviour for the widget in FBJS.
Step 1 - The visuals
Creating an FBJS widget is no different than a Javascript widget. And for Javascript widgets, I always start with a design. Mock up what your widget will look like using only html and css. My widget will mimic the aforementioned <fb:multi-friend-input>, except it will be used for entering and storing "tags". You might be familiar with "tags" if you've ever run your own WordPress blog or use the social bookmarking application del.icio.us. A tag is just a piece of text you want to associate with something such that you can lookup that something later on by specifying it's tag. Here is what I came up with:
That's where things start, with the design, and of course you now have some motivation to add behaviour. Believe me, getting this to look just right across browsers is by far harder than the FBJS part!
Step 2 - The Behaviour
Just to get this out of the way, yeah the u in behaviour is supposed to be there =)
Now, adding the behaviour to your widget is simply a matter of listing out all the things you want the widget to do and then finding out how to do them in FBJS. Here were my list of requirements:
- Allow users to type text into the widget
- When the hit enter, draw the blue box with the x button around the text and allow them to enter more text
- Don't allow them to enter duplicates
- A set of hidden form inputs should track the new tag for form submittal purposes
- Users should be allowed to delete tags
- By clicking the x button on the tag
- By pressing the backspace key
- A set of hidden form inputs should track the deleted tag for form submittal purposes
- A tag should be highlighted when
- hovered over with the mouse
- prior to being deleted via the backspace key
Behaviour 1 - Allow users to type text
For this behaviour what we'll do is have a "non-styled" text box be inserted in the widget and have the textbox be the one that gets focus when someone clicks on the widget, we do this by adding the following code to the widget's constructor function:
this.inputElem = document.createElement('input'); this.inputElem.setType('text'); if (limitTagSize != undefined) this.inputElem.setMaxLength(15); this.inputElem.setClassName('tag_input'); this.inputElem.addEventListener('keydown', _inputElemKeyDownHandler.bind(this)); this.inputElem.addEventListener('keyup', _inputElemKeyUpHandler.bind(this)); this.elem.appendChild(this.inputElem); |
Line 1 we create an input element and on line 2 we set it to be a text element. Line 3 just checks to see if a constructor parameter was passed in which limits a tag's number of characters.
Line 4 sets our style for the text input, which basically gives it no borders it's effectively invisible but the text carat still shows. Line 5-6 set some event handlers for when the user clicks keys in the input and line 7 adds the input to our widget's parent element
To make sure that the input element is always the element which gets focus, we add the following to our widget's parent element:
this.elem.addEventListener('click', function(evnt) { this.inputElem.removeClassName('tag_input_hide'); this.inputElem.focus(); }.bind(this)); |
Basically we remove a css class which effectively makes the input invisible and we set focus to the input.
addEventListener
As you can see from the above examples, addEventListener comes in handy. What it does is it allows you to intercept events on DOM elements and use your own code to handle them. For instance jQuery has a very similar method called bind() which does this and the FBJS method is simply an extended version of the W3C addEventListener.
The great thing about addEventListener is it's browser agnostic. You are passed an Event object which is an FBJS object that works in all browsers, so you don't need a bunch of conditionals to check if certain properties of the event object exist. The FBJS Event object you are passed have the following properties and methods
FBJS Event | ||||
Properties | Methods | |||
type | The type of event this is: click, mouseover, etc.. | preventDefault | Prevents the default event handler from firing on this element, but still propgates the event up the DOM to parent elements | |
target | The target DOM element this event applies to | stopPropogation | Stops the propogation of this event to handlers up the DOM tree | |
keyCode | The key code issued for events like keyup, keydown, keypressed | |||
ctrlKey | Tells us whether the control key was being held | |||
shiftKey | Tells us whether the shift key was being held | |||
metaKey | Tells us whether the meta key (i.e Windows Key, or Apple Key) was being held |
What does "this" reference in an event handler? That really all depends on whether you used bind or not when adding your event handler method to an element. If you did bind your event handler to some object then that object you specified is accessible via "this". However, if you didn't bind the event handler function, then "this" inside the handler simply is an alias for the target property listed above.
Behaviour 2 - Adding a tag to the widget
Adding a piece of text the user entered as a tag (i.e. ) involves the following steps:
- Detect the enter key being pushed
- Ensure the text is not empty or a duplicate of an existing tag
- Add the text of the tag to the hidden input fields which will be submitted with with the form
- Draw the blue boxed text with the x button in the widget
For step 1, we need to dive into the _inputElemKeyDownHandler, in there we have some code like this:
var ENTER = 13; if (evnt.keyCode == ENTER) { evnt.preventDefault(); } |
Steps 2-4 are handled within the _inputElemKeyDownHandler:
var tagText = trim(this.inputElem.getValue()); if (evnt.keyCode == ENTER && tagText != '') { //they hit Enter, add the tag if it's not empty this.addTag(tagText); this.inputElem.setValue(''); this.inputElem.focus(); } |
The bulk of the work is really done inside the "addTag" method of our widget which looks like this:
TagEntry.prototype.addTag = function(tagText) { for (i=0;i<this.tags.length;i++) { if (this.tags[i].value == tagText) { //tag already exists return; } } var tag = new Tag(tagText); this.tags.push(tag); this.hiddenInputsContainer.appendChild(this.createNewTagHiddenInput(tag)); this.elem.insertBefore(tag.elem, this.inputElem); tag.ondelete = removeTag.bind(this); tag.onmouseover = function(evnt) { this.unhighlightTag(); }.bind(this); } |
Lines 2-7 check to see if the incoming tag text is the same as any existing tag we use. If there is a duplicate, we simply return without taking any action. The rest of the method creates a new Tag object (which abstracts away a lot of work to do with tag management), throws that tag onto the widgets existing tags array, adds the hidden input for the tag text so it can be submitted via a form and sets some callback functions which are fired at appropriate times according to the Tag object.
Behaviour 3 - Removing a tag from the widget
In order to make the widget work like the <fb:multi-friend-input> we need to support removal of tags in two ways: 1) when they click the x button of the tag 2) if they hit the backspace button the text input and the text input is empty. Actually, the second one is a bit more complicated because we first highlight the tag immediately to the left of the text input, then their second backspace key press actually does the removal.
Given the above two means of which to remove a tag there is naturally two places where removal takes place, 1) in the x button's click handler 2) in the input element's key down handler.
function removeTag(tagText) { //gotta change the hidden input for this tag value to have it's named prepended with 'removed_' var node = this.hiddenInputsContainer.getFirstChild(); while ( node && node.getTagName().toLowerCase() == 'input' && node.getValue() != tagText) { node = node.getNextSibling(); } if (node && node.getValue() == tagText) { var tag = new Tag(tagText); //add the new hidden input this.hiddenInputsContainer.appendChild(this.createNewTagHiddenInput(tag, true)); //remove the old hidden input this.hiddenInputsContainer.removeChild(node); } var i; var found = false; for (i=0;i<this.tags.length;i++) { if (this.tags[i].value == tagText){ found = true; break; } } if (found) { this.tags.splice(i,1); } } |
Without seeing all of the code this might look a bit strange, but what the above code does is it finds the existing hidden input element which represents the tag being removed, appends a new hidden input that denotes that the tag has been removed (the "true" parameter on creteNewTagHiddenInput specifies that the hidden input to create should be a "deleted" tag) and finally the tag's original hidden input is removed from the DOM tree so it won't be submitted along wiht the form. The rest of the method searches for that same tag in the widget's tags array and splices it out. The removal of the visual elements of a tag are done elsewhere, and is simply a pruning of that element from the DOM tree.
Now for the Backspace method:
_inputElemKeyDownHandler
if (this.tagHighlighted) { evnt.preventDefault(); if (evnt.keyCode == BACKSPACE) { this.tagHighlighted.remove(); //this really calls the removeTag function listed above if (this.tags.length > 0) { var lastTag = this.tags[this.tags.length-1]; lastTag.elem.addClassName('tag_hover'); this.tagHighlighted = lastTag; } else { //unhide the input this.tagHighlighted = null; this.inputElem.removeClassName('tag_input_hide'); } } else if (evnt.keyCode == TAB) { //unhide the input this.unhighlightTag(); } } |
First we check to see if there is a tag highlighted already, if not, we don't remove anything and another chunk of code highlights the tag immediately to the left of the input. But if we currently do have a highlighted tag we check to see if the backspace key has been pushed. If it has then we remove the tag, and highlight the tag which was immediately to the left of the removed tag. Or if there are not tags left, we show the input so they can enter more tags.
Behaviour 4 - Tag highlighting
By describing what has been going on in the previous behaviours, we have shown how tags are highlighted. Basically, the DOM element which represents it gets a new CSS class called tag_hover which will highlight the tag, like so:
lastTag.elem.addClassName('tag_hover'); |
Using the widget
To use the widget we need to have a set of divs which look like this so:
<div class="myclearfix" style="border: 1px solid rgb(132, 150, 186); padding-right: 3px; padding-top:3px;padding-bottom:3px;width: 350px;"> <div id="tagEntry" class="myclearfix tag_entry"></div> </div> |
The clearfix div ensures all the CSS floated elements don't overlap each other and it provides a nice section with a blueish border to mimic the look of a text input. The child div is the div the javascript will attach to, it too is clearfix but also has the "tag_entry" class. In retrospect, I probably could have made the javascript do all these CSS class editions and div structure, but I'll leave that as an exercise to the reader
The Javascript to go along with the above tags is:
<script> var ds = { results: ['apple', 'orange', 'bananna', 'peach', 'grape', 'pineapple', 'plum'] }; var tagEntry = new TagEntry(document.getElementById('tagEntry'), ds, 'tags[]', 15); tagEntry.renderTags(); </script> |
That will render a tag input widget which looks like so
Closing thoughts
Of course the blog post is only shows the tip of the iceberg. When you download the actual source code for this widget you'll see there is a lot more going on.
I enjoyed creating the FBJS widget, I guess I didn't see FBJS as being as big an inconvenience as others profess. Probably because I was "starting fresh" and not trying to port an existing widget. Would I like to use prototype, jQuery or script.acul.us? You bet, but for now I'll have to make due. I would rather the Facebook folks let us use their awesome set of widgets they have put together, instead of us having to re-invent the wheel.