My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.
Showing posts with label events. Show all posts
Showing posts with label events. Show all posts

Tuesday, January 10, 2012

Introducing ObjectHandler

since the discussion about a better Function#bind will probably never end, and since a must have dependency would be the ES6 WeakMap which is in any existent shim leak prone due lack of control via ES5+, I have proposed another way to solve the problem.

ObjectHandler IDL Like


/**
* interface ObjectHandler implements EventListener {
* void handleEvent(in Event evt);
* void remitEvent(in Event evt);
* attribute Object events;
* };
*
* @link http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventListener
*/
var ObjectHandler = {
handleEvent: function handleEvent(e) {
var
events = this.events,
type = e.type
;
if (events.hasOwnProperty(type)) {
events[type].call(this, e);
}
},
// it could be called removeEvent too, as you wish
remitEvent: function cancelEvent(e) {
e.currentTarget.removeEventListener(
e.type, this, e.eventPhase === e.CAPTURING_PHASE
);
}
};

Above object is an Abstract implementation. In JavaScript we can borrow methods so whatever "class" would like to implement above behavior should extend the object or borrow those two methods and specify an events property.

Examples

Let's say we have a UI Class which aim is to react to certain events ... let's call it button:

function UIButton(parentNode) {
// add the node
this.el = parentNode.appendChild(
parentNode.ownerDocument.createElement("button")
);
// add quickly events to the node
for (var key in this.events) {
// add the listener ...
// NOTE: nothing to bind, no duplicated entries either
this.el.addEventListener(key, this, false);
}
}

// extends the class
Object.defineProperties(
UIButton.prototype,
{
// implements ObjectHandler
handleEvent: {value: ObjectHandler.handleEvent},
remitEvent: {value: ObjectHandler.remitEvent},
events: {value: {
// only once thanks to remitEvent
click: function (evt) {
alert("only once");
this.remitEvent(evt);
},
// some decoration on actions
mousedown: function (evt) {
this.el.style.border = "1px solid black";
},
mouseup: function (evt) {
this.el.style.border = null;
},
// recicle mouseup behavior
mouseout: function (evt) {
this.events.mouseup.call(this, evt);
}
}}
}
);

// example on window load
this.onload = function () {
new UIButton(document.body);
};

Got it? Events property is simply an object shared through the prototype with properties name that are exactly the equivalent of DOM events name ... easy!

Pros

  • memory safe, nothing to bind, trap as bound, and stuff like this ... all instances will share same logic and bind is not even needed, aka: problem solved
  • events are defined in a single place, easy to find, maintain, extend
  • events are recycled, as it was as example for mouseout, though the instance
  • most UI frameworks need the instance rather than the DOM node, to behave accordingly
  • ObjectHandler can be used same way for "ObjectTarget", those listeners/able JS implementations, in order to uniform an event driven approach
  • no risk to bind twice, no risk to remove the wrong callback, bound or not, it's the instance that makes the deal, not inline functions so it's less error prone
  • performances in therms of both memory footprint and operation per each instance ... once again, no need to bind all methods to the instance itself ... what is in events is basically bound automatically through a single entry point, defined in ObjectHandler, for every single use case


Cons

... no idea, I cannot find any and I just like this solution over the Function#bind improvements since it's both cross platform, whenever addEventListener is present, but it can be recycled for any other no DOM use case as long as the event will have a currentTarget property that points to the current instance ( I will write an example/proposal soon ... OK? )

As Summary

The main problem with proposal is adoptions and I would like to know UI Framework users feedbacks. Ideally this way to handle events could become a de facto standard dropping the redundant and massive usage of Function#bind for cases where the focus should be for the instance and not for the callback.

Sunday, October 25, 2009

Android DOM "Finger" Events


This is the Android based device I am talking about and the installed software is version 1.6, apparently with multi-touch support for more recent devices so mine is not the case.

Disclaimer

The reason I am writing this post is my attempt to reproduce the Android OS interface via browser, hilariously possible with every browser except the original one: the WebKit in Android.
Why would I do that? The mouse has been around for ages, thanks Xerox, but later these years we've seen so many touch screen based devices without common mouse pattern. Since I think both Android, iPhone, and others, have done an excellent work with the way we use the device UI I thought: Why not! It could be a great time to start analysis and piece of libraries to replicate in a cross browser way some device independent event. Well, as I've already said, and as you'll see next, right now it is not possible!

Mouse Events .. ahem, Finger Events

Every mouse related event we know does not make much sense with a touchscreen, at least the tested one. Think about it: a finger points something (aka: mousedown) move somewhere (aka: mousemove) remove the finger itself (aka: mouseup). This is not what truly happens.

Mouse Events Order

  • mouseout, optionally fired when point if before we pointed another element
  • mouseover, optionally fired if the pointed event is different (or no elements have been pointed before)
  • mousemove, always before mousedown, sometimes fired twice if the internal cursor is in position 0,0 - this event is not performed while we move the finger!
  • mousedown
  • mouseup
  • click, which is so far the only event that makes sense
  • DOMActivated, a common WebKit "bug"


Mousemove Events ... Whatever ...

Fired events are device dependent, my device fires these events just in order but whatever we change in the DOM wont be called/rendered until we release. Mousemove is fired once, maximum twice, but never during movement.
In few words the only "mouse event" that makes sense is click, not performed if we scroll.

Forget The Drag And Drop!

Even if the most interesting part of the Android interface is the drag and drop, via WebKit mobile we cannot even try to emulate it just because the only event fired while we move the pointer is scroll.

Scroll ... Whatever ...

This event could be considered just a notify and nothing else. If we do whatever action while we scroll a page nothing will happen until we release the pointed element. Accordingly we cannot use the element to synchronize somehow the rendered page since again, nothing will happen during the scroll. The phone browser is basically frozen during the entire lifecycle of a gesture, or a pointer (via finger) operation.

(window || document) // it does not matter
.addEventListener(
"scroll",
(function(i){
return function scroll(e){
// it's a waste of time
// to perform whatever
// action here
// let's speed up things
// adding delay
if(i)
clearTimeout(i);
i = setTimeout(function(){
i = 0;
onScroll();

}, /* about */ 250);
};
})(0)
)
;

Above snippet represents a delayed onScroll event which aim is to be performed faster without slowing down the page while we are scrolling.
I know this could sound a nonsense but operation will be performed in any cases but never rendered so why should we let our phone compute stuff will never be showed?
Also a scroll could be not perfect, since the mouse wheel won't be there and a pointer has 10 pixels of tolerance so just wait the end of the action, and only at that point perform something.

Resize

Both window or document, no difference in this case, will always perform a resize event. This is the last reliable event we have so far in a G1 device.

Drop Down Menu Not Worth It

Common JavaScript drop down menu usually activated via mouseover and removed on mouseout won't work as expected in common touchscreens while if we implement a "clever" click manager Desktop users will be a bit disoriented 'cause we failed implementing these menus everywhere just because these are strictly device pointer dependent, so not re-adaptable/usable at all.
Moreover, a finger is not a mouse and it's more complicated to click the right menu/link at first shot. We need to rethink a little bit the UI if our aim is to make a website usable, without having 2 versions to maintain, with both desktop and mobile or touchscreen.

As Summary

We have basically 3 events, and few others, such onload, DOMContentLoaded, focus and key related for inputs, but these a part other events won't necessarily act as expected.
We need to bear in mind that mousemove or mouseover/out are quite meaningless since there is no mouse and no sensor able to monitor the finger activity when close to the screen. iPhone implemented a touch event but still, touch does not allow drag and drop so if we would like to emulate the Mobile OS interface via the Mobile Browser, we simply cannot: what a joke, but that's how it is.

Sunday, August 16, 2009

W3 HTML5 Storage - What Works, What Does Not

My last sessionStorage is basically ready to implement storage events but apparently I cannot implement them right now. Why not? Doing some test I just discovered a lot of inconsistency for each browser which supports Web Storage in core, so here I am with a little report and some suggestion.

The Correct Way To Set or Get Items


Every single example I've read so far about Web Storage is not respecting standards defined by the official draft, even if related browsers respect the official API.

// bad sessionStorage usage example
if(sessionStorage.myKey !== null){
// not implementable with old browsers
// it could inherit from a modified Storage prototype
// it could be a false positive with native API
} else {
sessionStorage.myKey = "myValue";
// requires getter/setter, improbable with other browsers
// it could overwrite native properties or methods
// sessionStorage.key = "value"; will *break* the object
};

// W3 standard suggested Storage usage
if(sessionStorage.getItem("myKey") !== null){
// implementable with my standard solution
// it *should* *not* interfere with prototypes
// it *should* never be a false positive
} else {
sessionStorage.setItem("myKey", "myValue");
// it does not require get/set alchemy in other browsers
// it *should* *never* create conflicts with native methods
// or properties
};

The problem here is that if we follow W3 we are "safe enough" while if we use the quirks way to access or set properties, we will never know how the browser will react. An error because we tried to overwrite a native property or method? A silent operation that will break the object? A silent operation that will not break anything but that will make the logic inconsistent?

Which Browser Does Not Break Itself


Apparently, the only browser that implements internally a private dictionary, rather then set or get values from the sessionStorage instance itself, is Firefox. Both Internet Explorer 8 and Safari are able to make a sessionStorage, or a localStorage, useless. My implementation? It does not break the object, Firefox behavior!

// IE8 and Safari break theirself

sessionStorage.key; // native method
sessionStorage.key = "value";
// sessionStorage.key is broken, privileged property
// key with value "value" instead

sessionStorage.setItem("setItem", "break it!");
sessionStorage.setItem;
// the string "break it" rather than the API method
// setItem is not usable anymore in the whole page

As we know that we should never extend Object.prototype, how come every browser and every related example here suggests bad practices and/or allows us to easily break these objects?
Via official W3 API and bug fixes, we would like to be absolutely sure that setItem will never break the object itself and that getItem will always be the native API method.

Empty Key Support


OK, this is not a massive issue cause an empty string as key is something too silly to use.
On the other hand, since a Web Storage should support any kind of key as is for a generic object, where object[""] = 123 is normally saved, I wonder why Firefox browser does not save anything and it does not fire any kind of error as well.

// how to perform a ghost operation in Firefox ...
sessionStorage.setItem("", "empty string");
sessionStorage.getItem("") === null; // true !

I would like to have a unified behavior here as well ... don't you agree?

Storage Events - Nobody Compliant


This is the most interesting part of my analysis about Web Storage implementations, nothing is working as expected.
Firefox and Safari do not fire events at all, or at least I have not been able to do it.
Storage Events should be fired in the document, for gosh knows which reason (I rather prefer listeners in storages or directly in the top context, since everything is about the top window context).
Apparently storage events are two: onstorage, and onstoragecommit.
Both events are not described yet in the official draft so these are how Internet Explorer implemented them, and in an inefficient way.

// Internet Explorer is the only one
// able to fire storage events

document.onstorage = function(e){
alert(e); // undefined, global event instead
};

document.attachEvent("onstorage", function(e){
alert(e); // object Event ... hooorray? NO
e.key; // undefined
e.storageArea; // undefined
e.newValue; // undefined
e.oldValue; // undefined
e.target; // undefined
e.type; // storage, nice one
});

Funny enough, a web storage event in IE fires with clientX and clientY properties, extremely useful for a storage operation, isn't it? It does not matter, the problem is that there are no extra arguments and every expected property is not there.
At least, we have an event that fires in every storageSession present in the page, nested pages (i/frames) so it is usable as listener.
Accordingly, first usage of sessionStorage events will require developers skills to be implemented:

document.attachEvent("onstorage", (function(){

// this closure is necessary to
// create a memory variable
var modified = null;

// above variable is useful to manage
// events
return function(){

// unless we do not check each property
// in the storage, there is no way to understand
// what has been changed here

// we check if the sessionStorage is trying
// to tell us that something has been changed
// get("modified") will be null until it is set
// if we have more than a context (frames)
// we would like to perform the task just once
// this is why modified variable is used
if(modified !== sessionStorage.get("modified")){

// we can decide which action we should perform here
// and we should prevent next event execution
switch(modified = sessionStorage.get("modified")){
case "change userName":
// do some query or something else
// with the userName
sessionStorage.get("userName"); /// etc etc ..
break;

case "otherAction":
// ...
}
}
};

// define something
sessionStorage.setItem("userName", "Andrea Giammarchi");
// nothing is performed in the event

// now we want to perform something
sessionStorage.setItem("action", "change userName");

})());


Above example is just simple and not perfect but it is a hint about curent event management at least in Internet Explorer.
As Douglas Crockford did when He discovered JSON, we could enrich our event notifier protocol using JSON rather than plain strings, it is up to us.

Should I Fix Other Browsers ?


At this point I could decide to fix Firefox and Safari plus add events in my implementation for every other browser.
The problem here is that standards are not definitive and IE is alread messing up these standards.
Being sessionStorage and localStorage completely different objects, a type property in the event with value "storage" does not help at all to understand which one out of two has been changed ... or maybe, since onstoragecommit has never been fired in my tests, this event will be associated exclusively to the localStorage only ... who knows!
If I implement this behavior I gonna slow down events assignment due to addEventListener or attachEvent wrappers.
The alternative is to call, if present, the onstorage event for each document part of the same session. Still, it should be in core and a fake event object could just cause more problems so I am kinda stuck with storage events but I am pretty much happy about the rest cause everything seems to work as expected.

As Summary


There is one thing I am concerned about: is this what HTML5 will mean for us? Do we need to test every single new interface to fix or change their behaviors? Isn't HTML5 purpose to make things more standard and less problematic?
As we can see there are already "gotchas" everywhere and for a single, simple, misinterpreted interface as Storage is.
Examples are different and confused, specially the Safari one uses both setItem, getItem, and direct properties access too.
Behaviors are not the same and there is something missing or different for each browser (I'll test Google Chrome ASAP as well).
Well, HTML5 is good, but we all should try to do not mess it up with quick but not-standard or problematic implementations.

Wednesday, June 10, 2009

ExtJS And The Bloody TreePanel Arrow

First of all I am sorry for choose title, but I kinda lost dunno how many minutes to figure out what was wrong, and it was not me!
Please let me try to explain what was the requirement, and how I had to force ExtJS to act as I want, version 2.2.1 or 3.0RC2, it does not matter.

The Problem ...

The reason you or your company choose ExtJS is most likely because of its Office $uite / Window$ look and feel, closer than many others to a proper Desktop Application, perfect under Adobe Air, and one step forward about stability, performances, and documentation, even with the old enemy: Internet Explorer 6. Fair enough, it's a good library but, there is always a but, at the same time it does not replicate 100% same Window$ behaviors.
As example, try to click Start and Run a prompt command, explorer.
This is the typical Window$ file browser, something we deal with since ages, something still a user habit.

... In Details

How does it work? It is really simple, we have a representation of our files in the left, let's call it Tree, and many details on the right but only if we click a folder. That is the problem, the Jurassic explorer acts differently if we click the [+] rather than the folder/file. The difference is that while we are simply surfing a Tree structure, we do not need to show every single detail, preview, whatever, on the right side until we click, still on the left side, into the folder we need.
As summary, one click on the [+] to open the folder, one to [-] to close it, if we click the folder and it is closed, it will be opened as if we pressed the [+] and the right side will change showing details about that folder. Morevore, if we do a double click, nothing happen, the explorer works with click, and eventually right click. Seems to be truly simple, isn't it? It's NOT, unless I am missing an hidden part of ExtJS documentation because I swear I tried everything.

Ext.tree.TreePanel Hacked To Have More Control Over The Arrow

... where the rrow is the one in ExtJS 2, in version 3 they put a lovely [+]/[-] which reminds even more explorer surfing!!! I am not here to judge ExtJS choices, I am just writing my solution to this problem, specially because tdginnovations asked me, via twitter, to make this problem public, with solution as well.

// good old onload ...
onload = function(){

// simple function to add nodes
// just for this example
function addNodes(root, dummyData){
for(var
i = 0, length = dummyData.length,
cls = {true:"file",false:"folder"},
data, leaf;
i < length; ++i
){
data = dummyData[i];
leaf = root.appendChild(new Ext.tree.TreeNode({
text:data.text,
leaf:!data.items,
cls:cls[!data.items]
}));
if(data.items)
addNodes(leaf, data.items);
};
};

var
// just a sync example
// with a couple of folders
dummyData = [
{text:"Test 1", items:[
{text:"Sub Test 1 1"},
{text:"Sub Test 1 2"}
]},
{text:"Test 2"},
{text:"Test 3", items:[
{text:"Sub Test 3 1"},
{text:"Sub Test 3 2", items:[
{text:"Sub Test 3 2 1"},
{text:"Sub Test 3 2 2"}
]},
{text:"Sub Test 3 3"}
]}
],

// the lovely TreePanel
TreePanel = new Ext.tree.TreePanel({
renderTo:document.body,
border:false,
rootVisible: false,
root:{
expanded: true,
text: "",
draggable: false
},

// global TreePanel events
listeners:{

// Hack #1: The Lovely Arrow ..

// here starts the open hack ...
// when the arrow is clicked,
// no click event is propagated
// but when the node is clicked
// beforeexpandnode is called ...
// to avoid troubles I used a flag
// in order to discard next
// beforeexpandnode event
// without returning false
// but if this is the arrow
// without the click ... so
// the lonely beforeexpandnode ...
beforeexpandnode:function(Node){
// check if the flag is not setted
if(!Node.beforeexpandnode){
// set the flag, prevent
// return false onclick event
Node.beforeexpandnode = true;
// fire the click event
// the one in charge
// to load or show stuff
Node.fireEvent("click", Node);
// block this call
// or we gonna open twice ...
// with possible loops
// (at least in Ext 2.2.1)
return false;
}
},
// here we are ...
// if it was a click in the name/folder/leaf
// this event is fired before beforeexpandnode
// we need to check a couple of things ...
click:function(Node, e){
// if node is expanded
// we do not need to do anything
if(Node.isExpanded()){
console.log("already expanded");
}
// but if node is not expanded
// and the beforeexpandnode flag
// has been setted as true ...
else if(Node.beforeexpandnode) {
// we know that this click
// is from the arrow, and not
// from the folder or name
console.log("expand via arrow/plus or minus");
// we can load stuff async, putting
// a mask to the panel before, removing it
// on load success, add nodes
// and finally expand this Node
// this if should not exists if
// the node is a leaf, cause there
// is no arrow beside
Node.expand();
}
// if node is not expanded but it is
// a folder and beforeexpandnode flag
// has not been setted as true
// we know that the user performed
// a click ...
else if(!Node.isLeaf()) {
// to avoid another click event
// triggered via beforeexpandnode event
// we need to set the flag as true
// so beforeexpandnode will not
// return false calling this event
// again (precedent if)
Node.beforeexpandnode = true;
// at this point we could perform
// a different request
// which could return the entire node
// list plus some other information
// or simply do something else
// rather than just populate the
// Node with sub-nodes
console.log("expand via click");
// in this case, let's expand the node
Node.expand();
}
// none of precedent condition
// was true, interesting ...
// ... it must be just a leaf ;)
else {
// let's do something
// with this leaf
console.log(Node.text);
};
// the main problem with click
// is that it is not possible
// to avoid a double click
// e.stopEvent() ? no way
// it does not do anything with
// dblclick, useless
},
// last part of this little hack ...
// once the node is expanded
// it makes sense to
// set the flag as false
// so next beforeexpandnode event
// will work as expected
expandnode:function(Node){
Node.beforeexpandnode = false;
},

// Hack #2: ... I said:
// DO NOT CLOSE ON DOUBLE CLICK!!!

// at least this hack is even shorter
// but it still requires 2 events
// dblclick, as I said, cannot be
// stopped ... but it does not
// really matter, cause
// beforecollapsenode will be fired
// always before ... so, even
// implementing a proper stopEvent
// how could we avoid closing action
// from beforecollapsenode event
// since this is fired before
// dblclick?

// dblclick has to be a sort
// of filter able to change
// another flag
// beforecollapsenode
// if node is expanded
// we set the flag as false
// why ? ... read next ...
dblclick:function(Node, e){
if(Node.isExpanded())
Node.beforecollapsenode = false;
},

// beforecollapsenode event
// is fired before dblclick
// so this hack is about milliseconds.
// if the flag is not set
// we set it as true and we call a timetout
// Hopefully, the double click will be performed
// before choose timeout.
// If this happens, it means
// that the user double clicked the
// opened node, otherwise
// it means that the user did not
// mean a double click (the arrow
// does NOT fire a dblclick in any case)
// while He/She simpl clicked once in
// the arrow (or more than once to close it)
beforecollapsenode:function(Node){
if(!Node.beforecollapsenode){
Node.beforecollapsenode = true;
setTimeout(function(){
// so here this hack should
// perform its action ...
if(Node.beforecollapsenode)
// closing the node
// if it was not a dblclick
Node.collapse();
}, 20);
return false;
}
},
// to complete the task
// let's reset status
// on node collapsed
collapsenode:function(Node){
Node.beforecollapsenode = false;
}
}
})
;

// it's time to test
addNodes(TreePanel.getRootNode(), dummyData);
// and play with this version
TreePanel.render();

};

That's it, waiting for comments, better examples, or even solution I could not think about!