I want to create form fields in a Backbone view based on a Rails model along with Rails model validations, without a template. To accomplish this I used JBuilder like:
1 json.(#recipient, :firstName,
2 :lastName,
3 :street1,
4 :street2,
5 :city,
6 :state,
7 :zip,
8 :phone1,
9 :phone2)
10 json.required Recipient.validators[0].attributes
11 json.fields Recipient.accessible_attributes.reject {|x| x==""}
12 json.states [
13 ['Alabama', 'AL'],
14 ['Alaska', 'AK'],
15 ['Arizona', 'AZ'],
16 ['Arkansas', 'AR'],
...
64 ['Wyoming', 'WY']
65 ]
I'm sure you can see where I'm going with this...
So, the goal would be to iterate over the fields and append to the DOM my inputs, styling accordingly to the model.required array. Naturally the states field is there to dynamically insert options to a select for the states select. The validation is certainly only really performed by the server response on error (don't really care to do it on the client, but I suppose I could) by just reading the errors response.
So the question, I guess would be:
1) Do I just instantiate the Backbone model in the View constructor?
2) would you do that something like:
this.model.fetch({url:'/recipients/new',
success:function(index,model)
{
this.model = model.responseText
}
});
Better way to do this?
I did see powmedia/backbone-forms though it seems like double work for me and other things I would have to work around, and of course, I would have to re-define everything on the client.
If nothing more, at least this would be a point of reference for others with like thoughts (I couldn't find anything about this)
Possible Solution?
coffescript:
1 class Senditbacklater.Models.Recipient extends Backbone.Model
2
3 url: '/recipients'
4
5 initialize: ->
6 $.getJSON('/recipients/new', (data) =>
7 $.each(data, (key,val) =>
8 obj = {}
9 obj[key] = val
10 this.set(obj)
11 )
12 )
Then I should be good to go to create the form in the Views render
Your situation is not special, the only thing you have to do in changing your point of view: you are just dealing with a Model that has to be fetched from the server and rendered in a View.
The rendering is very complex, that's true, you will have to work in your View code to solve it.
You can try something like this:
// code simplified and no tested
var MyForm = Backbone.Model.extend({
url: "/recipients/new"
});
var MyFormView = Backbone.View.extend({
initialize: function(){
this.model.on( "reset", this.render, this );
},
render: function(){
_.each( this.model.fields, function( field ){
this.$el.find( ".fields" ).append( "<label>" + field + "</label><input name=\"" + field + "\" />" );
}, this );
// ... more render logic here
return this;
}
});
var myForm = new MyForm();
var myFormView = new MyFormView({ el: "#form", model: myForm });
This code is depending in an HTML like this:
<form id="form">
<div class="fields"></div>
</form>
Of course this is a very simplified example, there are more elegant solutions like delegating in sub-views for any family of fields but I just wanted to offer you a starting point.
Related
When showing a dropdown composite view collection of around 200 countries my application gets far too slow.
What is the best way to increase performance when dealing with large collections in marionette composite views?
Here is the function in the controller that is very slow to load. It is fast with only the following lines removed:
#layout.shippingCountryRegion.show shippingCountryView
#layout.billingCountryRegion.show billingCountryView
So it appears to be a very slow rendering issue.
Show.Controller =
showProfile: ->
#layout = #getLayoutView()
#layout.on "show", =>
headerView = #getHeaderView()
#layout.headerRegion.show headerView
accessView = #getAccessView()
#layout.accessRegion.show accessView
billingReadmeView = #getBillingReadmeView()
#layout.billingReadmeRegion.show billingReadmeView
billingFieldsView = #getBillingFieldsView()
#layout.billingFieldRegion.show billingFieldsView
shippingReadmeView = #getShippingReadmeView()
#layout.shippingReadmeRegion.show shippingReadmeView
shippingFieldsView = #getShippingFieldsView()
#layout.shippingFieldRegion.show shippingFieldsView
MyApp.request "location:get_countries", (countries) =>
billingCountryView = #getBillingCountryView(countries)
#layout.billingCountryRegion.show billingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('billing_country_id'), (states) =>
billingStateView = #getBillingStatesView(states)
#layout.billingStateRegion.show billingStateView
MyApp.request "location:get_countries", (countries) =>
shippingCountryView = #getShippingCountryView(countries)
#layout.shippingCountryRegion.show shippingCountryView
MyApp.request "location:get_states", MyApp.activeCustomer.get('shipping_country_id'), (states) =>
shippingStateView = #getShippingStatesView(states)
#layout.shippingStateRegion.show shippingStateView
MyApp.mainRegion.show #layout
The billing country view:
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
onRender: ->
this.$el.attr('value', this.model.get('id'));
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
this.$el.attr('selected', 'selected');
class View.BillingCountryDropdown extends MyApp.Views.CompositeView
template: billingCountryTpl
itemView: View.BillingCountryDropdownItem
itemViewContainer: "select"
The template, simply:
<label>Country
<select id="billing_country_id" name="billing_country_id">
<%- name %>
</select>
</label>
Your code can be optimized. Just move content of onRender method to the ItemView attributes.
class View.BillingCountryDropdownItem extends MyApp.Views.ItemView
template: billingCountryItemTpl
tagName: "option"
attributes: ->
var id = this.model.get('id');
var attributes = { 'value': id };
if MyApp.activeCustomer.get('billing_country_id') == this.model.get('id')
attributes['selected'] = 'selected';
return attributes
The difference between this method and onRender case is that, on render will execute when collection already rendered and 200+ operations will be done with DOM nodes, which will bring performance issues.
In case of attributes method, it executes upon view creation.
There are few advices you can follow:
1) Ask your self do you really need to render all items at once? Maybe you can render part of collection and render other items on scroll or use pagination or use 'virtual scrioll' with SlickGrid or Webix for example.
2) Checkout how often you re-render your view. Try to minify num of events cause re-render
3) Try to minify num of event listeners of ItemView. Its good practice to delegate context events to CollectionView
4) You can use setTimeout to render collection by parts. For example you divide you coll in 4 parts by 50 items and raise 4 timeouts to render it.
5) You can optimize underscore templating and get rid of with {} operator. http://underscorejs.org/#template
What's in your 'billingCountryItemTpl' varible? If it's just string with template ID then you could precompile your template using Marionette.TemplateCache.
So you'll have:
template: Marionette.TemplateCache.get(billingCountryItemTpl)
I have a backbone model of a patient, which I can use to pull patients from my Mongo database. But in addition to polling them by ID, I would like to be able to pull them by name. The only way I could think of doing this is to do something like:
class Thorax.Models.Patient extends Thorax.Model
urlRoot: '/api/patients'
idAttribute: '_id'
fetch: (options = {}) ->
if #get 'first' # has first name, lookup by that instead of id
#urlRoot = '/api/patients/by_name/' + (#get 'first') + '/' + (#get 'last')
#set '_id', ''
super options
But overriding the urlRoot seems bad. Is there another way to do this?
you may just use Backbone.Model#url as a method and apply all your logic there.
So if it is first name in the model use one url and else use default url root.
Here is jsbin code for this (just convert to your CoffeeScript)
You may open network tab to see 2 XHR requests for 2 models I created they are different.
var Model = Backbone.Model.extend({
urlRoot: 'your/url/root',
url: function() {
// If model has first name override url to lookup by first and last
if (this.get("first")) {
return '/api/patients/by_name/' + encodeURIComponent(this.get('first')) + '/' + encodeURIComponent(this.get('last'));
}
// Return default url root in other cases
return Backbone.Model.prototype.url.apply(this, arguments);
}
});
(new Model({ id: 1, first: 'Eugene', last: 'Glova'})).fetch();
(new Model({ id: "patient-id"})).fetch();
Also you may apply this logic to the url in fetch options. But I don't think it is good way.
Happy coding.
I want to fire fetch method on Backbone Collection which would pass an Id parameter similar to what happens in Model.fetch(id)
E.g.
var someFoo= new Foo({id: '1234'});// Where Foo is a Backbone Model
someFoo.fetch();
My Backbone collection:-
var tasks = backbone.Collection.extend({
model: taskModel,
url: '/MyController/GetTasks',
initialize: function () {
return this;
}
});
In my View when I try to fetch data:-
var _dummyId = 10; //
// Tried approach 1 && It calls an api without any `id` parameter, so I get 500 (Internal Server Error).
this.collection.fetch(_dummyId);
// Tried approach 2 && which fires API call passing Id, but just after that
// I am getting error as below:- Uncaught TypeError: object is not a function
this.collection.fetch({
data: {
id: _dummyId
}
});
Found it very late : To cut short the above story I want something like Get /collection/id in backbone.
Thank you for your answers, finally I got the solution from Backbone.js collection options.
Apologies that I couldn't explain the question properly while for same requirement others have done brilliantly and smartly.
Solution : I can have something like :-
var Messages = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
return '/messages/' + this.id;
},
model: Message,
});
var collection = new Messages([], { id: 2 });
collection.fetch();
Thanks to nrabinowitz. Link to the Answer
As mentioned by Matt Ball, the question doesn't make sense: either you call fetch() on a Collection to retrieve all the Models from the Server, or you call fetch() on a Model with an ID to retrieve only this one.
Now, if for some reason you'd need to pass extra parameters to a Collection.fetch() (such as paging information), you could always add a 'data' key in your options object, and it may happen that one of this key be an id (+add option to add this fetched model rather than replace the collection with just one model)... but that would be a very round-about way of fetching a model. The expected way is to create a new Model with the id and fetch it:
this.collection = new taskCollection();
newTask = this.collection.add({id: 15002});
newTask.fetch();
In your code however, I don't see where the ID is coming from, so I am wondering what did you expect to be in the 'ID' parameter that you wanted the collection.fetch() to send?
Here is my situation. I have a bunch of "Question" model inside a "Questions" collection.
The Question Collection is represented by a SurveyBuilder view.
The Question Model is represented by a QuestionBuilder view.
So basically you have an UL of QuestionBuilder views. The UL has a jQuery sortable attached (so you can reorder the questions). The question is once I'm done reordering I want to update the changed "question_number"s in the models to reflect their position.
The Questions collection has a comparator of 'question_number' so collection should be sorted. Now I just need a way to make their .index() in the UL reflect their question_number. Any ideas?
Another problem is DELETEing a question, I need to update all the question numbers. Right now I handle it using:
var deleted_number = question.get('question_number');
var counter = deleted_number;
var questions = this.each(function(question) {
if (question.get('question_number') > deleted_number) {
question.set('question_number', question.get('question_number') - 1);
}
});
if (this.last()) {
this.questionCounter = this.last().get('question_number') + 1;
} else {
this.questionCounter = 1;
}
But it seems there's got to be a much more straighforward way to do it.
Ideally whenever a remove is called on the collection or the sortstop is called on the UL in the view, it would get the .index() of each QuestionuBuilder view, update it's models's question_number to the .index() + 1, and save().
My Models,Views, and Collections: https://github.com/nycitt/node-survey-builder/tree/master/app/js/survey-builder
Screenshot: https://docs.google.com/file/d/0B5xZcIdpJm0NczNRclhGeHJZQkE/edit
More than one way to do this but I would use Backbone Events. Emit an event either when the user clicks something like done sorting, hasn't sorted in N seconds, or as each sort occurs using a jQuery sortable event such as sort. Listen for the event inside v.SurveyBuilder.
Then do something like this. Not tested obviously but should get you there relatively easily. Update, this should handle your deletions as well becuase it doesn't care what things used to be, only what they are now. Handle the delete then trigger this event. Update 2, first examples weren't good; so much for coding in my head. You'll have to modify your views to insert the model's cid in a data-cid attribute on the li. Then you can update the correct model using your collection's .get method. I see you've found an answer of your own, as I said there are multiple approaches.
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.eventHub = EventHub;
this.questions = new c.Questions();
this.questions.on('add', this.addQuestion, this);
this.eventHub.on('questions:doneSorting', this.updateIndexes)
},
updateIndexes: function(e) {
var that = this;
this.$('li').each(function(index) {
var cid = $(this).attr('data-cid');
that.questions.get(cid).set('question_number', index);
});
}
I figured out a way to do it!!!
Make an array of child views under the parent view (in my example this.qbViews maintains an array of QuestionBuilder views) for the SurveyBuilder view
For your collection (in my case this.questions), set the remove event using on to updateIndexes. That means it will run updateIndexes every time something is removed from this.questions
In your events object in the parent view, add a sortstop event for your sortable object (in my case startstop .question-builders, which is the UL holding the questionBuilder views) to also point to updateIndexes
In updateIndexes do the following:
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
And there is my full code for SurveyBuilder view:
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.qbViews = []; //will hold all of our QuestionBuilder views
this.questions = new c.Questions(); //will hold the Questions collection
this.questions.on('add', this.addQuestion, this);
this.questions.on('remove', this.updateIndexes, this); //We need to update Question Numbers
},
bindSortable: function() {
$('.question-builders').sortable({
items: '>li',
handle: '.move-question',
placeholder: 'placeholder span11'
});
},
addQuestion: function(question) {
var view = new v.QuestionBuilder({
model: question
});
//Push it onto the Views array
this.qbViews.push(view);
$('.question-builders').append(view.render().el);
this.bindSortable();
},
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
events: {
'click .add-question': function() {
this.questions.add({});
},
//need to update question numbers when we resort
'sortstop .question-builders': 'updateIndexes'
}
});
And here is the permalink to my Views file for the full code:
https://github.com/nycitt/node-survey-builder/blob/1bee2f0b8a04006aac10d7ecdf6cb19b29de8c12/app/js/survey-builder/views.js
Having some issues with pulling calendar events from Google Calendar using Backbone.
When I call collection.fetch() I am only getting a length of 1 returned, when there are 13 objects in the json.
I had a look at the parse:function(response) method that I am overriding in the Collection, and it is returning all 13 objects. I had a look at the add method in backbone.js, and the issue appears to occur on line 591:
models = _.isArray(models) ? models.slice() : [models];
When I wrap the line with console.log to check the status of the models variable:
console.log(models);
models = _.isArray(models) ? models.slice() : [models];
console.log(models);
I get the following result:
[Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object,Object] backbone.js:590
[child,undefined × 12]
I'm at a loss to explain why it would be failing on add. I have checked each model by changing the parse:function(response) method in the collection to return each object, and it works fine.:
parse: function(response) {
return response.feed.entry[5];
}
I have successfully parsed Google Calendar feeds with Backbone.js before, so I fear I am missing something really simple.
If I console.log response.feed the following is returned:
This is the full class:
/**
* Backbone
* #class
*/
var Gigs = Gigs || {};
Gigs.Backbone = {}
Gigs.Backbone.Model = Backbone.Model.extend();
Gigs.Backbone.Collection = Backbone.Collection.extend({
model: Gigs.Backbone.Model,
url: 'http://www.google.com/calendar/feeds/email#email.com/public/full?alt=json-in-script&orderby=starttime&callback=?',
sync: function(method, model, options) {
options.dataType = "jsonp";
return Backbone.sync(method, model, options);
},
parse: function(response) {
return response.feed.entry;
}
});
Gigs.Backbone.Controller = Backbone.View.extend({
initialize: function() {
var self = this;
this.collection = new Gigs.Backbone.Collection();
this.collection.on('reset', this.addElements, this);
this.collection.fetch();
},
addElements: function() {
log(this.collection);
}
});
var backbone = new Gigs.Backbone.Controller();
Apparently, Google Calendar provides its entries with an id wrapped in an object 1:
"id":{
"$t":"http://www.google.com/calendar/feeds/..."
}
which Backbone seems to dislike. A lot.
One simple solution would be to overwrite the id in your parse method:
parse: function(response) {
var entries=[];
_.each(response.feed.entry, function(entry,ix) {
entry.id=entry.id.$t;
entries.push(entry);
});
return entries;
}
And a Fiddle http://jsfiddle.net/bqzkT/
1 Check https://developers.google.com/gdata/docs/json to see how Google converts its XML data to JSON.
Edit : the problem comes from the way the data is returned with a straight XML to JSON conversion (requested via the alt=json-in-script parameter) wrapping the attributes in objects. Changing this parameter to alt=jsonc yields a much simpler JSON representation. Compare a jsonc output to the json-in-script equivalent.
See https://developers.google.com/youtube/2.0/developers_guide_jsonc#Comparing_JSON_and_JSONC for more info