From: <sab...@ma...> - 2002-06-10 13:38:40
|
Hi all you widget-developers hiding out there... In my work with widgets, I=B4ve developed some simple, yet powerful = techniques I=B4d like to share. The result is highly customizeable widgets without comprimizing the = simplicity of use. As a bi-product you even get faster development, easier mainteinance and = better documentation. (!) It is my hope they will be adopted you, the widget developer! The strategy builds on combining two tricks: 1) Grouping the constants into a static object 2) Grouping the parameters Grouping the constants. By grouping the constants into an object, you make it easy to identify = them. By making the object a property of the widget, you enable the = "constants" to be overridden. Example: MyWidget.defaults =3D { x: 0, y: 0, z: 0, w: 300, h: 200, marginLeft: 7, marginRight: 0, marginTop: 3, marginBottom: 0, borderColor: 'silver', canvasColor: 'black' } I=B4ve choosen to call the contants "defaults", to indicate they are = customizable. If a user wants to permantly change the behavior of a widget, he would = do like this: MyWidget.defaults.x =3D 100 MyWidget.defaults.marginLeft =3D 13 =20 Grouping the parameters Using constants is very well, but changing them is only appropiate for = permanent changes. To allow for individual customization of the instances, you would have = to accept the options as parameters. This is traditionally done like this: function = MyWidget(x,y,z,w,h,marginLeft,marginRight,marginTop,marginBottom,borderCo= lor,canvasColor) { } This approach however, has several problems. For one, the users have to be magicians: var mw =3D MyWidget(100,100,1,400,300,10,null,null,5,'red','white') Looking at the function call, it is not clear which numbers mean what. Secondly, you (usally) have to pass all the parameters. - Even if you = only want to change a few. This fact holds most programmers back from making highly customizable = widgets. Even 20 parameters seems overwhelming. Highly customizable widgets with, say 60 parameters... - Looks like a = joke. Thirdly, when you come up with new parameters, you have to place them = last - in the interest of combatibility, even though the logical place = might be next to similar parameters. The solution is to group the parameters into an object: function MyWidget(options) { } The client would now write: var mw =3D = MyWidget({x:100,y:100,z:1,w:400,h:300,marginLeft:10,marginBottom:5}) Note that looking at the function call itselves is much more = informative. You dont have to worry about getting the order right, and you dont have = to pass all available parameters. Combining the tricks By combining the tricks, you get the powers of both with no more effort. The true treasure, however is achieved by restricting the options to the = constants defined. The default-definition then also documents the options available. Or, in the new light, you=B4d say we now have a set of options which can = be manipulated in two ways: Permanent changes: (should be performed before making instances) MyWidget.defaults.x =3D 100 Customized instances:=20 mw =3D new MyWidget({x:100}) Implementation (example is written to DynApi 2.5.7) function MyWidget(options) { this.DynLayer =3D DynLayer; this.DynLayer(); var o =3D this.calcSettings(options) this.setBgColor(o.borderColor) this.setX(o.x) this.setY(o.y) this.setSize(o.w,o.h) this.setZIndex(o.z) this.mainlyr =3D new DynLayer() this.mainlyr.setBgColor(o.canvasColor) this.mainlyr.setX(o.marginLeft) this.mainlyr.setY(o.marginTop) this.mainlyr.setWidth(o.w-o.marginLeft-o.marginRight) this.mainlyr.setHeight(o.h-o.marginTop-o.marginBottom) this.addChild(this.mainlyr); this.o =3D o } MyWidget.prototype =3D new DynLayer() var p =3D MyWidget.prototype p.calcSettings =3D function(options) { var o =3D new Object() for (var i in MyWidget.defaults) { o[i]=3D((options[i]!=3Dnull)?options[i]:MyWidget.defaults[i]) } return o } MyWidget.defaults =3D { x: 0, y: 0, z: 0, w: 300, h: 200, marginLeft: 7, marginRight: 2, marginTop: 3, marginBottom: 2, borderColor: 'silver', canvasColor: 'black' } =20 As you can see, there=B4s really nothing to it. We have 2 objects: 1) The defaults, defined in the code 2) The options passed to the widget calcSettings() merges the two objects and returns the resulting object. You can see a working examples here:=20 DynApi 2.5.7: = http://d.rosell.dk/cm2.10/edit/clientlib/api/dynapi-2.5.7/coma-examples/M= yWidget.htm DynApi 2.9: = http://d.rosell.dk/cm2.10/edit/clientlib/api/dynapi-2.9/coma-examples/MyW= idget.htm =20 Implementation "deluxe" Beeing in an object-orientated mood, you might feel like subgrouping. In the above example, I=B4d personally prefer this declaration: MyWidget.defaults =3D { x: 0, y: 0, z: 0, w: 300, h: 200, margins: { left: 7, right: 2, top: 3, bottom: 2 }, colors: { border: 'silver', canvas: 'black' } } In this case the above implementation fails its maners. Passing "colors: {border:'yellow'}" would delete the canvas-property. This forces the user to specify all subproperties, and raises a = compatibility-issue: If you decide to add a new color, the users will have to change their = code too. The solution is ofcourse only to overide the subproperties specified. This piece of code does the trick: p.copyObjectProperties =3D function(oFrom,oTo) { for (var i in oFrom) { switch(typeof oFrom[i]) { case 'object': if (!oTo[i]) oTo[i] =3D new Object() this.copyObjectProperties(oFrom[i],oTo[i]) break; case 'array': if (!oTo[i]) oTo[i] =3D new Array() this.copyObjectProperties(oFrom[i],oTo[i]) break; default: oTo[i]=3DoFrom[i] } } } p.calcSettings =3D function(options) { var o =3D new Object() this.copyObjectProperties(MyWidget.defaults,o) this.copyObjectProperties(options,o) return o } The code also works for sub-sub-sub-sub-sub properties... You can see a working examples here:=20 DynApi 2.5.7: = http://d.rosell.dk/cm2.10/edit/clientlib/api/dynapi-2.5.7/coma-examples/M= yWidget2.htm DynApi 2.9: = http://d.rosell.dk/cm2.10/edit/clientlib/api/dynapi-2.9/coma-examples/MyW= idget2.htm Real world conciderations Adobting these techiques could easily lead to total abondon of = traditional constructor parameters, due to the ease of defining = default-values. I personally prefer having some of the parameters in the constructor the = traditional way. A label-widget, e.g:=20 function Label(label, options) { } Having "label" as its own parameter calls for more coding, but seems = more natural in use. If you have many standalone-properties, you might want to save some = lines of coding, like this: function PopUp(x,y,w,h,title,html,options) { var args =3D ['x','y','w','h','title','html'] var o =3D this.calcSettings(options) for (var i in args) eval('if ('+args[i]+'!=3Dnull) = o.'+args[i]+'=3D'+args[i]) .. } PopUp.defaults =3D { x: 0, y: 0, z: 0, w: 300, h: 200, .. } This techinique also has the advantage of defining the = default-definitions the same place as the options, separating constants = and code. A final word I hope you followed me all the way, and feel like implementing the = deluxe-model. If not, just applying the first "trick" (grouping the constants) is = enough to make our widget-world a better place to live in. I thought that maybe the "copyObjectProperties(oFrom,oTo)" could make it = into the dynapi-core? I find it strange that JavaScript lacks a method for hardcopying = objects, and the function occasional handy. Bye for now, and happy widget-writing! Bj=F8rn Rosell |