5

I have a class for mouse events. I'm using dojo b/c I like its OO approach

dojo.declare("MouseObject", null, {
  constructor: function(){},
  onclick : function(){...},
  _onClick : function(){...}
});

_onClick() listens for window generated mouse press/release events and determines if a click has occurred. If it has, onClick() is called. onClick() carries out functionality common to all possible clicks, so it needs to be called every time the user clicks.

sometimes the user may want to extend the functionality of onClick() Is there anyway to incorporate the original functionality without copy pasting it? Two ways I can think of, neither of which I like, are

dojo.declare("MouseObjectChild", [MouseObject], {
  constructor: function(){},
  onclick : function(){this.inherited(arguments);...}
});

which has the drawback that I have to keep creating new classes I don't really need, and two adding an intermediate function

dojo.declare("MouseObject", null, {
      constructor: function(){},
      onclick : function(){this._onClick()...}, //child onClick
      _onClick : function(){...}, //parent onClick
      __onClick : function(){...} //listener
    });

but this does not look like good code


wrt the bounty, I want expert advice on how best to address the program-user interaction. If the program provides a crucial function, such as draw a circle on the canvas, then how does the user best interact with that. What if the user wants to draw a triangle behind the circle? A square in front of the circle? Then the program would have to provide pre and post methods like so:

beforeDraw();
draw();
afterDraw();

Is this considered good design? Should I instead put function pointers in an array and call them in order?

hugomg
  • 68,213
  • 24
  • 160
  • 246
puk
  • 16,318
  • 29
  • 119
  • 199

6 Answers6

1

If someone just want to run some code when onclick() is called, he can just connect to it. Note that you also can connect to function calls not only events with dojo.connect().

dojo.connect(obj, 'onclick', function(){
  // Some extra code that runs when obj.onclick() is called
});

If he want to create a new class and extend the feature calling this.inherited(arguments); is the best way of doing it.

David Raab
  • 4,433
  • 22
  • 40
  • This is in html canvas, I do not believe connect works on it. Also, I don't want to create a new mouse class for every new shape drawn. – puk May 30 '11 at 16:44
  • You didn't say anything about HTML Canvas. So far i have not yet used canvas. Do you use dojox.gfx for abstraction and looked there how to connect to events? In your example you showed a normal Javascript class, if this is the case you always can connect to it with dojo.connect, whatever your class represents. – David Raab May 31 '11 at 08:35
  • This is not exactly what I was looking for, although it might carry out what I want done. – puk Jun 01 '11 at 01:15
1

Why not create your own simple event model?

// store our callbacks
var hooks = {};

// add a callback
var hook = function(name,fn){
  if(typeof hooks[name] === 'undefined'){
    hooks[name] = [];
  }
  hooks[name].push(fn);
  return true;
};

// remove a callback
var unhook = function(name,fn){
  if(typeof hooks[name] === 'undefined'){ return false }
  var i, u = hooks[name].length;
  for(i=0;i<u;i++){
    if(hooks[name][i] === fn){
      hooks[name].splice(i,1);
      return true;
    }
  }
};

// trigger a callback
var callHook = function(name, data){
   if(typeof hooks[name] === 'undefined'){ return false }
   var i, u = hooks[name].length;
   for(i=0;i<u;i++){
     hooks[name][i](data);
   }
};

Then later on (example passes the context):

hook('beforeDraw',function(ctx){ 
  console.log('I am called prior to drawing');
});

hook('afterDraw',function(ctx){
  console.log('I am called after drawing');
});

var drawCircle = function(){
  callHook('beforeDraw', ctx);
  // drawing code here
  callHook('afterDraw', ctx);
};

Alternatively, if you wanted to pass scope to the callbacks you could change the callHook method to accept a scope argument, and then use call or apply and pass in this or some other object:

var callHook = function(name, scope, data){
   if(typeof hooks[name] === 'undefined'){ return false }
   var i, u = hooks[name].length;
   for(i=0;i<u;i++){
     hooks[name][i].call(scope, data);
   }
};

// later
var drawCircle = function(){
   callHook('beforeDraw', this, ctx);
   // or
   callHook('beforeDraw', someObject, ctx);
};

Depending on how & where you define the event model functions you can scope the functionality to specific instances of an object, every instance of an object, globally, and so on depending on how you want to assign, change and trigger those callbacks.

Daniel Mendel
  • 9,862
  • 1
  • 24
  • 37
  • I considered that option, however, I think it's easier to just hard code the three functions for the user, especially since only those three will ever be needed. – puk Oct 31 '11 at 20:38
  • 1
    I guess it depends on if you want the user to be able to stack an array of functions for each of those or not. If it's really that specific a use case then you're probably right, hard coding the three function calls in as parameter arguments that are exposed to the user, then they can stack their code in those functions manually. – Daniel Mendel Nov 01 '11 at 13:25
1

You have several options.
1. The Promise pattern is gaining popularity (although there are competing specs): http://wiki.commonjs.org/wiki/Promises/B. jQuery has an implementation close to B and it's well documented. It has the most options, it's robust, but not easy to wrap your head around at first.

  1. AOP-style programming allows you to fiddle with before and after advices. However, for that to work you need to stop putting everything in anonymous functions and decide whether you apply advices after instantiation or plan for their use by baking the capability into the class. This takes more planning in code structure but offers a ton of utility.

for example:

var MYAPP = {
  onClick: function () {
    //do something...
  }
};

dojo.declare("MouseObject", null, {
  constructor: function(){},
  onclick : function(){...},
  _onClick : function(){
    return MYAPP.onClick.apply(this, arguments);
  }
});

Since my class calls MYAPP every time (yes there's a scoping charge for this), I can manipulate the function hanging off MYAPP and anyone using it will benefit.

MYAPP.after("onClick", function () {...}); //lots of ways to write .after, just google some AOP js or use a library

The other side:

var MYAPP = {
  onClick: function () {
    //do something...
  }
};

dojo.declare("MouseObject", null, {
  constructor: function(){},
  onclick : function(){...},
  _onClick : MYAPP.onClick
});

Now I can't change MYAPP and see the changes in MouseObject or its instances because the function has passed by reference to MouseObject. I can only change the instances with AOP (or all future instances by changing the prototype on the class).

Something like:

var m = new MouseObject();
m.after("onClick", function () {...}); //you need to add .after to your class or use an apply pattern from library code

It just depends on how you think it would be used: certain instances, all future instances (until turned off), or everything.

  1. An "event queue" was suggested. This is more like an Active Object Pattern where you use an array of objects and a queue manager to manage tasks. This is the easiest to implement but has several drawbacks where the queue can get "jammed up" and no longer processes. I wrote a library for this type of management called proto-q (http://code.google.com/p/proto-q/), based on an iPhone app I did. It worked great for me, but it's not for everyone's application.
AutoSponge
  • 1,444
  • 10
  • 7
  • btw, Dojo also has promises and actually had them for a long time before jQuery. Also Dojo already has "after" builtin - it's called dojo.connect and is the foundation of the event system. – hugomg Nov 02 '11 at 13:43
  • I'd never use Dojo, but that's good information. You should probably use those methods for this application. Hopefully I just clarified some reasons for using the different techniques. – AutoSponge Nov 02 '11 at 15:00
1

As suggested by the "should I put functions in their own array and call them in order" and "build your own event system" ideas, you might want to have a look at the dojox.lang.aspect library that comes bundled in the extended standard library. It should be basically a more advanced version of the "dojo.connect" solution.

dojo.require('dojox.lang.aspect');
var aop = dojox.lang.aspect;

function log(msg){ return function(){
    console.log(msg);
};}

var obj = {
   draw : log('draw circle');
};

obj.foo();
//prints
//  draw circle

aop.advise(obj, 'draw', [
    {before: log('draw triangle')},
    {after: log('draw a square')},
    {after: log('done!')}
]);

obj.foo();
//prints
//  draw a triangle
//  draw a circle
//  draw a square
//  done!
hugomg
  • 68,213
  • 24
  • 160
  • 246
0

__fn gets messier and messier. That, my friend, is the road to hell.

It is symptomatic of using the Magic Pushbutton approach. You should consider moving out to an actual inheritance based class model using many of the OO js libraries around. Then you would be able to call $base.fn() or $super.fn()

Asti
  • 12,447
  • 29
  • 38
  • Dojo's model is already inheritance based if you didn't notice the `this.inherited` part in the OP's question – hugomg Oct 29 '11 at 12:36
  • OO works for groups of classes but not for individual objects. I could in theory create a new object for each class, but that would be a tremendous waste of time and energy. – puk Oct 31 '11 at 20:41
  • I use this approach in MVVM using knockout.js With true inheritance, you basically can inherit views. For example, in a list-view view-model, my base class implements functions like searching, filtering and grouping, while in the derived class, I merely specify what the groups are, what filtering predicate to use, etc. It's a paradigm shift, but it gets easier once you get over the first hump. – Asti Nov 01 '11 at 15:31
  • Although, unless the object defines the behaviour for an entire part, it's useless. But like you said, this isn't good if you're targeting individual objects. – Asti Nov 01 '11 at 15:38
0

If I understand your question, then you want to modify functionality for specific cases. If this is the case, and you don't want any overhead, then I'd rely on overriding methods upon instantiation.

Override Model

Your Class:

dojo.declare("some.path.DrawingThing", [dijit._Widget], {
    draw: function () {
        this.inherited(arguments); // Call super class methods
        // override this method to do specialized things that're specific 
        // to an instance.
    }
});

Client Instantiation:

var drawer = new some.path.DrawingThing({
    draw: function () {
        beforeDraw();
        // Access the pre-existing functionality in a somewhat dirty way.
        this.constructor.prototype.draw.apply(this, arguments);
        afterDraw();
    }
});

If you follow this model there's less code, but it's not as clean. The other option is to write a real API, see below.

Event Model

If you want a true API, then I'd provide events:

// Dojo 1.6 style declaration.
dojo.declare("some.path.DrawingThing", [dijit._Widget], {
    onBeforeDraw: function(){
        // Hook into this method to do something before draw
    },
    onDraw: function(){
        // Hook into this method to do something after draw
    },

    draw: function () {
        // This method is actually called when the draw even fires
        this.onBeforeDraw();
        this.inherited(arguments); // Call super class methods
        this.onDraw();
    }
});
fncomp
  • 6,040
  • 3
  • 33
  • 42
  • I don't see how this relates to what I want – puk Nov 01 '11 at 22:32
  • If I get you right, then perhaps this more case specific example will help. I also figured I'd add a cleaner solution since you want to use this in a bunch of places. – fncomp Nov 01 '11 at 23:26
  • yes you have the right idea. However, the 'Client Instantiation:' example runs the risk that the user must remember to include the original `draw()` function. Imagine if `draw()` is 50 lines of code. that is a lot of cut and pasting – puk Nov 02 '11 at 00:05
  • the 'Event Model' version is essentially what I am using. I'll just stick with this latter approach. Thanks – puk Nov 02 '11 at 00:07