node-riveter
v2.0.0
Published
Mix-in, inheritance and constructor extend behavior for your JavaScript enjoyment.
Downloads
11,055
Readme
node-riveter
Installation
npm i node-riveter
const riveter = require('node-riveter')
What is it?
node-riveter is an experimental JavaScript micro-library (based on riveter) that provides the following helper methods:
riveter.mixin
- use this to mix object literals into the target constructor function's prototype. Mixin methods are only copied to the prototype if they do not already exist on the prototype (so it preserves/prefers existing prototype implementations). (Can be used stand-alone viariveter.mixin
or it can be attached to a constructor function.)riveter.compose
- likemixin
, you use this to mix objects which provide additional behavior into your constructor function - however, all of the mixins passed tocompose
are inserted into the target constructor's prototype chain, on the 'parent' prototype. Composed mix-ins may optionally provide initialization methods that can be called before or after the original constructor function executes. (Can be used stand-alone viariveter.compose
or it can be attached to a constructor function.)riveter.inherits
- an inheritance helper allowing a constructor function to inherit the prototype of another, allowing the ability to override the parent's constructor and also to provide 'shared' methods to the constructor function itself, etc. (Can be used stand-alone viariveter.inherits
or it can be attached to a constructor function.)riveter.extend
- helper method which can be attached to a constructor function. It wraps theinherits
call, providing the constructor to which it is attached as theparent
argument. (Must be attached to a constructor function to operate.)riveter.punch
- works likemixin
, but mixin methods will override the target prototype if they already exist.riveter.rivet
- Adds themixin
,extend
,compose
,inherits
andpunch
calls to a constructor function if they are not already present. While you can callriveter.rivet
, a short cut is provided by simply callingriveter()
and passing 1-to-n constructor functions as arguments, each of which will have the methods applied.
What is different between node-riveter and riveter?
Well the main difference is this is a node only implementation
Why would I use it?
The boilerplate/ceremonial code around managing mix-ins and inheritance in JavaScript can be a significant portion of a code base. We want to remove the unnecessary boilerplate while providing a consistent, reliable way to extend constructors through a powerful & concise means.
riveter is heavily informed by appendTo projects using Backbone.js. We've often wanted the backbone-style extend functionality, but with some slight tweaks, while maintaining compatibility with backbone objects. riveter
is our attempt to standardize that approach. riveter is not dependent on backbone, it just plays well with it. Speaking of dependencies, riveter does take one dependency: lodash.js.
How do I use it?
mixin
constructorFn.mixin( mixin1 [, mixin2, mixin3, etc.] );
riveter.mixin( constructorFn, mixin1 [, mixin2, mixin3, etc.] );
The mixin
call takes 1-to-n number of object literals, each containing methods you want mixed into the target constructor function's prototype. Methods that already exist on the prototype will not be overridden. For example, let's say you had a pub/sub mixin, which you wanted to use across multiple constructor functions, to ensure that those instances had a publish
and subscribe
call. Of course, you could add those calls to the individual prototypes of your various constructor functions, or you could have a common 'base' prototype from which each inherit. Or you could do this:
var Person = function( name ) {
this.name = name;
};
Person.prototype.greet = function() {
return "Hi, " + this.name;
};
var Order = function( id ) {
this.id = id;
};
Order.prototype.addItem = function( item ) {
// some behavior….
};
var Product = function( sku ) {
this.sku = sku;
};
riveter( Product, Person, Order );
var pubSub = {
publish: mediator.publish,
subscribe: function( topic, callback ) {
if( !this._subscriptions ) {
this._subscriptions = {};
}
this._subscriptions[ topic ] = mediator.subscribe( topic, callback );
},
removeSubscriptions: function() {
if( this._subscriptions ) {
_.each( this._subscriptions, function( sub ){
sub.unsubscribe();
});
}
}
};
Person.mixin( pubSub );
Order.mixin( pubSub );
Product.mixin( pubSub );
compose
constructorFn.compose( mixin1 [, mixin2, mixin3, etc.] );
riveter.compose( constructorFn, mixin1 [, mixin2, mixin3, etc. ] );
The compose
call can be used to mix blocks of behavior into a constructor function. compose
returns a new constructor function, with the mixed-in members on the resulting 'parent' prototype, but still-overridable by prototype or instance members applied from that point on.
Continuing from the example above (for mixin
s) - isn't it kind of ugly that the pubSub mixin's methods have to check to see if the _subscriptions
member is present? Wouldn't it be nice if we could initialize state for the mixin, much like it we had baked the logic into the actual constructor function? You can, by using compose
to bake your mixins into a single level of the prototype chain:
var pubSub = {
_postInit: function() {
this._subscriptions = {};
},
mixin: {
publish: mediator.publish,
subscribe: function( topic, callback ) {
this._subscriptions[ topic ] = mediator.subscribe( topic, callback );
},
removeSubscriptions: function() {
_.each( this._subscriptions, function( sub ){
sub.unsubscribe();
});
}
}
}
var MsgPerson = Person.compose( pubSub );
var MsgOrder = Order.compose( pubSub );
var MsgProduct = Product.compose( pubSub );
In the above example, our 'composable' mixin is now structured slightly differently. The actual mixin is on the mixin
member, and we've provided a _postInit
method. _postInit
will execute just after the constructor function, and is passed any arguments that were passed to the constructor. You can optionally use the _preInit
method to have your setup execute just before the constructor function executes. Although we haven't found a real use-case yet, you can provide both a _preInit
and a _postInit
method if you need to get way fancier than we have.
compose
may feel like a hybrid between mixin
and inherits
- that's because it is. Use mixin
for when you only need to mix behavior from other objects into a target constructor's prototype. Use inherits
if you need/want to follow a more classical inheritance approach. Use compose
if you're mixing in mixins that have _preInit
or postInit
behavior, or to simulate multiple inheritance at one level of the prototype chain.
inherits
There are two ways to use inherits
: the stand-alone version (riveter.inherits
) and when it's attached to a constructor function.
riveter.inherits (stand alone version)
riveter.inherits(child, parent [, ctorProps ]);
The inherits
method allows you to specify a parent
contructor function from which a child
constructor function can inherit. Optionally, the child
can be an object literal (which is then used at the prototype of a new instance). You can optionally provide the ctorProps
argument, which applies 'shared' methods to the constructor function itself. Really, inherits
is quite similar to many existing implementations which provide helper utilities around prototypical inheritance. It's worth noting that when child
inherits from parent
, it's prototype will be a new instance of parent
. Some examples:
var Person = function( name ) {
this.name = name;
};
Person.prototype.greet = function() {
return "Hi, " + this.name;
};
var Employee = function( name, title, salary ) {
Employee.__super.call( this, name );
this.title = title;
this.salary = salary;
};
Employee.prototype.giveRaise = function( amount ) {
this.salary += amount;
};
// Now, let's make Employee inherit from Person:
riveter.inherits( Employee, Person );
As mentioned above, we can also provide an object literal as the "child" argument:
var CEOProto = {
fireAllThePeeps: function() {
return "YOU'RE ALL FIRED!";
},
constructor: function( name, title, salary, shouldExpectFbiRaid ) {
CEOProto.constructor.__super.call( this,name, title, salary );
this.shouldExpectFbiRaid = shouldExpectFbiRaid;
}
};
// let's use the above object as the prototype of a new constructor function, which inherits from Employee:
var CEO = riveter.inherits( CEOProto, Employee );
Notice that when we provide a constructor function for the child
argument, the inherits
call mutates that constructor function in place, so there's no need to do this:
// Don't need to do this, since Employee is already a constructor function
var Employee = riveter.inherits( Employee, Person );
However, when we passed an object literal as the child
argument, then you will want to take advantage of the fact that inherits
returns the resulting constructor function:
// only other way to get CEO would be to reference CEOProto.constructor
// but constructor is an optional property if you pass an object literal...
var CEO = riveter.inherits( CEOProto, Employee );
The examples above also demonstrate that riveter puts a __super
function member on the resulting child constructor function, which is simply the constructor of the parent. In addition, a __super__
member is added as well (to match what Backbone.js provides) - which is a reference to the parent's prototype. We don't recommend over-(ab)using calls to a __super
constructor in your child instances, but given that this is still a fairly common approach with many JavaScript developers, we wanted to demonstrate that it's possible to do so if necessary.
constructor.inherits
constructorFn.inherits(parent [, ctorProps ]);
When inherits
is attached to a constructor function, it's functionality is identical to riveter.inherits
, except that the constructor function it is attached to is the child
argument, so all you have to do is provide a parent
argument and, optionally, any shared/constructor methods. The inherits
method can be attached by passing your constructor function to riveter.ensureHelpers
(it's attached automatically to any constructor that uses extend
or mixin
).
var Person = function( name ) {
this.name = name;
};
Person.prototype.greet = function() {
return "Hi, " + this.name;
};
var Employee = function( name, title, salary ) {
Employee.__super.call( this, name );
this.title = title;
this.salary = salary;
};
Employee.prototype.giveRaise = function( amount ) {
this.salary += amount;
};
// ensureHelpers adds inherits, extend and mixin
// if they do not already exist on the constructor
riveter.ensureHelpers( Employee );
// Now, let's make Employee inherit from Person:
Employee.inherits( Person );
extend
constructorFn.extend(childPrototypeProps [, ctorProps] );
riveter.extend(constructorFn, childPrototypeProps [, ctorProps] );
It's very common for JavaScript developers to have an existing constructor function they'd like to use as a 'base' constructor, from which other constructors could inherit. The extend
call can make this possible. It gets attached to an existing constructor function, and simply wraps a call to inherits
, passing the constructor to which it's attached as the parent
argument. This pattern will feel familiar to developers that have used similar approaches in libraries like Prototype.js, Closure and Backbone.js. For example:
var Person = function( name ) {
this.name = name;
this.initialize.apply( this, arguments );
};
_.extend(Person.prototype, {
greet: function() {
return "Hi, " + this.name;
},
initialize: function() {
}
});
// Here we attach the extend call to the constructor
// Using extend will result in riveter ensuring inherits,
// extend and mixin exist on the constructor from then on
Person.extend = riveter.extend;
var Employee = Person.extend({
giveRaise: function( amount ) {
this.salary += amount;
},
initialize: function( name, title, salary ) {
this.title = title;
this.salary = salary;
}
}, {
getInstance: function( name, title, salary ) {
return new Employee( name, title, salary );
}
});
var CEO = Employee.extend({
constructor: function( name, title, salary, shouldExpectFbiRaid ) {
CEO.__super.call( this, name, title, salary );
this.shouldExpectFbiRaid = shouldExpectFbiRaid;
},
fireAllThePeeps: function() {
return "YOU'RE ALL FIRED!";
}
});
Note in the above examples we've taken a cue from Backbone.js in providing a no-op initialize
call in the Person
prototype, and we call it at the end of the Person
constructor. That way, if any inheriting constructor provides an implementation of initialize
, it will call that instead.
punch
constructorFn.punch( mixin1 [, mixin2, mixin3, etc.] );
riveter.punch( constructorFn, mixin1 [, mixin2, mixin3, etc.] );
The punch
call takes 1-to-n number of object literals, each containing methods you want mixed into the target constructor function's prototype. Methods that already exist on the prototype will be overridden.
var Person = function( name ) {
this.name = name;
};
Person.prototype.greet = function() {
return "Hi, " + this.name;
};
riveter( Person );
var greetPunch = {
greet: function() {
return "OHSNAP, I get to greet you, " + this.name;
}
};
Person.punch( greetPunch );
Caveats
node-riveter is a node only fork of riveter. It is currently maintained as it is a dependency of foo-foo-mq. However, it may disappear at any time if it is no longer needed by foo-foo-mq.
appendTo Labs has stated similar caveats with the upstream of this project. See their original message below:
riveter is currently an 'appendTo Labs' effort. This means that we're excited about it enough to make it a micro-library - and we invite you to try it out with us and give us your feedback. However, being that it's experimental, we may also decide it's the worst idea since the 8-track cassette player. As long as it continues to prove promising, it stands the chance of becoming a more 'officially supported' appendTo project. In short - we may pull the plug or change the name at any moment.
Building & Running Tests
- Install node.js (and consider using nvm to manage your node versions)
- run
npm install
to install all dependencies - To run tests & examples:
- To run node-based tests:
npm run test
- To run node-based tests: