Skip to content

Instantly share code, notes, and snippets.

@dgs700
Created March 29, 2012 22:22
Show Gist options
  • Save dgs700/2244334 to your computer and use it in GitHub Desktop.
Save dgs700/2244334 to your computer and use it in GitHub Desktop.
Version .02 alpha of a Backbone.js based app for mentorship requests at StudentMentor.org. Some of the key features are: - dependancy management and loading via Require.js - nested / deep models via Backbone-relational.ja - complete separation of Javascri
//page level code that is the entry point for the application
//context specific config object is built including all css and html strings
//contained in logical groupings, and injected into a router object.
require(['sm_lib/sm.config'], function() {
require(['apps/requestApp'], function(requestApp){
var Request = {};
// for coherant organization of these config objects follow the pattern;
// uiView: {
// classVar: '', i.e. templateId
// classVar: '',
// css: {}, i.e. class or id strings to be inserted in markup
// events: {}, i.e. 'event selector' strings
// selectors: {}, i.e. classes, ids, elems or combination used for jquery wrapping - with '#', '.' etc.
// text: {}, i.e. text strings to be inserted in markup // all other markup should reside in templates
// uiChildView: { i.e. this might be a 'row' in a 'list'
// classVar: '',
// classVar: '',
// css: {},
// events: {},
// selectors: {},
// text: {},
// subUiView: {},
// uiChildView: {}
// },
// subUiView: {and conf specific to a sub class}
Request.config = {
router: {
routes: {},
selectors: {
domAttachClass: '.user_right_main_content',
listSectionId: '#requests-section',
rootPageElem: 'body',
categoryKeyId: '#category-key-0'
}
},
history: {
root: '/home/',
pushState: true
},
requestItem: {
urlRoot: '/api/requests/'
},
requestCollection: {
url: '/api/requests/',
item: {}
},
headerView: {
tagName: 'section',
className: null,
id: 'requests-section',
templateId: '#request-list-header-tpl',
events: {
createEvtTarget: 'click #create_request_btn'
},
selectors: {},
text: {}
},
listView: {
tagName: null,
className: null,
id: null,
templateId: null,
rowView: {
tagName: 'article',
className: 'my_request_row notepaper_bg',
id: null,
templateId: '#request-row-tpl',
css: {
expandBttnClass: 'btn_request_expand',
retractBttnClass: 'btn_request_excerpt',
retractClass: 'retract',
expandClass: 'expand'
},
events: {
expandEvtTarget: 'click .expand',
retractEvtTarget: 'click .retract',
updateEvtTarget: 'click .update',
deactivateEvtTarget: 'click .deactivate',
reactivateEvtTarget: 'click .reactivate'
},
selectors: {
descriptionId: '#description_',
expandId: '#expand_'
},
text: {
expandTitle: 'Show mentorship request details.',
retractTitle: 'Hide mentorship request details.'
}
}
},
createView: {
tagName: 'div',
className: 'user_modal',
id: 'request-update-modal',
templateId: '#request-update-tpl',
css: {
backgroundId: 'request-modal-bg',
backgroundClass: 'user_modal_bg',
categoryKeyClass: 'category-key-'
},
events: {
closeEvtTarget: 'click #close-button-id',
saveEvtTarget: 'submit #request_form'
},
selectors: {
formId: '#request_form',
submitId: '#request-mentor-button',
domAttachElem: 'body',
backgroundId: '#request-modal-bg',
backgroundClass: '.user_modal_bg',
titleId: '#request-title',
descriptionId: '#request-description',
categoryWrapClass: '.mentoring_cat_wrap',
categoryKeyElem: 'input[id^="category-key"]',
categoryTitleClass: '.category-title',
flashElem: '.user_right_main_content > section'
},
text: {
flashTitle: 'Mentorship request created successfully.',
flashMessage: 'Click "Find Mentors" to display your matches.'
},
// include compatable config opts obj for jquery.validate
validationConfig: {
//debug: true,
ignore: null, // default is ignore :hidden but we need to validate hidden fields
onkeyup: false,
errorPlacement: function(error, element){
if(element.attr('id').indexOf('category-key-') != -1) {
element.parent().parent().parent().append(error);
} else {
element.parent().append(error);
}
},
rules: {
'category-key-0':{
required:true
},
'request-description':{
required:true,
minlength:200
},
'request-title':{
required:true,
maxlength:100
}
}
},
categoryListView: {
tagName: 'div',
className: 'mentoring_cat_wrap',
id: 'clone-view-0',
templateId: '#category-list-tpl',
css: {
categoriesId: 'categories_menu0'
},
events: {
addEvtTarget: 'click #add-button-id'
},
selectors: {
categoriesTemplateId: '#mentoring_cats_template',
rootPageElem: 'body',
tax_idId: '#mentoring-cat-0',
cat_titleId: '#cat-title-0',
categoriesMenuId: '#categories_menu0',
categoriesKeyId: '#category-key-',
categoryCloneClass: '.mentoring_cat_wrap',
mcDropdownclass: '.mcdropdown_menu'
},
text: {},
categoryCloneView: {
tagName: 'div',
className: 'mentoring_cat_wrap',
id: 'clone-view-',
templateId: '#category-clone-tpl',
css: {
categoriesId: 'categories_menu'
},
events: {
remove: 'click .mentoring_cat_rm'
},
selectors: {
rootPageElem: 'body',
categoriesTemplateId: '#mentoring_cats_template',
categoriesMenuId: '#categories_menu',
categoriesKeyId: '#category-key-'
},
text: {}
}
}
},
updateView: {},
paginatorView: {
tagName: 'div',
className: 'paginator',
id: null,
templateId: '#request-paginator-tpl',
css: {},
events: {
gotoFirstEvtTarget: 'click a.first',
gotoPrevEvtTarget: 'click a.prev',
gotoNextEvtTarget: 'click a.next',
gotoLastEvtTarget: 'click a.last',
gotoPageEvtTarget: 'click a.page',
changeCountEvtTarget: 'click .howmany a',
sortByAscendingEvtTarget: 'click a.sortAsc',
sortByDescendingEvtTarget: 'click a.sortDsc'
},
selectors: {
sortByOptionId: '#sortByOption'
},
text: {}
}
};
// bootstrap the controller and config data
window.SM.Request.app = new requestApp.Router({
config: Request.config
});
Backbone.emulateHTTP = true;
Backbone.emulateJSON = true;
Backbone.history.start(Request.config.history);
});
});
//here we define all models and associated collections necessary to run
//this portion of the application
define([
'order!underscore',
'order!Backbone',
'order!Backbone_relational',
'order!Backbone_paginator'
],
function RequestModel(){
var Request = {};
if(_.isUndefined(window.SM))
window.SM = {};
if(_.isUndefined(window.SM.Request))
window.SM.Request = {};
window.SM.Request.Item = Request.Item = Backbone.RelationalModel.extend({
config: {},
urlRoot: "/api/requests/",
defaults:{
//"user_id": 1,
"title": "the title",
"date": '', // set on server
"description": "the long description",
"excerpt": "",
"status": 0,
"categories": [
{
"taxonomy_id": "" // "taxonomy_id" field in zend
}
]
},
//this allows for automatic model creation on a bootstrap or fetch
relations: [{
type: Backbone.HasMany,
key: 'categories',
relatedModel: 'SM.Request.CategoryItem',
collectionType: 'SM.Request.CategoryCollection'
}],
initialize: function(){
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.requestItem);
},
saveRequest: function(model, data, collection){
var response = {};
//model.set(data, {
// silent:true
//});
if(model.isNew()) {
// for now, just returning new id attr, but should return the whole model in json
window.SM.Request.response = model.save(data,{ //debug
silent: true,
success: function(model, response){
model.set(response); // add in server set fields- id, date
collection.add(model, {
at:0
}); //trigger add evt to render new row
//collection.pager('date', 'desc');
},
// need server to return non-200 code and error text in json
error: function(model, response){
//probably do the "flash" thing
}
});
} else {
// triggers a change and then a sync event
window.SM.Request.response = model.save(data,{ //debug
silent: true,
success: function(model, response){
//need to trigger change evt, rerender from here
model.change(); // fire a change evt
},
error: function(model, response){}
});
}
// should contain any error or success messages back to the view for display
return response;
}
});
window.SM.Request.Collection = Request.Collection = Backbone.Paginator.clientPager.extend({
config: {},
model: window.SM.Request.Item,
url: '/api/requests/', //decouple
//paginator specific settngs
displayPerPage: 5,
originalDPP: 5,
dppHasChanged: false,
page: 1,
sortDirection: 'desc',
sortField: 'date',
//only needed for building a query string
//perPageAttribute: '',
//skipAttribute: '',
//orderAttribute: '',
//customAttribute1: '',
//queryAttribute: '',
//formatAttribute: '',
//customAttribute2: '',
//perPage: 1000, // set to a high number to get everything
//query: '',
//format: 'json',
//customParam1: '',
//customParam2: '',
//not really needed unless the response includes some metadata that
// we need to grab or get rid of
parse: function (response) {
var requests = response;
//this.totalPages = response.d.__count;
return requests;
},
initialize: function(){
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.requestCollection);
}
});
//backbone relational breaks if related models can't be refeerenced from the window
window.SM.Request.CategoryItem = Request.CategoryItem = Backbone.RelationalModel.extend({
urlRoot: "/api/requests/",
// "mentorship_request_id" is set on server
defaults:{
"taxonomy_id": ""
}
});
window.SM.Request.CategoryCollection = Request.CategoryCollection = Backbone.Collection.extend({
model: window.SM.Request.CategoryItem,
url: '/api/requests/' //decouple
});
return Request;
});
// generic paginator view functions and bindings
// code here lifted from Adi Osmani's client example
define(['order!underscore', 'order!Backbone'], function PaginatorView(){
var PaginatorView = Backbone.View.extend({
config: {},
//this should be overridden
initialize: function(){
//if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.paginatorView);
//if(typeof this.tmpl != 'function')
// PaginatorView.prototype.tmpl = _.template( $(this.config.templateId).html() );
//_.bindAll(this, 'render' );
this.collection.on('reset', this.render, this);
this.collection.on('change', this.render, this);
},
render: function () {
var html = this.tmpl(this.collection.info());
this.$el.html(html);
return this;
},
events: function(){
var eventHash = {},
evtConf = this.config.events;
eventHash[evtConf.gotoFirstEvtTarget] = '_gotoFirst';
eventHash[evtConf.gotoPrevEvtTarget] = '_gotoPrev';
eventHash[evtConf.gotoNextEvtTarget] = '_gotoNext';
eventHash[evtConf.gotoLastEvtTarget] = '_gotoLast';
eventHash[evtConf.gotoPageEvtTarget] = '_gotoPage';
eventHash[evtConf.changeCountEvtTarget] = '_changeCount';
eventHash[evtConf.sortByAscendingEvtTarget] = '_sortByAscending';
eventHash[evtConf.sortByDescendingEvtTarget] = '_sortByDescending';
return eventHash;
},
_gotoFirst: function (e) {
e.preventDefault();
this.collection.goTo(1);
},
_gotoPrev: function (e) {
e.preventDefault();
this.collection.previousPage();
},
_gotoNext: function (e) {
e.preventDefault();
this.collection.nextPage();
},
_gotoLast: function (e) {
e.preventDefault();
this.collection.goTo(this.collection.information.lastPage);
},
_gotoPage: function (e) {
e.preventDefault();
var page = $(e.target).text();
this.collection.goTo(page);
},
_changeCount: function (e) {
e.preventDefault();
var per = $(e.target).text();
this.collection.howManyPer(per);
},
_sortByAscending: function (e) {
e.preventDefault();
var currentSort = this._getSortOption();
this.collection.pager(currentSort, 'asc');
this._preserveSortOption(currentSort);
},
_getSortOption: function () {
return $(this.config.selectors.sortByOptionId).val();
},
_preserveSortOption: function (option) {
$(this.config.selectors.sortByOptionId).val(option); // decouple
},
_sortByDescending: function (e) {
e.preventDefault();
var currentSort = this._getSortOption();
this.collection.pager(currentSort, 'desc');
this._preserveSortOption(currentSort);
}
});
return PaginatorView;
});
// here we define the application router class which also acts in a mediator
// capacity to reduce, where possible dependencies in model and view classes
define(['order!underscore',
'order!Backbone',
'validate',
'SM_request_model',
'SM_request_view'
],
function (und, bac, validate, Request, RequestView){
// some important conventions for maintainability and portability:
// 1. model and view classes should have minimal outside dependancies
// the primary exception being collections and container/list views
// 2. model and view classes should handle their specific tasks and nothing else
// 3. the router class should handle any inter-class app logic and act as the controller
// and handle any config / data bootstrapping plus dom intereaction
// 4. there should be absolutely no hard css, html, or dom dependancies anywhere
// in the backbone classes- use templates and config objects
var RequestApp = {};
// Route app urls to controller fucntions
//
RequestApp.Router = Backbone.Router.extend({
// todo: move this to the config obj to remove hard depndancies between this and view classes
routes:{
'': 'list',
'create': 'setRequest',
'update/:id': 'setRequest'
},
initialize:function (options) {
var atts = {};
if(!_.isUndefined(options)){
this.appConfig = options.config;
this.config = options.config.router;
atts = options.config.headerView;
}
_.bindAll(this, 'list', 'setRequest');
this.requestHeaderView = new RequestView.Header({
requestApp: this,
tagName: atts.tagName,
id: atts.id
});
$(this.config.selectors.domAttachClass).append(this.requestHeaderView.render().el);
},
//bootstrap data on initial load
//create new collection, add models, create row views and add to
//list view
//this should only be called once on page load
list: function() {
var atts = this.appConfig.headerView;
this.requestCollection = new Request.Collection();
// this.requestListView = new SM.RequestListView({
// model: this.requestCollection,
// requestApp: this
//
// });
// temporary fetch till data is bootstrapped w/ page
this.requestPaginatorView = new RequestView.Paginator({
collection: this.requestCollection,
requestApp: this,
className: this.appConfig.paginatorView.className
});
var self = this;
this.requestCollection.fetch({
success: function(collection, response){
//if we were to fetch instead of bootstrap, this is the place to kick off rendering
//window.SM.requestCollection = collection; // debug
//window.SM.response = response; // debug
collection.pager();
self.requestListView = new RequestView.List({
model: collection,
requestApp: self
});
$(self.config.selectors.listSectionId).append( self.requestListView.render().el );
$(self.config.selectors.listSectionId).append( self.requestPaginatorView.render().el );
},
error: function(collection, response){}
});
//_.each(window.SM.requests, function(item){ // bootstrap requests data
// var requestItem = new SM.RequestItem(item, {urlRoot: SM.appConf.requestItem.urlRoot});
// this.requestCollection.add(requestItem);
//}, this);
//alert('stop');
//window.SM.requestCollection = this.requestCollection.toJSON(); //for debug - these reference the same live data objs
//$(this.config.selectors.listSectionId).append( this.requestListView.render().el );
},
// grab existing or new request model and render the approptiate
// create / update view
setRequest: function(id) {
if(this.requestSetView) this.requestSetView = null;
var selectors = this.config.selectors;
var atts = this.appConfig.createView;
var options = {
requestApp: this,
id: atts.id,
className: atts.className
};
if(_.isUndefined(id)){ // new request
options.model = new Request.Item({
urlRoot: this.appConfig.requestItem.urlRoot
});
this.requestSetView = new RequestView.Create(options).render().showModalBackground();
} else { // existing request
options.model = this.requestCollection.get(id);
this.requestSetView = new RequestView.Update(options).render().showModalBackground();
}
$(selectors.rootPageElem).prepend(this.requestSetView.el);
this.requestSetView.validate(); // returns a validator obj
$(selectors.categoryKeyId).rules("add", {
required: true
});
}
});
return RequestApp;
});
<!-- erb style template for use with Underscore.template -->
<div class="my_request_actions_wrap">
<div class="my_request_expand_wrap">
<a class="btn_request_expand expand" id="expand_<%= id %>" title="Show mentorship request details"></a>
</div>
<div class="my_request_edit_wrap">
<a class="btn_request_edit update" title="Edit this mentorship request"></a>
</div>
<% if(status == 0) { %>
<div class="my_request_close_wrap">
<a class="btn_request_hide deactivate" title="Deactivate Mentorship Request"></a>
</div>
<a class="btn_mentors_find btn_purple" href="/mentorship/request/view/<%= id %>" title="Find Mentors">Find Mentors</a>
<% } else { %>
<a class="btn_mentors_find btn_purple reactivate" title="Reactivate" href="/mentorship/request/unhide/<%= id %>"> Reactivate </a>
<% } %>
</div>
<div class="my_request_details_wrap">
<p class="my_request_date"><%= date %></p>
<h2 class="my_request_title"><%= title %></h2>
<div class="content_excerpt">
<p class="my_request_description" id="description_<%= id %>"><%= excerpt %></p>
</div>
</div>
<div class="modal_info_wrap">
<div id="close-button-id" class="btn_modal_close"></div>
<h2><%= action %> Mentorship Request</h2>
<p>Please fill out this form as accurately as possible so you will be able to view the best mentor matches.
You will always have the opportunity to create additional mentorship requests.</p>
</div>
<div class="modal_form_wrap">
<form method="post" action="/mentorship/request/create" enctype="application/x-www-form-urlencoded" id="request_form">
<div class="element_wrap">
<label class="required" for="title">Give your mentoring request a brief title.</label>
<input type="text" maxlength="100" value="<%= title %>" id="request-title" name="request-title">
</div>
<div class="element_wrap">
<label class="required" for="description">Describe your mentoring request in detail, including your goals. Provide specifics to help potential mentors understand your request better.</label>
<textarea cols="80" rows="24" id="request-description" name="request-description" required="true"><%= description %></textarea>
</div>
<div class="request_submit_wrap">
<input type="submit" class="btn_modal_green" value="<%= action %>" id="request-mentor-button" name="request_mentor_button">
</div>
</form>
</div>
// here we define app context specific views, and require in common base views
// and utility views
define([
'order!underscore',
'order!Backbone',
'order!mcdropdown',
'validate',
'sm_widget_flash',
'SM_request_model',
'SM_common_paginator',
'SM_common_fieldclone',
],
function RequestView(und, bac, mcd, validate, flash, Request, PaginatorView, FieldCloneView){
var RequestView = {};
//create a header element with some event targets
RequestView.Header = Backbone.View.extend({
config: {},
initialize:function () {
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.headerView);
_.bindAll(this, 'render', 'events', 'createRequest');
},
events: function(){
var eventHash = {};
eventHash[this.config.events.createEvtTarget] = 'createRequest';
return eventHash;
},
render: function(eventName) {
var template = _.template( $(this.config.templateId).html() );
$(this.el).html(template());
return this;
},
createRequest: function(e) {
e.preventDefault();
this.options.requestApp.navigate("create", true);
return false;
}
});
// parent view of request rows, this model is the collection
// this class doesn't create any of its own elements
// all this class is doing is binding a cllection:add event
// to instantiate rows as models are added to the collection on bootstrap or later
//todo: now that pagination is added- need to make sure all subviews are compeltely removed
//and events unbound
RequestView.List = Backbone.View.extend({
config: {},
initialize: function(){
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.listView);
_.bindAll(this, 'render' );
// triggered on a collection fetch()
this.model.bind("reset", this.render, this);
//add a row view when a model is added to the collection
//triggered for all models on initial bootstrap plus any added later
var self = this;
this.model.bind("add", function(requestItem) {
var rowConf = self.config.rowView;
self.$el.prepend(new RequestView.Row({
model: requestItem,
requestApp: self.options.requestApp,
tagName: rowConf.tagName,
className: rowConf.className,
templateId: rowConf.templateId
}).render().el);
});
},
// we aren't actually creating any ui elements, just managing row views
// called on collection reset (fetch)
render: function(event){
this.$el.empty();
var rowConf = this.config.rowView;
_.each( this.model.models, function(requestItem){
this.$el.append(new RequestView.Row({
model: requestItem,
requestApp: this.options.requestApp,
tagName: rowConf.tagName,
className: rowConf.className,
templateId: rowConf.templateId
}).render().el);
}, this);
return this;
}
});
// primary view for display of individual request data and related action triggers
RequestView.Row = Backbone.View.extend({
config: {},
initialize: function() {
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.listView.rowView);
//set the template func dynamically on first call only for better performance
if(typeof this.tmpl != 'function')
RequestView.Row.prototype.tmpl = _.template( $(this.config.templateId).html() );
_.bindAll(this, 'render', 'expandDetails', 'retractDetails', 'updateRequest', 'setActive' );
this.model.set('excerpt', (this.model.get('description')).substring(0, 60), {
silent: true
});
// re-draw the row when a model attribute is changed
this.model.bind("change", this.render, this);
},
events: function(){
var eventHash = {},
evtConf = this.config.events;
eventHash[evtConf.expandEvtTarget] = 'expandDetails';
eventHash[evtConf.retractEvtTarget] = 'retractDetails';
eventHash[evtConf.updateEvtTarget] = 'updateRequest';
eventHash[evtConf.deactivateEvtTarget] = 'setActive';
eventHash[evtConf.reactivateEvtTarget] = 'setActive';
return eventHash;
},
render: function(event) {
var vars = this.model.toJSON();
this.$el.html(this.tmpl(vars));
return this;
},
setActive: function(e){
e.preventDefault();
var action = e.handleObj.selector.substring(1),
data;
if(action == 'deactivate'){
data = {status: 1}
}else if(action == 'reactivate'){
data = {status: 0}
}
this.model.saveRequest(this.model, data, this.model.collection);
return false;
},
expandDetails: function(e){
e.preventDefault();
var selectors = this.config.selectors,
css = this.config.css;
this.$(selectors.expandId+this.model.id)
.toggleClass(css.expandClass, false)
.toggleClass(css.retractClass, true)
.toggleClass(css.expandBttnClass, false)
.toggleClass(css.retractBttnClass, true)
.attr('title', this.config.text.retractTitle);
this.$(selectors.descriptionId + this.model.id).text(this.model.get('description'));
return false;
},
retractDetails: function(e){
e.preventDefault();
var selectors = this.config.selectors,
css = this.config.css;
this.$(selectors.expandId+this.model.id)
.toggleClass(css.expandClass, true)
.toggleClass(css.retractClass, false)
.toggleClass(css.expandBttnClass, true)
.toggleClass(css.retractBttnClass, false)
.attr('title', this.config.text.expandTitle);
this.$(selectors.descriptionId+this.model.id).text(this.model.get('excerpt'));
return false;
},
updateRequest: function(e){
e.preventDefault();
this.options.requestApp.navigate("update/" + this.model.id, true); // the path shoould be referenced from the conf obj
return false;
}
});
//taken more or less as is from the client paginator example
RequestView.Paginator = PaginatorView.extend({
config: {},
initialize: function(){
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.paginatorView);
if(typeof this.tmpl != 'function')
RequestView.Paginator.prototype.tmpl = _.template( $(this.config.templateId).html() );
_.bindAll(this, 'render' );
this.collection.on('reset', this.render, this);
this.collection.on('change', this.render, this);
}
});
//create a modal form with display data
//has a model, and manages form data for this and all children views
// eventually this should be an extension of a base modal dialog view w/ some kind of generic form view mixed in
// or the other way around
RequestView.Create = Backbone.View.extend({
config: {},
validator: {},
initialize:function () {
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.createView);
//attach template function to prototype so its isn't created with every new instance
if(typeof this.tmpl != 'function')
RequestView.Create.prototype.tmpl = _.template( $(this.config.templateId).html() );
//this.id = this.config.id;
//this.className = this.config.className;
//so we are not passing in live models, all the real model manipulation should happen at this level
this.categoryCollection = new Request.CategoryCollection(this.model.get('categories').toArray());
var listConf = this.config.categoryListView;
this.categoryListView = new RequestView.CategoryList({ //don't pass in live data
model: this.categoryCollection,
requestApp: this.options.requestApp,
id: listConf.id,
className: listConf.className,
tagName: listConf.tagName
}).render();
_.bindAll(this, 'render', 'close', 'validate', 'submit', 'showModalBackground', 'flashMessage');
},
events: function(){
var eventHash = {},
events = this.config.events;
eventHash[events.closeEvtTarget] = 'close';
eventHash[events.saveEvtTarget] = 'submit';
return eventHash;
},
//this should be called from outside the obj when attaching to DOM
render: function (event) {
var tmplVars = this.model.toJSON();
tmplVars.action = 'Create';
this.$el.html(this.tmpl(tmplVars));
this.$el.css({
'margin-top': $(document).scrollTop() + 100,
'display':'block'
});
this.$(this.config.selectors.formId).prepend(this.categoryListView.el);
return this;
},
// don't have a better place to put this but wanted to get it out of render()
showModalBackground: function(event){
var css = this.config.css,
selectors = this.config.selectors;
$(selectors.domAttachElem).prepend('<div id="' + css.backgroundId + '" class="' + css.backgroundClass + '"></div>');
$(selectors.backgroundId).show();
return this;
},
validate: function(){
this.validator = this.$(this.config.selectors.formId).validate(this.config.validationConfig);
return this.validator;
},
// clean up / reset view and model
// flash mssg, handle server response
// delegates actual saves, changes, etc to model obj
submit: function(evt){
evt.preventDefault();
//need to force validation for IE8 due to their non-standard event behavior
if(!this.validator.form()){
return false;
}
var selectors = this.config.selectors,
text = this.config.text;
var desc = $(selectors.descriptionId).val();
var data = {
title: $(selectors.titleId).val(),
description: desc,
excerpt: desc.substring(0, 60),
categories: []
};
var $cats = this.$(selectors.categoryWrapClass);
this.categoryArray = [];
var self = this;
$cats.each(function(idx, $elem){
var selectors = self.config.selectors;
if($(selectors.categoryKeyElem, $elem).val() == "")
return;
self.categoryArray[idx] = {};
if(!self.model.isNew()){
self.categoryArray[idx].request_id = self.model.id;
}
self.categoryArray[idx].title = $(selectors.categoryTitleClass, $elem).val();
self.categoryArray[idx].taxonomy_id = $(selectors.categoryKeyElem, $elem).val();
});
data.categories = this.categoryArray;
// need response to indicate success / error w/ any error messages
var response = this.model.saveRequest(this.model, data, this.options.requestApp.requestCollection);
this.flashMessage(response);
this.close();
return false; //prevent default form submit
},
flashMessage: function(response){
var selectors = this.config.selectors,
text = this.config.text;
if(response instanceof Object){ //debug
flash.success(
selectors.flashElem,
text.flashTitle,
text.flashMessage
);
}else{
flash.error(); // ???
}
},
//remove bindings, elements, instances, reset counters on this and all children
//return to base url
close: function(){
this.categoryListView.close();
//this.categoryListView.unbind();
this.categoryListView.remove();
this.categoryListView = null;
this.categoryCollection.reset();
$(this.config.selectors.backgroundId).remove();
this.remove();
//this.unbind();
this.options.requestApp.navigate("", false);
return false;
}
});
//extension of create view
RequestView.Update = RequestView.Create.extend({
config: {},
render: function (event) {
var tmplVars = this.model.toJSON();
tmplVars.action = 'Update';
this.$el.html(this.tmpl(tmplVars));
this.$el.css({
'margin-top': $(document).scrollTop() + 100,
'display':'block'
});
this.$(this.config.selectors.formId).prepend(this.categoryListView.el);
this.categoryListView.initCloneViews();
return this;
}
});
// generic
RequestView.FieldList = Backbone.View.extend({
config: {},
fieldViewArray: [], //array of cloned form fields that this view manages
fieldViewCount: 0, //array length for above, for numbering field ids, etc
initialize: function(){
//_.bindAll(this, 'render', 'addField', 'close' );
},
events: function(){
var eventHash = {};
return eventHash;
},
render: function(){
return this;
},
addField: function(){
return this;
},
close: function(){
//this.remove();
//this.unbind();
return false;
}
});
//model = CategoryCollection
// child (not decendent) view of RequestCreateView
// manages display of a dynamic form field list that can grow/shrink, renders initial
// field sets for an array of model data
// that is passed in. this object and sub objects should not manipulate live model data
RequestView.CategoryList = RequestView.FieldList.extend({
config: {},
catViewArray: [],
index: 1,
initialize: function() {
RequestView.FieldList.prototype.initialize.call(this, this.options); //call super - does nothing for now
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.createView.categoryListView);
this.catViewArray = [];
this.index = 1;
if(typeof this.tmpl != 'function')
RequestView.CategoryList.prototype.tmpl = _.template( $(this.config.templateId).html() );
if(typeof this.catsTmpl != 'function')
RequestView.CategoryList.prototype.catsTmpl = _.template( $(this.config.selectors.categoriesTemplateId).html());
_.bindAll(this, 'render', 'initCloneViews', 'addCategory', 'close' );
var cats = this.model.length;
if(cats > 1){
for(var i = 1; i < cats; i++){
var cat = this.model.at(i);
this.catViewArray[this.catViewArray.length] = new RequestView.CategoryClone({
model: cat,
index: this.index,
requestApp: this.options.requestApp,
tagName: this.config.categoryCloneView.tagName,
className: this.config.categoryCloneView.className,
id: this.config.categoryCloneView.id + i,
templateId: this.config.categoryCloneView.templateId
}).render(false);
this.index++;
}
}
},
events: function(){
var eventHash = {};
eventHash[this.config.events.addEvtTarget] = 'addCategory';
return eventHash;
},
// register add bttn click handler
// on click -> add categoy to collection
// render new clone view
render: function(event) {
RequestView.FieldList.prototype.render.call(this, this.config); //call super - does nothing for now
var selectors = this.config.selectors,
css = this.config.css;
var categories = this.catsTmpl({
categoriesId: css.categoriesId
});
$(selectors.rootPageElem).append(categories);
var tmplVars = this.model.at(0).toJSON(); //IE8 bug
tmplVars.index = 0;
$(this.el).html(this.tmpl(tmplVars));
this.$(selectors.categoriesKeyId+'0').mcDropdown(selectors.categoriesMenuId, {
minRows: 20,
change: function(event, value){
$(event.target).valid();
}
});
return this;
},
initCloneViews: function(){
_.each(this.catViewArray, function(view){
$(this.el).after(view.el);
}, this);
return this;
},
addCategory: function(event){
var options = this.config.categoryCloneView;
var selectors = this.config.selectors;
var cloneView = new RequestView.CategoryClone({
id: options.id + this.index,
className: options.className,
templateId: options.templateId,
index: this.index,
requestApp: this.options.requestApp
}).render(true);
this.catViewArray[this.catViewArray.length] = cloneView;
$(selectors.categoryCloneClass).last().after(cloneView.el);
$(selectors.categoriesKeyId + this.index).rules("add", {
required: true
});
this.index++;
return this;
},
close: function(){
RequestView.FieldList.prototype.close.call(this, this.config);
$(this.config.selectors.mcDropdownclass).remove();
_.each(this.catViewArray, function(view){
view.close();
view.unbind();
}, this);
this.catViewArray = [];
return this;
}
});
// creates view for a cloned multi-level select form field
RequestView.CategoryClone = FieldCloneView.extend({
config: {},
initialize: function() {
if(!_.isUndefined(this.options)) $.extend(this.config, this.options.requestApp.appConfig.createView.categoryListView.categoryCloneView);
if(typeof this.tmpl != 'function')
RequestView.CategoryClone.prototype.tmpl = _.template( $(this.config.templateId).html() );
if(typeof this.catsTmpl != 'function')
RequestView.CategoryClone.prototype.catsTmpl = _.template( $(this.config.selectors.categoriesTemplateId).html());
_.bindAll(this, 'render', 'close' );
},
render: function (isNew) {
var idx = this.options.index,
tmplVars = {},
selectors = this.config.selectors,
css = this.config.css;
if(isNew){
tmplVars = {
title: "",
taxonomy_id: ""
};
}else{
tmplVars = this.model.toJSON();
}
tmplVars.index = idx;
this.$el.html(this.tmpl(tmplVars));
var categories = this.catsTmpl({
categoriesId: css.categoriesId + idx
});
$(selectors.rootPageElem).append(categories);
this.$(selectors.categoriesKeyId + idx).mcDropdown(selectors.categoriesMenuId + idx, {
minRows: 20,
change: function(event, value){
$(event.target).valid();
}
});
return this;
}
});
return RequestView;
});
@dgs700
Copy link
Author

dgs700 commented Apr 4, 2012

Version .02 alpha of a Backbone.js based app for mentorship requests at StudentMentor.org. Some of the key features are:

  • dependancy management and loading via Require.js
  • nested / deep models via Backbone-relational.ja
  • complete separation of Javascript, html and css
  • collection pagination with Backbone.paginator.js
  • clean separation between M, V and C layers
    The entry point for the app is via index_request.js. Some less than interesting template and js files are not included.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment