Andrew Sumin, November 4, 2007
Up-to-date web application is an interaction between distributed (calendar, tree) and specific (i.e. needed only once) components.
At first, I’ll consider the standard component – calendar. It’s very simple and any JavaScript developer is able to program it quickly.
But the problem is that managers want another design and server-programmers want to have a possibility to send data to a server in several variants. And a developer should try to meet the wishes of them both.
This picture already shows that there exist a lot of realization variants and in reality there is much more. If a developer wants to meet the wishes of both programmers and managers, he shouldn’t obligatory change the code of the calendar, which has already been programmed.
CallBacks is an object that has only two methods: add and dispatch. The first one allows to add event listener to the event and the second – to initialize this event.
CallBacks.add(event’s name, link to listener function, object to listen to the event on);
CallBacks.dispatch(event’s name, object, object)
Take a unique object’s identifier in add function (if there’s no such a key - generate it). Key is generated on the basis of an identifier and object’s name. And a listener function, or functions, is assigned to this key.
When performing a function dispatch, again you generate a key, take functions that have been assigned to this key, and perform them. The transferred event’s objects serve as an inbound parameter for these functions.
CallBacks.add = function (type, listener, object) {
var key = this.getId(object) + type;
if (typeof(this.listeners[key]) == 'undefined') {
this.listeners[key] = [];
}
this.listeners[key].push(listener);
};
CallBacks.dispatch = function (type, object, event) {
var key = this.getId(object) + type;
if (typeof(this.listeners[key]) == 'undefined') {
return;
}
this.execListeners(this.listeners[key], event);
};
In my programs a separate component is a separate object, which has a link to HTML node it was created from. This approach helps me to wrap the programmed calendar in different wrappers.
When changed, a calendar initializes an event on HTML object it was created from and transmits new date as an event object.
CallBacks.dispatch('change', this.element /* link to HTML node */, newDate);
Now, to react to the changes of the calendar, a wrapper should only have a link to the HTML node it was created from.
CallBacks.add('change', function () {…}, calendar /* link to HTML node */);
To open a calendar clicking the date or a button “Calendar”, you should act backwards.
CallBacks.add('open', this.open, this.element /* link to HTML node */);
CallBacks.dispatch('open', calendar /* link to HTML node */); // без объекта событие
If you had one calendar, you could execute just on if operators. But in this interface it turns out to be extremely complicated. Interconnections:
The behavior of this interface has already changed for several times and will still keep on changing. Here we have the calendar that has already been considered, but now it has a modified interface, as well as facility to inform the surrounding objects about its condition.
To exchange events with adjacent components, an object that sees all these components, is to be found. Usually my complicated interfaces have a root object – namespace. You can add and dispatch events to it.
CallBacks.dispatch('some-event', Price /* Root object of the interface */, event);
CallBacks.add('some-event', function () {…}, Price /* Root object of the interface */);
In this case we’re not satisfied just with the simple interaction between the components via “root” JS object. We should limit event scopes of these two blocks. Otherwise the generated events of one block will be reflected on both.
I use two following methods to delimit the scope:
In the first instance, a component being initialized adds the line to all its events, which serves as a parameter to it. Objects-listeners also get this line and listen to the events, which names have been generated with the help of the line.
CallBacks.dispatch('some-event' + key, …);
CallBacks.add('some-event' + key, …);
This method is effective in case interacting components are scattered all over the page and it’s hard to find an HTML node that joins them.
In the second instance, an HTML node that joins the required components is highlighted and the events are dispatched to it (though it can be a JS object).
CallBacks.dispatch('some-event' , HTMLNode, event);
CallBacks.add('some-event' , function(){…}, HTMLNode);
When dealing with common components (for example, calendar), I can’t change names and objects, the calendar adds events to and listens events from. Everywhere it’s used, that is one and the same object and one and the same event’s name. I think it’s better to listen from and dispatch events to the HTML node it was initialized on.
I use Proxy Objects to install such components in interface. On the one hand, they know the behavior of the component they are proxying (calendar), on the other – the context the component is to be installed in.
Proxy Object listens all the events that happen around and are intended for it, then makes them understandable for it and vise a versa.
function proxyChangeCalendar(event){
CallBacks.dispatch('change-calendar', Price, event);
}
CallBacks.add('change', proxyChangeCalendar, calendar);
This allows to install calendar in any interfaces easily.
If you use asynchronous initialization of the components, you’ll have difficulties in synchronizing initial states. If the state of a component depends on other component’s state (including initial state), you should ensure the possibility of getting this state.
If component A has initialized and dispatched an event with information about its initial state and component B didn’t have time to add a handler, that will lead to error in behavior.
I found two solutions of this problem:
The first method implies the objects’ possibility to initialize events flags. If a component dispatches event flag, CallbBacks object not just executes handlers, but also stores in memory the event it was dispatched with.
When a component wants to add a listener of the event and this event is a flag, and a CallBack object has the status of this event, it executes the transmitted function listener, giving the status saved as a parameter.
You can create a separate method of event generation for event flag. To indicate the function of a listener you don’t even need to change anything.
CallBacks.dispatchFlag(…);
CallBacks.add(…);
This method has a certain drawback: if an object listens to the state of several objects of the same type, it can’t be used directly. Homogeneous objects will generate events with similar key (id of an object an event and event’s name are dispatched to) and that will lead to conflicts.
The main point of the second method is to allow components to listen to the events, in answer to which they’ll generate events with their own state. If component A has already initialized and generated event with its state, component B, during initialization, generates the event, component A is being listening to and will send its state once more.
If component B has initialized before, component A won’t have time to begin listening the event which makes it send its state. So nothing will happen. And component B will already be listening to an event generated by component A while initialization.
Probably, methods that seem to be complicated at first allowed me to transfer the components from project to project quite easily, to “unbound” the components (deletion of the component doesn’t bring to inoperativeness of the whole code ) and not to be afraid of any modernizations and modifications.