├── test ├── test.html └── compose.js ├── package.js ├── package.json ├── compose.js └── README.md /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | var miniExcludes = { 2 | "compose/README.md": 1, 3 | "compose/package": 1 4 | }, 5 | amdExcludes = { 6 | }, 7 | isJsRe = /\.js$/, 8 | isTestRe = /\/test\//; 9 | 10 | var profile = { 11 | resourceTags: { 12 | test: function(filename, mid){ 13 | return isTestRe.test(filename); 14 | }, 15 | 16 | miniExclude: function(filename, mid){ 17 | return isTestRe.test(filename) || mid in miniExcludes; 18 | }, 19 | 20 | amd: function(filename, mid){ 21 | return isJsRe.test(filename) && !(mid in amdExcludes); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compose", 3 | "version": "0.1.2", 4 | "author": "Kris Zyp", 5 | "description": "Fast and light object composition based on mixins and traits", 6 | "licenses": [ 7 | { 8 | "type": "AFLv2.1", 9 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L43" 10 | }, 11 | { 12 | "type": "BSD", 13 | "url": "http://trac.dojotoolkit.org/browser/dojo/trunk/LICENSE#L13" 14 | } 15 | ], 16 | "repository": { 17 | "type":"git", 18 | "url":"http://github.com/kriszyp/compose" 19 | }, 20 | "dependencies": { 21 | "patr": "0.3.0" 22 | }, 23 | "directories": { 24 | "lib": "." 25 | }, 26 | "main": "./compose", 27 | "mappings":{ 28 | "patr": "http://github.com/kriszyp/patr/zipball/v0.2.5" 29 | }, 30 | "icon": "http://icons.iconarchive.com/icons/kawsone/teneo/64/BiiBall-icon.png", 31 | "dojoBuild": "package.js" 32 | } 33 | -------------------------------------------------------------------------------- /compose.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ComposeJS, object composition for JavaScript, featuring 3 | * JavaScript-style prototype inheritance and composition, multiple inheritance, 4 | * mixin and traits-inspired conflict resolution and composition 5 | */ 6 | (function(define){ 7 | "use strict"; 8 | define([], function(){ 9 | // function for creating instances from a prototype 10 | function Create(){ 11 | } 12 | var delegate = Object.create ? 13 | function(proto){ 14 | return Object.create(typeof proto == "function" ? proto.prototype : proto || Object.prototype); 15 | } : 16 | function(proto){ 17 | Create.prototype = typeof proto == "function" ? proto.prototype : proto; 18 | var instance = new Create(); 19 | Create.prototype = null; 20 | return instance; 21 | }; 22 | function validArg(arg){ 23 | if(!arg){ 24 | throw new Error("Compose arguments must be functions or objects"); 25 | } 26 | return arg; 27 | } 28 | // this does the work of combining mixins/prototypes 29 | function mixin(instance, args, i){ 30 | // use prototype inheritance for first arg 31 | var value, argsLength = args.length; 32 | for(; i < argsLength; i++){ 33 | var arg = args[i]; 34 | if(typeof arg == "function"){ 35 | // the arg is a function, use the prototype for the properties 36 | var prototype = arg.prototype; 37 | for(var key in prototype){ 38 | value = prototype[key]; 39 | var own = prototype.hasOwnProperty(key); 40 | if(typeof value == "function" && key in instance && value !== instance[key]){ 41 | var existing = instance[key]; 42 | if(value == required){ 43 | // it is a required value, and we have satisfied it 44 | value = existing; 45 | } 46 | else if(!own){ 47 | // if it is own property, it is considered an explicit override 48 | // TODO: make faster calls on this, perhaps passing indices and caching 49 | if(isInMethodChain(value, key, getBases([].slice.call(args, 0, i), true))){ 50 | // this value is in the existing method's override chain, we can use the existing method 51 | value = existing; 52 | }else if(!isInMethodChain(existing, key, getBases([arg], true))){ 53 | // the existing method is not in the current override chain, so we are left with a conflict 54 | console.error("Conflicted method " + key + ", final composer must explicitly override with correct method."); 55 | } 56 | } 57 | } 58 | if(value && value.install && own && !isInMethodChain(existing, key, getBases([arg], true))){ 59 | // apply modifier 60 | value.install.call(instance, key); 61 | }else{ 62 | instance[key] = value; 63 | } 64 | } 65 | }else{ 66 | // it is an object, copy properties, looking for modifiers 67 | for(var key in validArg(arg)){ 68 | var value = arg[key]; 69 | if(typeof value == "function"){ 70 | if(value.install){ 71 | // apply modifier 72 | value.install.call(instance, key); 73 | continue; 74 | } 75 | if(key in instance){ 76 | if(value == required){ 77 | // required requirement met 78 | continue; 79 | } 80 | } 81 | } 82 | // add it to the instance 83 | instance[key] = value; 84 | } 85 | } 86 | } 87 | return instance; 88 | } 89 | // allow for override (by es5 module) 90 | Compose._setMixin = function(newMixin){ 91 | mixin = newMixin; 92 | }; 93 | function isInMethodChain(method, name, prototypes){ 94 | // searches for a method in the given prototype hierarchy 95 | for(var i = 0; i < prototypes.length;i++){ 96 | var prototype = prototypes[i]; 97 | if(prototype[name] == method){ 98 | // found it 99 | return true; 100 | } 101 | } 102 | } 103 | // Decorator branding 104 | function Decorator(install, direct){ 105 | function Decorator(){ 106 | if(direct){ 107 | return direct.apply(this, arguments); 108 | } 109 | throw new Error("Decorator not applied"); 110 | } 111 | Decorator.install = install; 112 | return Decorator; 113 | } 114 | Compose.Decorator = Decorator; 115 | // aspect applier 116 | function aspect(handler){ 117 | return function(advice){ 118 | return Decorator(function install(key){ 119 | var baseMethod = this[key]; 120 | (advice = this[key] = baseMethod ? handler(this, baseMethod, advice) : advice).install = install; 121 | }, advice); 122 | }; 123 | }; 124 | // around advice, useful for calling super methods too 125 | Compose.around = aspect(function(target, base, advice){ 126 | return advice.call(target, base); 127 | }); 128 | Compose.before = aspect(function(target, base, advice){ 129 | return function(){ 130 | var results = advice.apply(this, arguments); 131 | if(results !== stop){ 132 | return base.apply(this, results || arguments); 133 | } 134 | }; 135 | }); 136 | var stop = Compose.stop = {}; 137 | var undefined; 138 | Compose.after = aspect(function(target, base, advice){ 139 | return function(){ 140 | var results = base.apply(this, arguments); 141 | var adviceResults = advice.apply(this, arguments); 142 | return adviceResults === undefined ? results : adviceResults; 143 | }; 144 | }); 145 | 146 | // rename Decorator for calling super methods 147 | Compose.from = function(trait, fromKey){ 148 | if(fromKey){ 149 | return (typeof trait == "function" ? trait.prototype : trait)[fromKey]; 150 | } 151 | return Decorator(function(key){ 152 | if(!(this[key] = (typeof trait == "string" ? this[trait] : 153 | (typeof trait == "function" ? trait.prototype : trait)[fromKey || key]))){ 154 | throw new Error("Source method " + fromKey + " was not available to be renamed to " + key); 155 | } 156 | }); 157 | }; 158 | 159 | // Composes an instance 160 | Compose.create = function(base){ 161 | // create the instance 162 | var instance = mixin(delegate(base), arguments, 1); 163 | var argsLength = arguments.length; 164 | // for go through the arguments and call the constructors (with no args) 165 | for(var i = 0; i < argsLength; i++){ 166 | var arg = arguments[i]; 167 | if(typeof arg == "function"){ 168 | instance = arg.call(instance) || instance; 169 | } 170 | } 171 | return instance; 172 | } 173 | // The required function, just throws an error if not overriden 174 | function required(){ 175 | throw new Error("This method is required and no implementation has been provided"); 176 | }; 177 | Compose.required = required; 178 | // get the value of |this| for direct function calls for this mode (strict in ES5) 179 | 180 | function extend(){ 181 | var args = [this]; 182 | args.push.apply(args, arguments); 183 | return Compose.apply(0, args); 184 | } 185 | // Compose a constructor 186 | function Compose(base){ 187 | var args = arguments; 188 | var prototype = (args.length < 2 && typeof args[0] != "function") ? 189 | args[0] : // if there is just a single argument object, just use that as the prototype 190 | mixin(delegate(validArg(base)), args, 1); // normally create a delegate to start with 191 | function Constructor(){ 192 | var instance; 193 | if(this instanceof Constructor){ 194 | // called with new operator, can proceed as is 195 | instance = this; 196 | }else{ 197 | // we allow for direct calls without a new operator, in this case we need to 198 | // create the instance ourself. 199 | Create.prototype = prototype; 200 | instance = new Create(); 201 | } 202 | // call all the constructors with the given arguments 203 | for(var i = 0; i < constructorsLength; i++){ 204 | var constructor = constructors[i]; 205 | var result = constructor.apply(instance, arguments); 206 | if(typeof result == "object"){ 207 | if(result instanceof Constructor){ 208 | instance = result; 209 | }else{ 210 | for(var j in result){ 211 | if(result.hasOwnProperty(j)){ 212 | instance[j] = result[j]; 213 | } 214 | } 215 | } 216 | } 217 | } 218 | return instance; 219 | } 220 | // create a function that can retrieve the bases (constructors or prototypes) 221 | Constructor._getBases = function(prototype){ 222 | return prototype ? prototypes : constructors; 223 | }; 224 | // now get the prototypes and the constructors 225 | var constructors = getBases(args), 226 | constructorsLength = constructors.length; 227 | if(typeof args[args.length - 1] == "object"){ 228 | args[args.length - 1] = prototype; 229 | } 230 | var prototypes = getBases(args, true); 231 | Constructor.extend = extend; 232 | if(!Compose.secure){ 233 | prototype.constructor = Constructor; 234 | } 235 | Constructor.prototype = prototype; 236 | return Constructor; 237 | }; 238 | 239 | Compose.apply = function(thisObject, args){ 240 | // apply to the target 241 | return thisObject ? 242 | mixin(thisObject, args, 0) : // called with a target object, apply the supplied arguments as mixins to the target object 243 | extend.apply.call(Compose, 0, args); // get the Function.prototype apply function, call() it to apply arguments to Compose (the extend doesn't matter, just a handle way to grab apply, since we can't get it off of Compose) 244 | }; 245 | Compose.call = function(thisObject){ 246 | // call() should correspond with apply behavior 247 | return mixin(thisObject, arguments, 1); 248 | }; 249 | 250 | function getBases(args, prototype){ 251 | // this function registers a set of constructors for a class, eliminating duplicate 252 | // constructors that may result from diamond construction for classes (B->A, C->A, D->B&C, then D() should only call A() once) 253 | var bases = []; 254 | function iterate(args, checkChildren){ 255 | outer: 256 | for(var i = 0; i < args.length; i++){ 257 | var arg = args[i]; 258 | var target = prototype && typeof arg == "function" ? 259 | arg.prototype : arg; 260 | if(prototype || typeof arg == "function"){ 261 | var argGetBases = checkChildren && arg._getBases; 262 | if(argGetBases){ 263 | iterate(argGetBases(prototype)); // don't need to check children for these, this should be pre-flattened 264 | }else{ 265 | for(var j = 0; j < bases.length; j++){ 266 | if(target == bases[j]){ 267 | continue outer; 268 | } 269 | } 270 | bases.push(target); 271 | } 272 | } 273 | } 274 | } 275 | iterate(args, true); 276 | return bases; 277 | } 278 | // returning the export of the module 279 | return Compose; 280 | }); 281 | })(typeof define != "undefined" ? 282 | define: // AMD/RequireJS format if available 283 | function(deps, factory){ 284 | if(typeof module !="undefined"){ 285 | module.exports = factory(); // CommonJS environment, like NodeJS 286 | // require("./configure"); 287 | }else{ 288 | Compose = factory(); // raw script, assign to Compose global 289 | } 290 | }); 291 | -------------------------------------------------------------------------------- /test/compose.js: -------------------------------------------------------------------------------- 1 | (function(define){ 2 | define(function(require, exports, module){ 3 | 4 | var assert = require("patr/assert"), 5 | Compose = require("../compose"), 6 | required = Compose.required, 7 | around = Compose.around, 8 | from = Compose.from, 9 | create = Compose.create, 10 | Widget, MessageWidget, SpanishWidget; 11 | 12 | exports.testCompose = function() { 13 | Widget = Compose({ 14 | render: function(node){ 15 | node.innerHTML = "
hi
"; 16 | } 17 | }); 18 | var node = {}; 19 | var widget = new Widget(); 20 | widget.render(node); 21 | assert.equal(node.innerHTML, "
hi
"); 22 | }; 23 | exports.testComposeWithConstruct = function() { 24 | Widget = Compose(function(node){ 25 | this.node = node; 26 | },{ 27 | render: function(){ 28 | this.node.innerHTML = "
hi
"; 29 | }, 30 | getNode: function(){ 31 | return this.node; 32 | } 33 | }); 34 | var node = {}; 35 | var widget = new Widget(node); 36 | widget.render(); 37 | assert.equal(node.innerHTML, "
hi
"); 38 | }; 39 | exports.testInheritance = function() { 40 | MessageWidget = Compose(Widget, { 41 | message: "Hello, World", 42 | render: function(){ 43 | this.node.innerHTML = "
" + this.message + "
"; 44 | } 45 | }); 46 | var node = {}; 47 | var widget = new MessageWidget(node); 48 | widget.render(); 49 | assert.equal(node.innerHTML, "
Hello, World
"); 50 | }; 51 | exports.testInheritanceViaExtend = function() { 52 | MessageWidget = Widget.extend({ 53 | message: "Hello, World", 54 | render: function(){ 55 | this.node.innerHTML = "
" + this.message + "
"; 56 | } 57 | }); 58 | var node = {}; 59 | var widget = new MessageWidget(node); 60 | widget.render(); 61 | assert.equal(node.innerHTML, "
Hello, World
"); 62 | }; 63 | exports.testInheritance2 = function() { 64 | SpanishWidget = Compose(MessageWidget, { 65 | message: "Hola", 66 | }); 67 | var node = {}; 68 | var widget = new SpanishWidget(node); 69 | widget.render(); 70 | assert.equal(node.innerHTML, "
Hola
"); 71 | }; 72 | exports.testMultipleInheritance = function() { 73 | var Renderer = Compose(Widget, { 74 | render: function(){ 75 | this.node.innerHTML = "test" 76 | } 77 | }); 78 | var RendererSpanishWidget = Compose(Renderer, SpanishWidget); 79 | var SpanishWidgetRenderer = Compose(SpanishWidget, Renderer); 80 | var EmptyWidget = Compose(Widget,{}); 81 | var MessageWidget2 = Compose(MessageWidget, EmptyWidget); 82 | var node = {}; 83 | var widget = new RendererSpanishWidget(node); 84 | assert["throws"](function(){ 85 | widget.render(); // should throw conflicted error 86 | }); 87 | var widget = new SpanishWidgetRenderer(node); 88 | widget.render(); 89 | assert.equal(node.innerHTML, "test"); 90 | assert.equal(widget.getNode(), node); 91 | var widget = new MessageWidget2(node); 92 | widget.render(); 93 | assert.equal(node.innerHTML, "
Hello, World
"); 94 | }; 95 | exports.testAround = function() { 96 | var WithTitleWidget = Compose(MessageWidget, { 97 | message: "Hello, World", 98 | render: around(function(baseRender){ 99 | return function(){ 100 | baseRender.apply(this); 101 | node.innerHTML = "Title" + node.innerHTML; 102 | } 103 | }) 104 | }); 105 | var node = {}; 106 | var widget = new WithTitleWidget(node); 107 | widget.render(); 108 | assert.equal(node.innerHTML, "Title
Hello, World
"); 109 | }; 110 | exports.testRequired = function() { 111 | var logged; 112 | var Logger = Compose({ 113 | logAndRender: function(){ 114 | logged = true; 115 | this.render(); 116 | }, 117 | render: required 118 | }); 119 | var LoggerMessageWidget = Compose(Logger, MessageWidget); 120 | var node = {}; 121 | var widget = new LoggerMessageWidget(node); 122 | widget.logAndRender(); 123 | assert.equal(node.innerHTML, "
Hello, World
"); 124 | assert.equal(logged, true); 125 | var MessageWidgetLogger = Compose(MessageWidget, Logger); 126 | var node = {}; 127 | var widget = new MessageWidgetLogger(node); 128 | widget.logAndRender(); 129 | assert.equal(node.innerHTML, "
Hello, World
"); 130 | assert.equal(logged, true); 131 | var widget = new Logger(node); 132 | assert["throws"](function(){ 133 | widget.render(); 134 | }); 135 | }; 136 | exports.testCreate = function() { 137 | var widget = Compose.create({ 138 | render: function(node){ 139 | node.innerHTML = "
hi
"; 140 | } 141 | }); 142 | var node = {}; 143 | widget.render(node); 144 | assert.equal(node.innerHTML, "
hi
"); 145 | }; 146 | exports.testInheritanceCreate= function() { 147 | var widget = Compose.create(Widget, { 148 | message: "Hello, World", 149 | render: function(){ 150 | this.node.innerHTML = "
" + this.message + "
"; 151 | } 152 | }, {foo: "bar"}); 153 | widget.node = {}; 154 | widget.render(); 155 | assert.equal(widget.node.innerHTML, "
Hello, World
"); 156 | assert.equal(widget.foo, "bar"); 157 | }; 158 | exports.testNestedCompose = function() { 159 | var ComposingWidget = Compose(Compose, { 160 | foo: "bar" 161 | }); 162 | var widget = ComposingWidget({ 163 | bar: "foo" 164 | }); 165 | assert.equal(widget.foo, "bar"); 166 | assert.equal(widget.bar, "foo"); 167 | }; 168 | exports.testFromAlias = function() { 169 | var AliasedWidget = Compose(Widget, MessageWidget, { 170 | baseRender: from(Widget, "render"), 171 | messageRender: from("render"), 172 | render: function(){ 173 | this.baseRender(); 174 | var base = this.node.innerHTML; 175 | this.messageRender(); 176 | var message = this.node.innerHTML; 177 | this.node.innerHTML = base + message; 178 | } 179 | }); 180 | var node = {}; 181 | var widget = new AliasedWidget(node); 182 | widget.render(node); 183 | assert.equal(node.innerHTML, "
hi
Hello, World
"); 184 | }; 185 | exports.testFromExclude = function() { 186 | var ExcludeWidget = Compose(Widget, MessageWidget, { 187 | render: from(Widget) 188 | }); 189 | var node = {}; 190 | var widget = new ExcludeWidget(node); 191 | widget.render(); 192 | assert.equal(node.innerHTML, "
hi
"); 193 | }; 194 | exports.testComplexHierarchy = function(){ 195 | var order = []; 196 | var Widget = Compose( 197 | function(args){ 198 | this.id = args.id; 199 | }, 200 | { 201 | render: function(){ 202 | order.push(1); 203 | } 204 | } 205 | ); 206 | 207 | var SubMixin1 = Compose( 208 | { 209 | render: Compose.after(function(){ 210 | order.push(2); 211 | }) 212 | } 213 | ); 214 | var SubMixin2 = Compose( 215 | function(args){ 216 | }, 217 | { 218 | render: Compose.after(function(){ 219 | order.push(3); 220 | }) 221 | } 222 | ); 223 | var Mixin = Compose(SubMixin1, SubMixin2, 224 | { 225 | render: Compose.after(function(){ 226 | order.push(4); 227 | }) 228 | } 229 | ); 230 | 231 | var Mixin2 = Compose( 232 | { 233 | render: around(function(baseRender){ 234 | return function(){ 235 | baseRender.apply(this, arguments); 236 | order.push(5); 237 | }; 238 | }) 239 | } 240 | ); 241 | 242 | var Button = Compose(Widget, Mixin, Mixin2, 243 | function(args){ 244 | }, 245 | { 246 | render: Compose.around(function(baseRender){ 247 | return function(){ 248 | baseRender.apply(this, arguments); 249 | order.push(6); 250 | }; 251 | }) 252 | } 253 | ); 254 | var myButton = new Button({id: "myId"}); 255 | 256 | myButton.render(); 257 | assert.deepEqual(order, [1,2,3,4,5,6]); 258 | }; 259 | 260 | exports.testExtendError = function(){ 261 | var CustomError = Compose(Error, function(message){ 262 | this.message = message; 263 | },{ 264 | name: "CustomError" 265 | }); 266 | var error = new CustomError("test"); 267 | assert.equal(error.name, "CustomError"); 268 | assert.equal(error.toString(), "CustomError: test"); 269 | assert.equal(error instanceof CustomError, true); 270 | assert.equal(error instanceof Error, true); 271 | assert.equal(error.constructor, CustomError); 272 | } 273 | exports.testAfterNothing = function(){ 274 | var fooCount = 0, barCount = 0; 275 | var Base = Compose({ 276 | foo: Compose.after(function(){ 277 | fooCount++; 278 | }) 279 | }); 280 | var Sub1 = Compose(Base, { 281 | bar: Compose.after(function(){ 282 | barCount++; 283 | }) 284 | }); 285 | var sub = new Sub1; 286 | sub.foo(); 287 | sub.bar(); 288 | assert.equal(fooCount, 1); 289 | assert.equal(barCount, 1); 290 | } 291 | exports.testDiamond = function(){ 292 | var baseCallCount = 0, sub1CallCount = 0, sub2CallCount = 0, fooCallCount = 0, fooSub1Count = 0, fooSub2Count = 0; 293 | var Base = Compose(function(){ 294 | baseCallCount++; 295 | }, { 296 | foo: function(){ 297 | fooCallCount++; 298 | } 299 | }); 300 | var Sub1 = Compose(Base, function(){sub1CallCount++;}, { 301 | foo: Compose.after(function(){ 302 | fooSub1Count++; 303 | }) 304 | }); 305 | var Sub2 = Compose(Base, function(){sub2CallCount++;}, { 306 | foo: Compose.after(function(){ 307 | fooSub2Count++; 308 | }) 309 | }); 310 | var Combined = Sub1.extend(Sub2); 311 | var combined = new Combined; 312 | assert.equal(baseCallCount, 1); 313 | assert.equal(sub1CallCount, 1); 314 | assert.equal(sub2CallCount, 1); 315 | combined.foo(); 316 | assert.equal(fooCallCount, 1); 317 | assert.equal(fooSub1Count, 1); 318 | assert.equal(fooSub2Count, 1); 319 | } 320 | exports.testNull = function() { 321 | var errored; 322 | try{ 323 | Compose(null, {}); 324 | }catch(e){ 325 | errored = true; 326 | } 327 | assert.equal(errored, true); 328 | } 329 | exports.testAdvice = function() { 330 | var order = []; 331 | var obj = { 332 | foo: function(value){ 333 | order.push(value); 334 | return 6; 335 | }, 336 | }; 337 | Advised = Compose(obj, { 338 | "foo": Compose.around(function(base){ 339 | return function(){ 340 | order.push(2); 341 | try{ 342 | return base.apply(this, arguments); 343 | }finally{ 344 | order.push(4); 345 | } 346 | } 347 | }) 348 | }); 349 | Advised = Compose(Advised, { 350 | "foo": Compose.after(function(){ 351 | order.push(5); 352 | }) 353 | }); 354 | Advised = Compose(Advised, { 355 | "foo": Compose.before(function(value){ 356 | order.push(value); 357 | return [3]; 358 | }) 359 | }); 360 | obj = new Advised(); 361 | order.push(obj.foo(1)); 362 | assert.deepEqual(order, [1,2,3,4,5,6]); 363 | order = []; 364 | Advised = Compose(Advised, { 365 | "foo": Compose.before(function(value){ 366 | order.push(0); 367 | return Compose.stop; 368 | }) 369 | }); 370 | obj = new Advised(); 371 | obj.foo(1); 372 | assert.deepEqual(order, [0]); 373 | }; 374 | 375 | exports.testDecorator = function(){ 376 | var order = []; 377 | overrides = function(method){ 378 | return new Compose.Decorator(function(key){ 379 | var baseMethod = this[key]; 380 | if(!baseMethod){ 381 | throw new Error("No method " + key + " exists to override"); 382 | } 383 | this[key] = method; 384 | }); 385 | }; 386 | 387 | Widget = Compose({ 388 | render: function(){ 389 | order.push("render"); 390 | } 391 | }); 392 | SubWidget = Compose(Widget, { 393 | render: overrides(function(){ 394 | order.push("sub render") 395 | }) 396 | }); 397 | widget = new SubWidget(); 398 | widget.render(); 399 | assert.deepEqual(order, ["sub render"]); 400 | } 401 | 402 | if (require.main === module) 403 | require("patr/runner").run(exports); 404 | }); 405 | })(typeof define != "undefined" ? 406 | define: // AMD/RequireJS format if available 407 | function(factory){ 408 | factory(require, exports, module); // CommonJS environment, like NodeJS 409 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComposeJS 2 | 3 | ComposeJS is robust object composition built on native JavaScript mechanisms. 4 | ComposeJS is lightweight (3K minified, 1K gzipped) JavaScript module based on the 5 | philosophy that JavaScript's 6 | powerful composition mechanisms, including prototype inheritance, closures, and object 7 | literals should be embraced, not contorted into an emulation of classes from other 8 | languages. It is designed to be secure, to-the-metal fast, simple, and easy to 9 | use. ComposeJS builds on some of the best concepts from mixins, (traits)[http://traitsjs.org], and 10 | deterministic multiple inheritance. ComposeJS assists in composing constructors and instances, providing 11 | shorthand and robustness for best practice JavaScript. In fact, in the documentation 12 | equivalent JavaScript code is provided to show exactly what is happening. 13 | 14 | The core of ComposeJS is the Compose function. Compose() 15 | takes objects or constructors as arguments and returns a new constructor. The arguments 16 | are composed from left to right, later arguments taken precedence (overriding) former 17 | arguments, and any functions be executed on construction from left to right. A second 18 | key function is Compose.create() which behaves like Compose(), except that 19 | it returns object/instances rather than constructors. 20 | 21 | If you are using ComposeJS in a CommonJS environment, you can load it: 22 |
 23 | var Compose = require("compose");
 24 | 
25 | Or an AMD module loader (RequireJS, Dojo, etc), you can load it: 26 |
 27 | define(["compose"], function(Compose){
 28 |   ...
 29 | });
 30 | 
31 | 32 | If ComposeJS is loaded as a plain script, it will create Compose as a global variable. 33 | 34 | Now to start using Compose, let's create a simple object constructor: 35 |
 36 | 	Widget = Compose({
 37 | 		render: function(node){
 38 | 			node.innerHTML = "
hi
"; 39 | } 40 | }); 41 | var widget = new Widget(); 42 | widget.render(node); 43 |
44 | And the equivalent JavaScript: 45 |
 46 | 	Widget = function(){
 47 | 	};
 48 | 	Widget.prototype = {
 49 | 		render: function(node){
 50 | 			node.innerHTML = "
hi
"; 51 | } 52 | } 53 | var widget = new Widget(); 54 | widget.render(node); 55 |
56 | One the features provided by ComposeJS is that it creates constructors that will work 57 | regardless of whether they are called with the new operator, making them less prone 58 | to coding mistakes. One can also choose to omit the new operator to save bytes (for faster 59 | download), although calling with the new operator is slightly faster at runtime (so 60 | the faster overall would depend on how many times it is called). 61 | 62 | ## Extending existing constructor 63 | 64 | To extend our Widget we can simply include the Widget in Compose arguments: 65 |
 66 | 	HelloWidget = Compose(Widget, {
 67 | 		message: "Hello, World",
 68 | 		render: function(){
 69 | 			this.node.innerHTML = "
" + this.message + "
"; 70 | } 71 | }); 72 | var widget = new HelloWidget(); 73 | widget.render(node); 74 |
75 | And the equivalent JavaScript: 76 |
 77 | 	HelloWidget = function(){
 78 | 		this.message = "Hello, World";
 79 | 	};
 80 | 	HelloWidget.prototype = new Widget();
 81 | 	HelloWidget.prototype.render: function(){
 82 | 		this.node.innerHTML = "
" + this.message + "
"; 83 | }; 84 | var widget = new HelloWidget(); 85 | widget.render(node); 86 |
87 | 88 | Now let's create the constructor with a function to be executed on instantiation. Any 89 | functions in the arguments will be executed on construction, so our provided argument 90 | can be used to prepare the object on instantiation: 91 |
 92 | 	Widget = Compose(function(node){
 93 | 		this.node = node;
 94 | 	},{
 95 | 		render: function(){
 96 | 			this.node.innerHTML = "
hi
"; 97 | }, 98 | getNode: function(){ 99 | return this.node; 100 | } 101 | }); 102 | var widget = new Widget(node); 103 | widget.render(); 104 |
105 | And the equivalent JavaScript: 106 |
107 | 	Widget = function(node){
108 | 		this.node = node;
109 | 	};
110 | 	Widget.prototype = {
111 | 		render: function(){
112 | 			this.node.innerHTML = "
hi
"; 113 | }, 114 | getNode: function(){ 115 | return this.node; 116 | } 117 | } 118 | var widget = new Widget(node); 119 | widget.render(); 120 |
121 | 122 | Compose can compose constructors from multiple base constructors, effectively 123 | providing multiple inheritance. For example, we could create a new widget from Widget 124 | and Templated base constructors: 125 |
126 | 	TemplatedWidget = Compose(Widget, Templated, {
127 | 	  // additional functionality
128 | 	});
129 | 
130 | Again, latter argument's methods override former argument's methods. In this case, 131 | Templated's methods will override any Widget's method of the same name. However, 132 | Compose is carefully designed to avoid any confusing conflict resolution in ambiguous cases. 133 | Automatic overriding will only apply when later arguments have their own methods. 134 | If a later argument constructor or object inherits a method, this will not automatically override 135 | former base constructor's methods unless it has already overriden this method in another base 136 | constructor's hierarchy. In such cases, the appropriate method must be designated in the final 137 | object or else it will remain in a conflicted state. This essentially means that explicit ordering 138 | provides straightforward, easy to use, method overriding, without ambiguous magical conflict 139 | resolution (C3MRO). 140 | 141 | We can specify required methods that must be overriden as well. For example, we can 142 | define the Widget to require a generateHTML method: 143 |
144 | 	var required = Compose.required;
145 | 	Widget = Compose({
146 | 		generateHTML: required,
147 | 		...
148 | 	});
149 | 
150 | 151 | And now to extend the Widget constructor, we must provide a generateHTML method. 152 | Failure to do so will result in an error being thrown when generateHTML is called. 153 | 154 | ## Apply to an existing object 155 | 156 | Compose can also be applied to existing objects to add/mixin functionality to that object. 157 | This is done by using the standard call() or apply() function methods to define |this| for the 158 | call. When Compose is applied in this way, the target object will have the methods from 159 | all the provide objects or constructors added to it. For example: 160 |
161 | 	var object = {a: 1};
162 | 	Compose.call(object, {b: 2});
163 | 	object -> {a: 1, b: 2}
164 | 
165 | 166 | We can use this form of Compose to add methods during construction. This is one style 167 | of creating instances that have private and public methods. For example, we could extend 168 | Widget with: 169 |
170 | 	var required = Compose.required;
171 | 	Widget = Compose(Widget, function(innerHTML){
172 | 		// this will mixin the provide methods into |this|
173 | 		Compose.call(this, {
174 | 			generateHTML: function(){
175 | 				return "
" + generateInner() + "
"; 176 | } 177 | }); 178 | // private function 179 | function generateInner(){ 180 | return innerHTML; 181 | } 182 | }); 183 |
184 | 185 | Applying Compose can also be conveniently leveraged to make constructors that mixin properties 186 | from an object argument. This is a common pattern for constructors and allows an 187 | instance to be created with preset properties provided to the constructor. This also 188 | also makes it easy to have independent optional named parameters with defaults. 189 | We can implement this pattern by simple having Compose be a base constructor 190 | for our composition. For example, we can create a widget that extends Compose 191 | and therefore we can instantiate Widgets with an object argument that provides initial property settings: 192 |
193 | 	Widget = Compose(Compose, {
194 | 		render: function(){
195 | 			this.node.innerHTML = "
hi
"; 196 | } 197 | }); 198 | var widget = new Widget({node: byId("some-id")}); 199 | widget.node -> byId("some-id") 200 | widget.render(); 201 |
202 | This is a powerful way to build constructors since constructors can be created that include 203 | all the functionality that Compose provides, including decorators and multiple 204 | objects or constructors as arguments. 205 | 206 | ## Compose.create 207 | 208 | Compose.create() is another function provided by the ComposeJS library. This function 209 | is similar to Compose() and takes exactly the same type of arguments (any mixture 210 | of constructors or objects), but rather 211 | than creating a constructor, it directly creates an instance object. Calling the constructor 212 | returned from Compose with no arguments and calling Compose.create act approximately 213 | the same action, i.e. Compose(...)() acts the same as Compose.create(...). The main 214 | difference is that Compose.create is optimized for instance creation and avoids 215 | unnecessary prototype creation involved in creating a constructor. 216 | 217 | Compose.create is particularly useful in conjunction with the closure-style constructors. 218 | A closure-style constructor (sometimes called the module pattern) can have private 219 | variables and generally returns an object created using object literal syntax. 220 | For base constructors that don't extend anything else, This is well-supported by native JavaScript 221 | there is no need to use ComposeJS (or another library) to create a simple base constructor. 222 | But for extending base constructors, Compose.create is very useful. For example, 223 | we could create a base widget using this pattern (again, we can just use native JavaScript): 224 |
225 | 	Widget = function(node){ // node is a private variable
226 | 		return {
227 | 			render: function(){
228 | 				node.innerHTML = this.message;
229 | 			},
230 | 			message: "Hello"
231 | 		};
232 | 	};
233 | 
234 | And now we could extend this widget, continuing to use the closure-style constructor, 235 | with help from Compose.create. Here we will call base constructor, and use the returned 236 | base instance to compose an extended instance. The "node" variable continues to stay 237 | protected from direct access: 238 |
239 | 	BoldWidget = function(node){
240 | 		baseWidget = Widget(node);
241 | 		return Compose.create(baseWidget, {
242 | 			render: function(){
243 | 				baseWidget.render(); 
244 | 				node.style.fontWeight = "bold";
245 | 			}
246 | 		});
247 | 	};
248 | 
249 | 250 | ##Constructor.extend 251 | Constructors created with Compose also include a "static" extend method that can be 252 | used for convenience in creating subclasses. The extend method behaves the same 253 | as Compose with the target class being the first parameter: 254 |
255 | 	MyClass = Compose(...);
256 | 	SubClass = MyClass.extend({
257 | 		subMethod: function(){}
258 | 	});
259 | 	// same as:
260 | 	SubClass = Compose(MyClass,{
261 | 		subMethod: function(){}
262 | 	});
263 | 
264 | 265 | ## Decorators 266 | Decorators provides a customized way to add properties/methods to target objects. 267 | Several decorators are provided with ComposeJS: 268 | 269 | ### Aspects (or Super-calls) 270 | 271 | Compose provides an aspect-oriented decorator to add functionality to existing method 272 | instead of completely overriding or replacing the method. This provides super-call type 273 | functionality. The after() function allows one to add code that will be executed after 274 | the base method: 275 |
276 | 	var after = Compose.after;
277 | 	WidgetWithTitle = Compose(Widget, {
278 | 		render: after(function(){
279 | 			// called after the original render() from Widget  
280 | 			this.node.insertBefore(header, this.node.firstChild);
281 | 		}
282 | 	});
283 | 
284 | 285 | The after() advice (provided function) can return a value that will be returned to the original caller. If 286 | nothing is returned, the inherited method's return value will be returned. 287 | 288 | The before() function allows one to add code that will be executed before 289 | the base method: 290 |
291 | 	var before = Compose.before;
292 | 	BoldWidget = Compose(Widget, {
293 | 		render: before(function(){
294 | 			// called before the original render() from Widget  
295 | 			this.node.style.fontWeight = "bold";
296 | 		}
297 | 	});
298 | 
299 | 300 | The before() advice can return an array that will be used as the arguments for the 301 | inherited function. If nothing is returned, the original calling arguments are passed to 302 | the inherited function. If Compose.stop is returned, the inherited function will not be 303 | called. 304 | 305 | The around function allows one to closure around an overriden method to combine 306 | functionality. For example, we could override the render function in Widget, but still 307 | call the base function: 308 |
309 | 	var around = Compose.around;
310 | 	BoldWidgetWithTitle = Compose(Widget, {
311 | 		render: around(function(baseRender){
312 | 			// return the new render function
313 | 			return function(){
314 | 				this.node.style.fontWeight = "bold";
315 | 				baseRender.call(this);
316 | 				this.node.insertBefore(header, this.node.firstChild);
317 | 			};
318 | 		});
319 | 	});
320 | 
321 | 322 | ### Composition Control: Method Aliasing and Exclusion 323 | One of the key capabilities of traits-style composition is control of which method to 324 | keep or exclude from the different components that are being combined. The from() 325 | decorator provides simple control over which method to use. We can use from() with 326 | the base constructor to indicate the appropriate method to keep. For example, if we 327 | composed from Widget and Templated, we could use from() to select the save() 328 | method from Widget and render() from Templated: 329 |
330 | 	var from = Compose.from;
331 | 	TemplatedWidget = Compose(Widget, Templated, {
332 | 		save: from(Widget),
333 | 		render: from(Templated)
334 | 	});
335 | 
336 | 337 | We can also alias methods, making them available under a new name. This is very useful 338 | when we need to access multiple conflicting methods. We can provide a string argument 339 | that indicates the method name to retrieve (it will be aliased to the property name that 340 | it is being applied to). With the string argument, the constructor argument is optional 341 | (defaults to whatever method would naturally be selected for the given name): 342 |
343 | 	var from = Compose.from;
344 | 	TemplatedWidget = Compose(Widget, Templated, {
345 | 		widgetRender: from(Widget, "render"),
346 | 		templateRender: from(Templated, "render"),
347 | 		saveTemplate: from("save"),
348 | 		render: function(){
349 | 			this.widgetRender();
350 | 			this.templateRender();
351 | 			// do other stuff
352 | 		},
353 | 		save: function(){
354 | 			this.saveTemplate();
355 | 			//...
356 | 		}
357 | 	});
358 | 
359 | 360 | #### Conflict Example 361 | To help understand conflicts, here is the simplest case where Compose would give a conflict error: 362 | 363 | A = Compose({ 364 | foo: function(){ console.log("A foo"); } 365 | }); 366 | 367 | B = Compose({ 368 | foo: function(){ console.log("B foo"); } 369 | }); 370 | 371 | C = Compose(B, {}); 372 | 373 | D = Compose(A, C); 374 | new D().foo() 375 | 376 | Compose considers class D's foo() method to be a conflict because for C takes 377 | precedence over A, but C only inherits foo, it doesn't directly have foo. In other 378 | words, a bread-first linearization of methods would give A's foo precedence, 379 | but a depth-first linearization of methods would give B's foo precedence, and 380 | since this disagree, it is considered ambiguous. Note that these are all conflict free: 381 | 382 | D = Compose(C, A); // A's own foo() wins 383 | D = Compose(A, B); // B's own foo() wins 384 | D = Compose(A, C, {foo: Compose.from(A)}); // explicitly chose A's foo 385 | D = Compose(A, C, {foo: Compose.from(B)}); // explicitly chose B's foo 386 | 387 | ### Creating Decorators 388 | Decorators are created by newing the Decorator constructor with a function argument 389 | that is called with the property name. The function's |this| will be the target object, and 390 | the function can add a property anyway it sees fit. For example, you could create a decorator 391 | that would explicitly override another methods, and fail if an existing method as not there. 392 |
393 | 	overrides = function(method){
394 | 		return new Compose.Decorator(function(key){
395 | 			var baseMethod = this[key];
396 | 			if(!baseMethod){
397 | 				throw new Error("No method " + key + " exists to override");
398 | 			}
399 | 			this[key] = method;
400 | 		});
401 | 	};
402 | 	
403 | 	Widget = Compose({
404 | 		render: function(){
405 | 			...
406 | 		}
407 | 	});
408 | 	SubWidget = Compose(Widget, {
409 | 		render: overrides(function(){
410 | 			...
411 | 		})
412 | 	});
413 | 
414 | 415 | In addition, the Decorator function accepts a second argument, which is the function 416 | that would be executed if the decorated method is directly executed and does not override another method. 417 | 418 | ### Security 419 | By default Compose will add a constructor property to your constructor's prototype to make 420 | the constructor available from instances: 421 |
422 | 	Widget = Compose({...});
423 | 	var widget = new Widget();
424 | 	widget.constructor == Widget // true
425 | 
426 | However, in the context of object capability security, providing access to the constructor 427 | from instances is considered a violation of principle of least access. If you would like to 428 | disable this feature for the purposes of using Compose in secure environments, you can 429 | set: 430 |
431 | Compose.secure = true;
432 | 
433 | 434 | ### Enumeration on Legacy Internet Explorer 435 | Internet Explorer 8 and earlier have a known issue with enumerating properties that 436 | shadow dontEnum properties (toString, toValue, hasOwnProperty, etc. for objects), which 437 | means that these properties will not be copied to your class on these versions of IE. There is a known 438 | fix for this, but it does meet the high performance and space priorities of Compose. 439 | However, if you need to override one of these methods, you can easily workaround this 440 | issue by setting the method in the constructor instead of the provide object. For example: 441 |
442 | 	Widget = Compose(function(){
443 | 		this.toString = function(){
444 | 			// my custom toString method
445 | 		};
446 | 	},{
447 | 		// my other methods
448 | 	});
449 | 
450 | 451 | # License 452 | 453 | ComposeJS is freely available under *either* the terms of the modified BSD license *or* the 454 | Academic Free License version 2.1. More details can be found in the [LICENSE](LICENSE). 455 | The ComposeJS project follows the IP guidelines of Dojo foundation packages and all 456 | contributions require a Dojo CLA. --------------------------------------------------------------------------------