Two way data binding in backbone.js - backbone.js

I'm developing a jQuery Backbone.js web application.
As it is in Adobe Flex, I have implemented 2 way data binding in my app for
input elements/widgets.
So, every input element/widget knows its corresponding model and model attribute name.
When the user hits tab or enter, the field value is automatically given to the model.
container.model.set(this.attrName, this.value, options); // command 1
In the other direction, when the model gets updated from the backend, the view of the
input element/widget should automatically get
updated:
container.model.bind("change:"+ this.attrName, this.updateView, this); // command 2
The problem is:
When the user hits enter and the model is automatically updated, also the "change:abc" is
triggered and this.updateView is called, not only when a new model comes from the
backend.
My solution until now was to pass an option "source: gui" when setting the model value when the user pressed enter (command 1), and to check for that in my updateView method. But I am not content with this solution anymore.
Does anybody have a better solution?
Thanks alot in advance
Wolfgang
Update:
When the option silent: true is passed, the validate method of the model is not called, so
that does not help. See Backbone.js source 0.9.2:
_validate: function(attrs, options) {
if (options.silent || !this.validate) return true;

From Backbone.js site:
A "change" event will be triggered, unless {silent: true} is passed as an option
options.silent = true;
container.model.set(this.attrName, this.value, options);
Update:
You added a new comment to your question, so I just complemented my answer to fix the new use case(validation flow) that you mentioned:
var ExtendedModel = Backbone.Model.extend({
uiChange : false,
uiSet: function (attributes, options, optional) {
this.uiChange = true;
this.set(attributes, options, optional);
this.uiChange = false;
}
});
var MyModel = ExtendedModel.extend({
});
var model = new MyModel();
model.on('change:name', function(){
console.log('this.uiChange: ', this.uiChange);
});
//simulates the server side set
model.set({name:'hello'});
//simulates the ui side set you must use it to set from UI
model.uiSet({name:'hello2'});

Two-way binding just means that:
When properties in the model get updated, so does the UI.
When UI elements get updated, the changes get propagated back to the
model.
Backbone doesn't have a "baked-in" implementation of 2 option (although you can certainly do it using event listeners)
In Backbone, we can easily achieve option 1 by binding a view's "render" method to its model's "change" event. To achieve option 2, you need to also add a change listener to the input element, and call model.set in the handler.
check (jsfiddle.net/sunnysm/Xm5eH/16)jsfiddle example with two-way binding set up in Backbone.

Backbone.ModelBinderplugin works great for providing Two-way data binding between your Backbone Views and Models. I wrote a blog post covering some essential features of this plugin Here is the direct link: http://niki4810.github.io/blog/2013/03/02/new-post/

I wanted to see what the bare bones code would be to have two-way binding with Backbone.js. This is what I came up with:
var TwoWayBoundView = Backbone.View.extend({
initialize: function(options) {
this.options = _.defaults(options || {}, this.options);
_.bindAll(this, "render");
this.model.on("change", this.render, this);
this.render();
},
events: {
"change input,textarea,select": "update"
},
// input updated
update: function(e) {
this.model.set(e.currentTarget.id, $(e.currentTarget).val());
},
// model updated...re-render
render: function(e) {
if (e){
var id = Object.keys(e.changed)[0];
$('#'+id).val(e.changed[id]);
}
else{
_.each(this.model.attributes, function(value, key){
$('#'+key).val(value);
});
}
}
});
And the usage:
var model = new Backbone.Model({ prop1: "uno 1", prop2: "dos 2", prop3: "3" });
var view = new TwoWayBoundView({
el: "#myContainer",
model: model
});
Here's a jsbin for it: http://jsbin.com/guvusal/edit?html,js,console,output
I've used libraries that do this, such as Epoxy.js (only 11k minified). And there are several others besides, which I would recommend long before using the proof of concept code above.
I would be interested in potential pitfalls and improvements that could be made with the TwoWayBoundView class above (but nothing beyond basic two-way binding please! i.e. I'm not looking for more features to add.)

Related

How to pass Backbone.js model data to Bootbox with Handlebars.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.

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.

Is it possible to trigger an event when initializing a model?

In a backbone model, is it possible to trigger an event in the initialize function, for a nested view? I based my current code off this example: https://stackoverflow.com/a/8523075/2345124 and have updated it for backbone 1.0.0. Here is my initialize function, for a Model:
var Edit = Backbone.Model.extend({
initialize: function() {
this.trigger('marquee:add');
this.on('change', function(){
this.trigger('marquee:add');
});
}
...
}
I'm trying to call a method renderMarquee when the model is initialized:
var EditRow = Backbone.View.extend({
initialize: function() {
this.listenTo(this.model, "change", this.render); // works
this.listenTo(this.model, "marquee:add", this.renderMarquee); // only called when changed, but not when initially created
...
}
renderMarquee IS called when the model is changed, but not when it is initialized. 'change' events work as expected (this.render is called). Any thoughts?
Thanks!
I am currently facing a similar problem. I needed to trigger the change event in the initialize method of my model.
I looked into the backbone code which revealed why this is not happening:
var Model = Backbone.Model = function(attributes, options) {
...
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
the set is executed before the initialize and this.change is emptied setting the model state to "nothing has changed".
In order to overwrite behavior this I added the following code to my initialize method.
initialize: function(attributes, options) {
...
this.changed = attributes;
this.trigger('change');
for (attr_name in attributes) {
this.trigger('change:' + attr_name);
}
},
I trigger all change events manually, this is important for me since inheriting models may bind to change or change:attrxy. But this is not enough, because if I just trigger the events the changedAttributes() method would return false therefore I also set this.changed to the current attributes.
This doesn't make a lot of sense because you are initializing the model somewhere prior to doing the view.listenTo call. Unfortunately, you don't really have a choice in that matter.
You are probably going to want to move the event handling to a Backbone.Collection which already has built in events you can listen on for adding/removing.

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.

How to pass data from one view to another with custom events?

Say I have a View that displays a search box with a submit button.
When I click on the submit button how do i pass the value of the search box to another view ?
I tried:
In view 1, inside the submit callback : this.trigger('abc', $('#searchBox').val())
In view 2, in the initialize function: this.bind('abc', function(data){ console.log(data); })
but that does not seem to work: the custom event is fired but View 2 does not see it.
Here's a great article by Derick Bailley # LosTechies.com:
References, Routing, And The Event Aggregator: Coordinating Views In Backbone.js
This article discusses a simple solution using PubSub that is built in Backbone.JS. I agree with Derick when he mentions that views should be decoupled.
Unfortunately you can't bind this way - you will need to share a reference to view1 in view2:
var View2 = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'foo');
this.view1.bind('abc', this.foo);
},
foo: function(data) {
console.log(data);
}
});
This also means that at some point you need to set view1 on your instance of View2 so that you can bind against it.
If you don't want to pass the references around, simply bind the two views together in whatever container you are holding them in (i.e. another view or a controller):
var view1 = new View1();
var view2 = new View2();
view1.bind('abc', view2.foo);
I suggest using a PubSub framework in addition to backbone. MinPubSub is a popular choice. I use the jquery pubsub extension extracted from https://github.com/phiggins42/bloody-jquery-plugins.
Then, View 2 doesn't need a reference to View 1. To modify Andrew Hare's example, you would do:
var View2 = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'foo');
$.subscribe('abc', this.foo);
},
foo: function(data) {
console.log(data);
}
});
Then in View 1:
$.publish('abc', $('#searchBox').val());
Of course, with a pubsub system, you will probably want to use something better than "abc", perhaps instead choosing "searchBox:submit" as the topic.

Resources