Skip to content

Instantly share code, notes, and snippets.

@constantology
Created November 14, 2012 15:24
Show Gist options
  • Save constantology/4072739 to your computer and use it in GitHub Desktop.
Save constantology/4072739 to your computer and use it in GitHub Desktop.
Backbone.View.$uper
;!function() {
"use strict";
// fn is the super class' callSuper that has being curried with its __super__ prototype
// proto was curried in by $uper's custom extend method
function callNestedSuper( fn, proto/*, ...args*/ ) {
var args = Super._.toArray( arguments ).slice( 1 ), val1, val2;
if ( args[1] === this.__lastMethod__ ) // avoid possible recursion errors
return this;
val1 = getRetVal( this, callSuper.apply( this, args ) );
val2 = getRetVal( this, fn.apply( this, args.slice( 1 ) ) );
return val1 === this && val2 !== this
? val2
: val1 !== this
? val1
: this;
}
function callSuper( proto, method, args ) {
var fn = ( proto || empty )[method], val;
if ( Super._.isFunction( fn ) && method !== this.__lastMethod__ ) { // avoid possible recursion errors
this.__lastMethod__ = method;
val = fn.apply( this, args );
delete this.__lastMethod__;
return val === UNDEF ? this : val;
}
return this;
}
function curry( fn ) {
var args = Super._.toArray( arguments ).slice( 1 ), fun;
fun = function curried() {
return fn.apply( this, args.concat( Super._.toArray( arguments ) ) );
};
fun.original = fn;
return fun;
}
function extend() {
var Class = Backbone.View.extend.apply( this, arguments );
Class.extend = extend;
Class.prototype.$uper = curry( Super._.wrap( Class.__super__.$uper, callNestedSuper ), Class.__super__ );
return Class;
}
function getRetVal( ctx, val ) {
return val === ctx || val === UNDEF
? ctx
: val;
}
var UNDEF, Super, empty = {};
Super = Backbone.View.extend( {
constructor : function( options ) {
Super._.extend( this, options || {} );
return this.$uper( 'constructor', arguments );
},
$uper : curry( callSuper, Backbone.View.prototype ),
_configure : function() {
this.$uper( '_configure', arguments );
if ( this.className ) {
this.slcEl = '.' + this.className;
this.slcCt = this.slcEl + '-ct';
}
},
afterRender : function() {
return this;
},
assignContainer : function( ct ) {
if ( ct ) {
if ( Super._.isElement( ct ) || Super._.isString( ct ) )
ct = $( ct );
if ( Super._.isObject( ct ) && ct.length )
ct.append( this.el );
this.$ct = ct;
this.ct = ct[0];
}
return this;
},
destroy : function() {
this.destroyed = true;
this.rendered = false;
this.undelegateEvents();
return this.destroyRefs().remove().trigger( 'destroy', this );
},
destroyRefs : function() {
delete this.$ct; delete this.ct;
delete this.$elCt; delete this.elCt;
return this;
},
make : function() {
return Super._.isFunction( this.template ) ? this.template( this ) : this.$uper( 'make', arguments );
},
onRender : function() {
return this;
},
prepareRefs : function() {
if ( this.slcCt ) {
this.$elCt = this.$el.find( this.slcCt );
this.elCt = this.$elCt[0];
}
return this;
},
render : function( ct ) {
if ( !this.rendered ) {
this.assignContainer( ct ).onRender().prepareRefs().afterRender();
this.rendered = true;
return this.$uper( 'render', arguments ).trigger( 'render', this );
}
return this;
}
} );
define( ['Handlebars', 'Modernizr', 'jQuery', 'underscore'], function( Handlebars, Modernizr, $, _ ) {
return Backbone.View.$uper || ( Backbone.View.$uper = _.extend( Super, {
_ : _,
$ : $,
Handlebars : Handlebars,
Modernizr : Modernizr,
extend : extend
} ) );
} );
}();
define( ['Handlebars', 'Modernizr', 'jQuery', 'underscore', 'SuperView'], function( Handlebars, Modernizr, jQuery, _, $uper ) {
"use strict";
describe( 'Trying to bring an iota of sanity back to UI development — and deal with Backbone mania — with the very inelegant Backbone.View.$uper', function() {
describe( 'Using some semblance of an inheritance pattern that gives the developer convenience rather than potential confusion.', function() {
it( 'allows you to create Backbone.Views', function() {
var called_ctor = false,
view1 = new $uper,
view2 = new ( $uper.extend( {
constructor : function() {
called_ctor = true;
this.$uper( 'constructor', arguments );
}
} ) );
assert( view1 instanceof $uper );
assert( view1 instanceof Backbone.View );
assert( view2 instanceof $uper );
assert( view2 instanceof Backbone.View );
assert( called_ctor === true );
} );
it( 'Allows you to call super methods using a nasty looking: '
+ '`this.$uper( method_name:String, args:Arguments|Array )` — very crumby indeed — '
+ 'however, being able to call super on a Class with an inheritance chain longer than 1 — which is '
+ 'impossible in Backbone without making your code a refactoring nightmare and fuglier than a '
+ 'naked dick cheeney — is nice.', function() {
var spy_ctor = sinon.spy( Backbone.View.prototype, 'constructor' ),
spy_init = sinon.spy( Backbone.View.prototype, 'initialize' );
var called_ctor1 = false, called_ctor2 = false, called_ctor3 = false,
called_init1 = false, called_init2 = false, called_init3 = false,
called_foo1 = false, called_foo2 = false, called_foo3 = false;
var View1 = $uper.extend( {
constructor : function( config ) {
called_ctor1 = true;
assert( typeof config === 'object' );
assert( config.id === id );
this.id = config.id;
this.$uper( 'constructor', arguments );
},
initialize : function() {
called_init1 = true;
return this.$uper( 'initialize', arguments );
},
foo : function( val ) {
called_foo1 = true;
assert( val === 'bar', 'View1.foo received incorrect argument: ', val );
return this;
}
} ),
View2 = View1.extend( {
constructor : function( config ) {
called_ctor2 = true;
assert( typeof config === 'object' );
assert( config.id === id );
this.$uper( 'constructor', arguments );
},
initialize : function() {
called_init2 = true;
return this.$uper( 'initialize', arguments );
},
foo : function( val ) {
called_foo2 = true;
assert( val === 'bar', 'View1.foo received incorrect argument: ', val );
return this.$uper( 'foo', arguments );
}
} ),
View3 = View2.extend( {
constructor : function( config ) {
called_ctor3 = true;
assert( typeof config === 'object' );
assert( config.id === id );
this.$uper( 'constructor', arguments );
},
initialize : function() {
called_init3 = true;
return this.$uper( 'initialize', arguments );
},
foo : function( val ) {
called_foo3 = true;
assert( val === 'bar', 'View1.foo received incorrect argument: ', val );
return this.$uper( 'foo', arguments );
}
} );
var id = 'view-3',
conf = { id : id },
view = new View3( conf );
assert( view.id === id, 'view.id not set' );
assert( view.foo( 'bar' ) === view, 'context not returned' );
assert( view instanceof Backbone.View );
assert( view instanceof $uper );
assert( view instanceof View1 );
assert( view instanceof View2 );
assert( view instanceof View3 );
assert( called_ctor3 === true, 'View3.constructor not called' );
assert( called_ctor2 === true, 'View2.constructor not called' );
assert( called_ctor1 === true, 'View1.constructor not called' );
assert( called_init3 === true, 'View3.initialize not called' );
assert( called_init2 === true, 'View2.initialize not called' );
assert( called_init1 === true, 'View1.initialize not called' );
assert( called_foo3 === true, 'View3.foo not called' );
assert( called_foo2 === true, 'View2.foo not called' );
assert( called_foo1 === true, 'View1.foo not called' );
assert( spy_ctor.calledWith( conf ) );
assert( spy_init.calledWith( conf ) );
} );
} );
describe( 'Implementing actual render/destroy functionality, though did not bother to fix the fact that Backbone.View breaks lazy rendering.', function() {
it( 'Allows you to pass a container to render your view to', function() {
var view = new $uper( {
className : 'test',
template : $uper.Handlebars.compile( '<div class="{{className}}"><div class="{{className}}-ct"></div></dib></div>' )
} );
view.render( 'body' );
assert( view.rendered ); assert( !view.destroyed );
assert( view.ct === document.body );
assert( view.$ct[0] === view.ct );
assert( $uper.$.contains( view.$ct[0], view.el ) );
assert( view.slcEl === '.' + view.className );
assert( view.slcCt === view.slcEl + '-ct' );
assert( $uper.$.contains( view.el, view.elCt ) );
} );
it( 'Allows you to destroy your view, because, how many times are you going to add and remove the same damn Backbone.View instance to the DOM‽ ⇐ notice: cool use of interobang :D', function() {
var ct,
view = new $uper( {
className : 'test',
template : $uper.Handlebars.compile( '<div class="{{className}}"><div class="{{className}}-ct"></div></dib></div>' )
} );
view.render( 'body' );
assert( $uper.$.contains( view.$ct[0], view.el ) );
ct = view.ct;
view.destroy();
assert( !$uper.$.contains( ct, view.el ) );
assert( !view.$ct ); assert( !view.ct );
assert( !view.$elCt ); assert( !view.elCt );
assert( !view.rendered ); assert( view.destroyed );
} );
} );
} );
describe( 'Conveniences', function() {
it( '$uper.$ === jQuery', function() {
assert( $uper.$ === $, 'jQueery not goodie' );
} );
it( '$uper._ === _', function() {
assert( $uper._ === _, 'underscore not goodie' );
} );
it( '$uper.Handlebars === Handlebars', function() {
assert( $uper.Handlebars === Handlebars, 'Handlebars not goodie' );
} );
it( '$uper.Modernizr === Modernizr', function() {
assert( $uper.Modernizr === Modernizr, 'Modernizr not goodie' );
} );
} );
} );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment