I just started on learning BackboneJS and I'm doing a small sample document to test it's way of working. But I have a hard time understanding it and at this moment it doesn't make any sense at all :)
So, I have the next file structure :
/* application.js */
var Application = Application || {};
;(function ( $, window, document, undefined ) {
new Application.ApplicationView();
})( jQuery, window, document );
/* sample.js */
var Application = Application || {};
;(function ( $, window, document, undefined ) {
Application.ApplicationView = Backbone.View.extend({
});
})( jQuery, window, document );
/* utilities.js */
var Application = Application || {};
;(function ( $, window, document, undefined ) {
Application.ApplicationUtilities = Backbone.Collection.extend({
polyfillize : function(tests) {
return _.each(tests, function(obj){
console.log(obj);
Modernizr.load([{
test : _.reduce(obj.test, function(initial_value, current_value){
return initial_value && current_value;
}, 1),
nope : obj.polyfill,
callback: function(url, result, key) {
console.log('Polyfill : ', [ url ]);
}
}]);
});
}
});
})( jQuery, window, document );
/* options.js */
var Application = Application || {};
;(function ( $, window, document, undefined ) {
Application.ApplicationOptions = Backbone.Model.extend({
version : [ 1 ],
prettify : true,
libraries : {
google_code_prettyfier : 'assets/js/libraries/google/prettify.js'
},
dependencies : {
google_code_prettyfier : function() {
return Modernizr.load([{
test : this.prettify,
yep : [ this.libraries.google_code_prettyfier ],
callback: function(url, result, key) {
return window.prettyPrint && prettyPrint();
}
}]);
}
},
polyfills : {
json_polyfill_url : 'http://cdn.chaoscod3r.com/code/polyfills/json3_polyfill.js',
storage_polyfill_url : 'http://cdn.chaoscod3r.com/code/polyfills/localstorage_polyfill.js'
}
});
})( jQuery, window, document );
/* polyfills.js */
var Application = Application || {};
;(function ( $, window, document, undefined ) {
Application.ApplicationPolyfills = Backbone.Model.extend({
loadPolyfills : function() {
return Application.ApplicationUtilities.polyfillize([
{
test : [Modernizr.localstorage, Modernizr.sessionstorage],
polyfill : [Application.ApplicationOptions.polyfills.storage_polyfill_url]
},
{
test : [Modernizr.json],
polyfill : [Application.ApplicationOptions.polyfills.json_polyfill_url]
}
]);
}
});
})( jQuery, window, document );
Each model it's placed in it's own models folder, each collection in it's collections folder and so on. Then I'm loading all the script in the DOM, first the collections, then the models, the views and last the application.js.
What I want to do is be able to use the collection as a collection of functions that I can use whenever I want in the Views or the Models, the options should be available on any level and in any of the collections / models / views. So it would be like my sample.js file will contain the main View which will initialize everything that needs to run onDOM load like loading all the polyfills if necessary, enabling the google code prettifier and so on.
Obviously nothing works at this state, so I would like someone more experienced to help me out if possible or maybe point to some tutorials that match what I'm trying to do right now :)
You seem to be a little confused about what Backbone collections are all about.
A collection in Backbone is a collection of (Backbone) models:
Backbone.Collection
Collections are ordered sets of models. [...]
When you say this:
var C = Backbone.Collection.extend({
method: function() { ... }
});
you're creating a "class" and method will be available as an "instance method" after you create a new instance but you can't call method directly on C because extend puts method on C.prototype for inheritance:
C.method(); // This doesn't work, you'll just get an error.
var c = new C;
c.method(); // This works
You say that
it made sense to have it in a collection, a collection of utility functions :)
but, unfortunately, just because something makes sense doesn't mean that it really is sensible :)
You don't have any models for Application.ApplicationUtilities so it isn't a collection in the Backbone sense. I think you just want to use an object as a namespace:
Application.ApplicationUtilities = {
polyfillize: function(tests) { /* ... */ }
};
Related
BRIEF:
I write my custom backbone editor, but Backbone can't initialize it because can't find schema for it. Backbone looks for schema in Form.editors array at backbone-forms.js? How can I register schema of my custom editor?
DETAILED:
I use Backbone Forms which are initialized in the next way:
backbone-forms.js
var Form = Backbone.View.extend({
initialize: function(options) {
//.....
//Check which fields will be included (defaults to all)
var selectedFields = this.selectedFields = options.fields || _.keys(schema);
_.each(selectedFields, function(key) {
var fieldSchema = schema[key];
fields[key] = this.createField(key, fieldSchema); // <==== Here troubles begins
}, this);
},
//....
}, {
//....
editors: {} // <===== QUESTION: where I should put my custom editor in this array???
});
Problem: When Backbone creating new Form it calls createSchema method which looks like:
createSchema: function(schema) {
//........
//PROBLEM: Form.editors[schema.type] is undefined
schema.type = (_.isString(schema.type)) ? Form.editors[schema.type] : schema.type;
return schema;
}
and Form.editors[schema.type] is undefined. That means I can't create/render my custom editor!
Question: Where/How I can register my custom editor in Form.editors array?
You can reference the custom editor directly from within the schema, i.e. reference the function itself rather than a string:
var CustomEditor = Backbone.Form.editors.Base.extend({});
var form = new Backbone.Form({
schema: {
customField: { type: CustomEditor }
}
});
Or, like you found, you can also add the editor to Backbone.Form.editors so you can use the string as a shortcut.
Our shop just recently adopted Backbone, and even gave me the green light to try out Marionette. However the latter is not giving me the bang for the buck I'd hoped for (possibly due to my ignorance), so instead I'm trying to devise a lightweight dependency injection system where all my Backbone views get an instance of an "Application" object of my own devising.
My system is based on a (necro) answer I posted yesterday and below is code that is working for me. However I don't want to have duplicate blocks of code for each view inside the injectViewsWithApp function, but rather would like to just loop over the views. But this is where I can't figure out how to get things working inside the loop, I think it's some sort of scoping issue.
I will continue working on it so I may post updates to the code during my remaining hour before my weekend begins.
define(['backbone', './app/views/searchform', './app/views/searchresults', './app/views/FooBardetailrow'],
function(Backbone, SearchFormView, SearchResultsView, FooBarDetailRowView) {
var APP = initApp();
injectViewsWithApp();
launchApp();
function initApp() {
return {
collections : { // No need for separate files/modules if each view gets injected with the APP
extendedHaulFooBars : new (Backbone.Collection.extend({})),
stn333sts : new (Backbone.Collection.extend({})),
FooBarTypes : new (Backbone.Collection.extend({})),
FooBarSymbols : new (Backbone.Collection.extend({})),
FooBarSearchParams : new (Backbone.Collection.extend({}))
},
defaultModel : Backbone.Model.extend({}),
views : {}
};
}
function injectViewsWithApp() {
SearchFormView.instantiator = SearchFormView.extend({
initialize : function() {
this.app = APP;
SearchFormView.prototype.initialize.apply(this, arguments);
}
});
SearchResultsView.instantiator = SearchResultsView.extend({
initialize : function() {
this.app = APP;
SearchResultsView.prototype.initialize.apply(this, arguments);
}
});
FooBarDetailRowView.instantiator = FooBarDetailRowView.extend({
initialize : function() {
this.app = APP;
FooBarDetailRowView.prototype.initialize.apply(this, arguments);
}
});
}
I've simplified things and gotten the following to "work". However, I'd like to assign view.instantiator back to view, and this I cannot get to work :( Definitely will accept somebody else's answer if they can make it so that in the end I can simply call "new MyView()" and get access to APP/app -- otherwise the following is not so bad I suppose.
var APP = {foo : 'bar'};
var MyView = Backbone.View.extend({});
var views = [MyView];
for (var i=views.length; i--; ) {
var view = views[i];
view.instantiator = view.extend({
initialize : function() {
this.app = APP;
view.prototype.initialize.apply(this, arguments);
}
});
console.log(new view.instantiator().app.foo);
}
console.log(new MyView.instantiator().app.foo);
// OUTPUT: 'bar' times 2
Here's another arguably better way; inheritance is even possible, as demonstrated, and this is particularly important in my case. Thanks to this fine answer as to how to retain the value of 'app' across calls to the ParentView and ChildView modules.
define(['backbone'],
function(Backbone) {
var APP = {
foo : 'bar'
};
var MyParentView = (function() {
var app;
return function() {
return {
ctor : Backbone.View.extend({
initialize : function() {
this.app = app;
}
}),
inject : function(_app) {app = _app;}
}
}
})();
var MyChildView = (function() {
var app;
return function() {
return {
ctor : MyParentView().ctor.extend({
initialize : function() {
console.log(this.app); // OUTPUT: undefined
MyParentView().ctor.prototype.initialize.apply(this, arguments);
console.log(this.app.foo); // OUTPUT: bar
}
}),
inject : function(_app) {app = _app;}
}
}
})();
var injectableViews = [MyParentView, MyChildView];
for (var i = injectableViews.length; i--; ) {
injectableViews[i]().inject(APP);
}
new (MyChildView().ctor)();
}
);
I'm currently using Backbone + RequireJS.
In my application, I display a tree widget that is constructed with the same Model with nested Collections.
That is to say:
FooCollection
define(['backbone', 'models/foo'], function(Backbone, FooModel) {
var FooCollection = Backbone.Collection.extend({
model: FooModel
});
return FooCollection;
});
FooModel
define(['backbone', 'underscore'], function(Backbone, _) {
var FooModel = Backbone.Model.extend({
initialize : function() {
_.bindAll(this, 'adoptOne', 'adoptAll');
var self = this;
// Need to do it this way or RequireJS won't find it
require(['collections/foos'], function(FooCollection) {
self.foos = new FooCollection();
self.on('change:foos', function() {
self.foos.reset(self.get('foos'));
});
self.foos.on('reset', self.adoptAll);
self.foos.on('add', self.adoptOne);
self.foos.reset(self.get('foos');
});
},
adoptAll : function() {
this.foos.each(this.adoptOne);
},
adoptOne : function(foo) {
foo.parent = this;
}
});
return FooModel;
});
The above works. I don't get any errors and everything is constructed as expected.
However...
// In a view
this.foos = new FooCollection();
this.foos.fetch({
success : function(foos) {
var treeView = new TreeView();
treeView.render(foos); // Doesn't work!!
}
});
The above doesn't work because of a sync problem: the TreeView gets rendered before the nested collections have finished creating (either because it takes longer to run the code or because it takes time to load 'collections/foos'.
Either way, I can fix it with this:
setTimeout(function() {
treeView.render(foos);
}, 100);
But that, of course, it's just a hack. In a production environment it could take more than 100 miliseconds and the code wouldn't work.
So, I guess that what I should do is to trigger some sort of event that my view listens to. However, my question to y'all is the following: when do I know that the entire collection of foos have been constructed and where do I attach the listener?
Thanks in advance!!
I have a single JS file that I use across the whole of my app. The top of it looks like this:
$(document).ready( function() {
if( $('#element-a').length ) {
var featureA = new ElementAFeature( $('#element-a') );
}
if( $('#element-b').length ) {
var featureB = new ElementBFeature( $('#element-b') );
}
// repeat ad nauseam for elements C thru Z etc etc
});
// actual objects and logic go here
It works, but it's sort of ugly. Short of running different scripts on different pages, is there any way of tidying this up?
In each page do something like this
window.MYAPP = window.MYAPP || {};
window.MYAPP.element = $("page-element");
window.MYAPP.feature = new ElementXFeature(window.MYAPP.element);
then modify your init script to
$(document).ready( function() {
var feature = window.MYAPP.feature;
//Use feature here.
});
If you are writing a lot of specific init code for each page you might wanna consider having both a global init method and defining a local one for each page and pass any context needed from the global init.
window.MYAPP.initMethod = function(context) {}
//in global init
if (typeof window.MYAPP.initMethod === "function") {
window.MYAPP.initMethod({ pageSpecificSetting : 0});
}
I am want to build a plugin with accessible methods and options, this for a complex plugin.
I need the methods to be accessible outside the plugin because if somebody ads something to the DOM it needs to be updated(so we dont need the run the complete plugin again).
I have seen in the past that there are plugin that do it like this, but I cant find them, so I cant take a look at them. I am still new to javascript so any help would be nice.
It would be nice if we still can globally override the options.
How I want to use the plugin:
// options
$('#someid').myplugin({name: 'hello world'});
// methods(would be nice if we can use this)
$('#someid').myplugin('update');
// my old plugin wrapper
;(function($, window, document, undefined){
$.fn.pluginmyPlugin = function(options) {
options = $.extend({}, $.fn.pluginmyPlugin.options, options);
return this.each(function() {
var obj = $(this);
// the code
});
};
/**
* Default settings(dont change).
* You can globally override these options
* by using $.fn.pluginName.key = 'value';
**/
$.fn.pluginmyPlugin.options = {
name: '',
...
};
})(jQuery, window, document);
Update
So after looking at the jQuery docs I have build the following code, please let me know if there's something wrong with the code, if it can be build better...
;(function($, window, document, undefined){
var methods = {
init : function( options ) {
options = $.extend({}, $.fn.pluginmyPlugin.options, options);
return this.each(function(){
alert('yes i am the main code')
});
},
update : function( ) {
alert('updated')
}
};
$.fn.pluginmyPlugin = function(method) {
if ( methods[method] ) {
return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist on this plugin' );
}
};
/**
* Default settings(dont change).
* You can globally override these options
* by using $.fn.pluginName.key = 'value';
**/
$.fn.pluginmyPlugin.options = {
name: 'john doe',
//....
};
})(jQuery, window, document);
An alternative:
var Plugin = function($self, options) {
this.$self = $self;
this.options = $.extend({}, $.fn.plugin.defaults, options);
};
Plugin.prototype.display = function(){
console.debug("Plugin.display");
};
Plugin.prototype.update = function() {
console.debug("Plugin.update");
};
$.fn.plugin = function(option) {
var options = typeof option == "object" && option;
return this.each(function() {
var $this = $(this);
var $plugin = $this.data("plugin");
if(!$plugin) {
$plugin = new Plugin($this, options);
$this.data("plugin", $plugin);
}
if (typeof option == 'string') {
$plugin[option]();
} else {
$plugin.display();
}
});
};
$.fn.plugin.defaults = {
propname: "propdefault"
};
Usage:
$("span").plugin({
propname: "propvalue"
});
$("span").plugin("update");
This absurdly resembles the Twitter Bootstrap's JavaScript template. But, it wasn't completely taking from there. I have a long history of using .data().
Don't forget to wrap it appropriately.
If you want to use the plugin like this:
// Init plugin
$('a').myplugin({
color: 'blue'
});
// Call the changeBG method
$('a').myplugin('changeBG')
// chaining
.each(function () {
// call the get method href()
console.log( $(this).myplugin('href') );
});
or if you need independent Plugin instance per element:
$('a').each(function () {
$(this).myplugin();
});
you will want to setup your plugin like this:
/*
* Project:
* Description:
* Author:
* License:
*/
// the semi-colon before function invocation is a safety net against concatenated
// scripts and/or other plugins which may not be closed properly.
;(function ( $, window, document, undefined ) {
// undefined is used here as the undefined global variable in ECMAScript 3 is
// mutable (ie. it can be changed by someone else). undefined isn't really being
// passed in so we can ensure the value of it is truly undefined. In ES5, undefined
// can no longer be modified.
// window is passed through as local variable rather than global
// as this (slightly) quickens the resolution process and can be more efficiently
// minified (especially when both are regularly referenced in your plugin).
var pluginName = "myplugin",
// the name of using in .data()
dataPlugin = "plugin_" + pluginName,
// default options
defaults = {
color: "black"
};
function privateMethod () {
console.log("private method");
}
// The actual plugin constructor
function Plugin() {
/*
* Plugin instantiation
*
* You already can access element here
* using this.element
*/
this.options = $.extend( {}, defaults );
}
Plugin.prototype = {
init: function ( options ) {
// extend options ( http://api.jquery.com/jQuery.extend/ )
$.extend( this.options, options );
/*
* Place initialization logic here
*/
this.element.css( 'color', 'red' );
},
destroy: function () {
// unset Plugin data instance
this.element.data( dataPlugin, null );
},
// public get method
href: function () {
return this.element.attr( 'href' );
},
// public chaining method
changeBG: function ( color = null ) {
color = color || this.options['color'];
return this.element.each(function () {
// .css() doesn't need .each(), here just for example
$(this).css( 'background', color );
});
}
}
/*
* Plugin wrapper, preventing against multiple instantiations and
* allowing any public function to be called via the jQuery plugin,
* e.g. $(element).pluginName('functionName', arg1, arg2, ...)
*/
$.fn[pluginName] = function ( arg ) {
var args, instance;
// only allow the plugin to be instantiated once
if (!( this.data( dataPlugin ) instanceof Plugin )) {
// if no instance, create one
this.data( dataPlugin, new Plugin( this ) );
}
instance = this.data( dataPlugin );
/*
* because this boilerplate support multiple elements
* using same Plugin instance, so element should set here
*/
instance.element = this;
// Is the first parameter an object (arg), or was omitted,
// call Plugin.init( arg )
if (typeof arg === 'undefined' || typeof arg === 'object') {
if ( typeof instance['init'] === 'function' ) {
instance.init( arg );
}
// checks that the requested public method exists
} else if ( typeof arg === 'string' && typeof instance[arg] === 'function' ) {
// copy arguments & remove function name
args = Array.prototype.slice.call( arguments, 1 );
// call the method
return instance[arg].apply( instance, args );
} else {
$.error('Method ' + arg + ' does not exist on jQuery.' + pluginName);
}
};
}(jQuery, window, document));
Notes:
This boilerplate will not use .each() for every method call, you
should use .each() when you need
Allow re-init plugin, but only 1
instance will be created
Example of destroy method is included
Ref: https://github.com/jquery-boilerplate/jquery-boilerplate/wiki/jQuery-boilerplate-and-demo
Have you tried the jQuery UI Widget Factory?
There was a bit of a learning curve, but I love it now, handle the options, and defaults and allows methods, keeps everything wrapped up tight very fancy :)
EDIT
The jQuery UI Widget Factory is a separate component of the jQuery UI Library that provides an easy, object oriented way to create stateful jQuery plugins.
– Introduction to Stateful Plugins and the Widget Factory.
I don't think the extra overhead is much to be worried about in most circumstances. These days I write in coffescript and everything is compiled, minified and gzipped, so a few extra lines here and there doesn't make much of a difference. My research in website speed seems to indicate that the number of HTTP requests is a big deal - indeed the former colleague that put me on this track worked for a browser based game and was all about speed speed speed.
Here is the latest boilerplate https://github.com/techlab/jquery-plugin-boilerplate
Also you can use the create-jquery-plugin npm CLI utility. just run
npx create-jquery-plugin