How to pass Backbone.js model data to Bootbox with Handlebars.js? - backbone.js

I have a marionette view that have a method to create a new model from a bootbox. Now i need to be able to edit the model from the bootbox, how can i I pass the current model data to the box?
This is some of my current code:
Module.Views.Chaptersx = Marionette.CompositeView.extend({
template: Module.Templates['documents/create/course/chapter/index'],
childView: Module.Views.ChapterItemx,
childViewContainer: "#chaptersCollection",
events: {
'click .chapters-create': 'create',
//'click #uploadFilesChapters': 'startUpload'
},
create: function (evt) {
console.log('create');
evt.preventDefault();
var me = this;
var box = bootbox.dialog({
show: false,
title: "Nueva Seccion",
message: Module.Templates['documents/create/course/chapter/chapterModal'],
buttons: {
success: {
label: "Guardar",
className: "btn-success",
callback: function () {
var chapterNo = $('#cn').val();
var chapterName = $('#chapterName').val();
var chapter = new Module.Models.Chapter({
chapterNo: chapterNo,
chapterName: chapterName,
});
me.collection.add(chapter);
}
}
}
});
box.on("show.bs.modal", function () {
console.log('numbers');
var number = (me.collection.size() + 1);
$('#cn').val(number);
});
box.modal('show');
},

TL;DR - use model's custom events or an event bus to pass the data.
You can reference this.model in the view, which is somewhat of a compromise (you're tying the view and the model).
You could pass the data via the event object's data property, but for that you're gonna have to extend some methods and get into backbone's nitty gritty.
Use a data- attribute on the element:
<div class="chapters-create" data-cats></div>
create: function (evt) {
var cats = $(evt.currentTarget).data('cats');
// ...
}
… which is considered bad habit by the way - you're still tying data to the DOM (or model to view, MVC speaking).
Well, I don't like either of the above, as they tend to have high coupling - I'd do it with custom events on a shared model resides at a higher level.
I don't know where the data comes from, but bottom line - shoot it in a custom event, or, better yet, use an event bus, like the one offered by marionette.js.

You need to create another view, call it EditView or something, render it, and provide the view.el as a message option to bootbox. However, the whole thing feels like a hack to me, and I think that it's better to implement a modalRegion and manage the modals yourself.

Related

Two Views Sharing an Event

I've been thrown into a Backbone code base and one of the modifications I need to make requires duplicating a text element with typeahead. Rather than copy and paste code, I'd like to re-use the event code but as I know hardly anything about Backbone I'm not sure how this should be done. Should it be a helper? If so, where do I put the helper code so it can be used by both views? I'd rather not attempt view inheritance if at all possible because I'd like to keep the changes as simple and minimal as possible.
events: {
// all other events removed for conciseness.
'typeahead:selected #ud_producerid': 'producerChanged'
}
I need the same event with the identical functionality in the producerChanged function as well as the setupBindings code that wires up the typeahead to work in 2 different views.
I know you said you didn't want to use inheritance here but it is easy in Backbone and well suited to the task.
var TypeaheadBase = Backbone.View.extend({
events: {
'typeahead:selected #ud_producerid': 'producerChanged'
},
producerChanged: function(e) {
...
},
anotherBaseMethod: function() {
...
}
});
var TypeaheadBaseA = TypeaheadBase.extend({
someOtherAMethod: function() {
...
},
// You can do some extra functionality on `producerChanged`.
// (Or you can override by not calling the Base prototype).
producerChanged: function() {
TypeaheadBase.prototype.producerChanged.apply(this, arguments);
// Do some additional stuff.
}
});
var TypeaheadBaseB = TypeaheadBase.extend({
// You can also extend things like events, which could be a hash (Object).
events: function() {
var parentEvents = _.result(TypeaheadBase.prototype, 'events');
return _.extend({}, parentEvents, {
'click a': 'someClickEvent'
});
},
someClickEvent: function() {
...
}
});

Backbone - user case

Say a user is going down a page and checking off and selecting items.
I have a Backbone model object, and each time the user selects something I want to update the object.
I have this in a separate JavaScript file that I source in my HTML:
var app = {};
var newLineup = null;
var team = document.getElementsByName('team');
app.Lineup = Backbone.Model.extend({
defaults: {
team: team,
completed: false
},
idAttribute: "ID",
initialize: function () {
console.log('Book has been intialized');
this.on("invalid", function (model, error) {
console.log("Houston, we have a problem: " + error)
});
},
constructor: function (attributes, options) {
console.log('document',document);
console.log('Book\'s constructor had been called');
Backbone.Model.apply(this, arguments);
},
validate: function (attr) {
if (attr.ID <= 0) {
return "Invalid value for ID supplied."
}
},
urlRoot: 'http://localhost:3000/api/lineups'
});
function createNewLineupInDatabase(){
newLineup = new app.Lineup({team: team, completed: false});
newLineup.save({}, {
success: function (model, respose, options) {
},
error: function (model, xhr, options) {
}
});
}
When the user first accesses the page, I will create a new lineup object by calling the above function. But how do I update that object as the user interacts with the page? Is there a better way to do this other than putting the Backbone model object at the top of my JavaScript file?
The Backbone pattern was designed to answer your question. As other respondents said, wire up a View, which takes your model as a parameter and lets you bind DOM events to the model.
That said, you don't have to use the rest of the framework. I guess you can use all the functionality Backbone provides models by handling the model yourself.
You need to worry about a couple of things.
Give you model a little encapsulation.
Set up a listener (or listeners) for your checkbox items.
Scope the model to your app
Backbone provides neat encapsulation for your model inside a View, but if you can live with it, just use your app variable which is within scope of the JavaScript file you posted.
When you're ready to instantiate your model, make it a property of app:
app.newLineup = new app.Lineup({team: team, completed: false});
It may look weird to have the instance and the constructor in the same object, but there aren't other options until you pull out the rest of Backbone.
The listener
So you have N number of checkboxes you care about. Say you give them a class, say, .options. Your listener will look like
$( ".options" ).change(function() {
if(this.checked) {
//Do stuff with your model
//You can access it from app.newLineup
} else {
}
});
Voila! Now your page is ready to talk to your model.
If there is frontend ui / any user interaction within your code it is extremely useful to create a backbone view which makes use of an events object where you can set up your event handler.
You can also link a view to a model to allow your model / your object to be updated without scope issues.

rerender Backbone views without losing references to dom

I have the following problem with backbone and I'd like to know what strategy is the more appropriated
I have a select control, implemented as a Backbone view, that initially loads with a single option saying "loading options". So I load an array with only one element and I render the view.
The options will be loaded from a collection, so I fire a fetch collection.
Then I initialize a component that is in charge of displaying in line errors for every field. So I save a reference of the dom element of the combo.
When the fetch operation is finally ready, I rerender the control with all the options loaded from the collection.
To render the view I user something like this:
render: function() {
this.$el.html(this.template(this.model.attributes));
return this;
}
pretty standard backbone stuff
the problem is that after rendering the view for the second time the reference of the dom is no longer valid,
perhaps this case is a bit strange, but I can think of lots of cases in which I have to re-render a view without losing their doms references (a combo that depends on another combo, for example)
So I wonder what is the best approach to re-render a view without losing all the references to the dom elements inside the view...
The purpose of Backbone.View is to encapsulate the access to a certain DOM subtree to a single, well-defined class. It's a poor Backbone practice to pass around references to DOM elements, those should be considered internal implementation details of the view.
Instead you should have your views communicate directly, or indirectly via a mediator.
Direct communication might look something like:
var ViewA = Backbone.View.extend({
getSelectedValue: function() {
return this.$(".combo").val()
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
this.viewA = options.viewA;
},
doSomething: function() {
var val = this.viewA.getSelectedValue();
}
});
var a = new ViewA();
var b = new ViewB({viewA:a});
And indirect, using the root Backbone object as a mediator:
var ViewA = Backbone.View.extend({
events: {
"change .combo" : "selectedValueChanged"
},
selectedValueChanged: function() {
//publish
Backbone.trigger('ViewA:changed', this.$('.combo').val());
}
});
var ViewB = Backbone.View.extend({
initialize: function(options) {
//subscribe
this.listenTo(Backbone, 'ViewA:changed', this.doSomething);
},
doSomething: function(val) {
//handle
}
});
var a = new ViewA();
var b = new ViewB();
The above is very generic, of course, but the point I'm trying to illustrate here is that you shouldn't have to worry whether the DOM elements are swapped, because no other view should be aware of the element's existence. If you define interfaces between views (either via method calls or mediated message passing), your application will be more maintainable and less brittle.

Routing & events - backboneJS

How should I be handling routing in BackboneJS? When routing, after new-upping my view, should I be triggering an event, or rendering the view directly?
Here are the two scenarios:
Trigger Event:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
var viewOrderView = new ViewOrderView();
vent.trigger('order:show', orderId);
}
In my view, I have:
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function () {
vent.on('order:show', this.show, this);
},
show: function (id) {
this.id = id;
this.render();
},
render: function () {
var template = viewOrderTemplate({ id: this.id });
this.$el.html(template);
return this;
}
});
OR, should I go this route:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
var viewOrderView = new ViewOrderView({id : orderId });
viewOrderView.render();
}
In my view, I have:
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function () {
//init code here
},
render: function () {
var template = viewOrderTemplate({ id : this.id});
this.$el.html(template);
return this;
}
});
I think it's the first scenario - given that backbone is event driven, but the 2nd obviously has less code.
Also, I suppose a third scenario would be to keep the view code in the first scenario, but grab the router scenario of the second... rendering the view on navigation, but exposing an event in case I want to trigger that elsewhere.
Thoughts?
So all backbone questions usually end up with many plausible answers. In this case, I believe your second example is a more canonical/typical backbone pattern. Putting aside the tricky issue of handling loading spinners and updating after data loads, the simplified basic pattern in your router would be:
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
//Use models to represent your data
var orderModel = new Order({id: orderId});
//models know how to fetch data for themselves given an ID
orderModel.fetch();
//Views should take model instances, not scalar model IDs
var orderView = new OrderView({model: orderModel});
orderView.render();
//Exactly how you display the view in the DOM is up to you
//document.body might be $('#main-container') or whatever
$(document.body).html(orderView.el);
}
I think that's the textbook pattern. Again, the issue of who triggers the fetching of data and rerendering after it arrives is tricky. I think it's best if the view knows how to render a "loading" version of itself until the model has fetched data, and then when the model fires a change event after fetch completes, the view rerenders itself with the loaded model data. However, some people might put that logic elsewhere. This article on building the next soundcloud I think represents many very good "state of the art" backbone patterns, including how they handle unfetched models.
In general, you can code things with callbacks or events as you prefer. However, a good rule of thumb is to ask yourself some questions:
Is more than one independent logical piece of work going to respond to this event?
Do I need to decouple the source of this event from the things that happen in response to it?
If both of those are "yes", then events should be a good fit. If both are "no", than straightforward function logic is a better fit. In the case of "navigating to this URL triggers this view", generally the answer to both questions is "no", so you can just code that logic into the router's route handler method and be done with it.
I'd use second scenario. Don't see any benefits of using first approach. It would make more sence this way (but still arguable):
/* ... */
routes: {
'orders/view/:orderId' : 'viewOrder'
},
viewOrder: function (orderId) {
vent.trigger('order:show', orderId);
}
/* ... */
vent.on('order:show', function(orderId) {
var viewOrderView = new ViewOrderView();
viewOrderView.render();
});
var ViewOrderView = Backbone.View.extend({
el: "#page",
initialize: function (options) {
this.orderId = options.orderId;
},
render: function () {
var template = viewOrderTemplate({
id: this.orderId
});
this.$el.html(template);
return this;
}
});
This way at least you'd be able to trigger route action without updating a url. But same effect might be achieved using Backbone.router.viewOrder(1) probably. Events are pretty powerful, but i wouldn't use them if i don't really need.

Multiple backbone views referencing one collection

I am trying to create my first backbone app and am having some difficulty getting my head around how I am meant to be using views.
What I am trying to do is have a search input that each time its submitted it fetches a collection from the server. I want to have one view control the search input area and listen to events that happen there (a button click in my example) and another view with sub views for displaying the search results. with each new search just prepending the results into the search area.
the individual results will have other methods on them (such as looking up date or time that they where entered etc).
I have a model and collection defined like this:
SearchResult = Backbone.model.extend({
defaults: {
title: null,
text: null
}
});
SearchResults = Backbone.Collection.extend({
model: SearchResult,
initialize: function(query){
this.query = query;
this.fetch();
},
url: function() {
return '/search/' + this.query()
}
});
In my views I have one view that represents the search input are:
var SearchView = Backbone.View.extend({
el: $('#search'),
events: {
'click button': 'doSearch'
},
doSearch: function() {
console.log('starting new search');
var resultSet = new SearchResults($('input[type=text]', this.el).val());
var resultSetView = new ResultView(resultSet);
}
});
var searchView = new SearchView();
var ResultSetView = Backbone.View.extend({
el: $('#search'),
initialize: function(resultSet) {
this.collection = resultSet;
this.render();
},
render: function() {
_(this.collection.models).each(function(result) {
var resultView = new ResultView({model:result});
}, this);
}
});
var ResultView = Backbone.view.extend({
tagName: 'div',
model: SearchResult,
initialize: function() {
this.render();
},
render: function(){
$(this.el).append(this.model.get(title) + '<br>' + this.model.get('text'));
}
});
and my html looks roughly like this:
<body>
<div id="search">
<input type="text">
<button>submit</button>
</div>
<div id="results">
</div>
</body>
In my code it gets as far as console.log('starting new search'); but no ajax calls are made to the server from the initialize method of the ResultSetView collection.
Am I designing this right or is there a better way to do this. I think because the two views bind to different dom elements I should not be instantiating one view from within another. Any advice is appreciated and if I need to state this clearer please let me know and I will do my best to rephrase the question.
Some problems (possibly not the only ones):
Your SearchView isn't bound to the collection reset event; as written it's going to attempt to render immediately, while the collection is still empty.
SearchView instantiates the single view ResultView when presumably it should instantiate the composite view ResultSetView.
You're passing a parameter to the SearchResults collection's constructor, but that's not the correct way to use it. See the documentation on this point.
You haven't told your ResultSetView to listen to any events on the collection. "fetch" is asynchronous. When completed successfully, it will send a "reset" event. Your view needs to listen for that event and then do whatever it needs to do (like render) on that event.
After fixing all the typos in your example code I have a working jsFiddle.
You see like after clicking in the button an AJAX call is done. Of course the response is an error but this is not the point.
So my conclusion is that your problem is in another part of your code.
Among some syntax issues, the most probable problem to me that I see in your code is a race condition. In your views, you're making an assumption that the fetch has already retrieved the data and you're executing your views render methods. For really fast operations, that might be valid, but it gives you no way of truly knowing that the data exists. The way to deal with this is as others have suggested: You need to listen for the collection's reset event; however, you also have to control "when" the fetch occurs, and so it's best to do the fetch only when you need it - calling fetch within the search view. I did a bit of restructuring of your collection and search view:
var SearchResults = Backbone.Collection.extend({
model: SearchResult,
execSearch : function(query) {
this.url = '/search/' + query;
this.fetch();
}
});
var SearchView = Backbone.View.extend({
el: $('#search'),
initialize : function() {
this.collection = new SearchResults();
//listen for the reset
this.collection.on('reset',this.displayResults,this);
},
events: {
'click button': 'doSearch'
},
/**
* Do search executes the search
*/
doSearch: function() {
console.log('starting new search');
//Set the search params and do the fetch.
//Since we're listening to the 'reset' event,
//displayResults will execute.
this.collection.execSearch($('input[type=text]', this.el).val());
},
/**
* displayResults sets up the views. Since we know that the
* data has been fetched, just pass the collection, and parse it
*/
displayResults : function() {
new ResultSetView({
collection : this.collection
});
}
});
Notice that I only created the collection once. That's all you need since you're using the same collection class to execute your searches. Subsequent searches only need to change the url. This is better memory management and a bit cleaner than instantiating a new collection for each search.
I didn't work further on your display views. However, you might consider sticking to the convention of passing hashes to Backbone objects. For instance, in your original code, you passed 'resultSet' as a formal parameter. However, the convention is to pass the collection to a view in the form: new View({collection: resultSet}); I realize that that's a bit nitpicky, but following the conventions improves the readability of your code. Also, you ensure that you're passing things in the way that the Backbone objects expect.

Resources