Created
March 29, 2012 22:22
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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; | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Version .02 alpha of a Backbone.js based app for mentorship requests at StudentMentor.org. Some of the key features are:
The entry point for the app is via index_request.js. Some less than interesting template and js files are not included.