I need to display three different views which are related to three different model or collections.
In order to perform this task I wrote the following code. (*)
Please tell me if it is the right way to make this, anyway it works.
Here my problem.
In one of this view, let's say the firstView, is possible to perform a DELETE request to the server which take care to delete all the data related to this three view.
Now I need to delete my three view…
but from the firstView I cannot access to the others two views.
1) How can I perform this task?
2) Should I redesign/improve my implementation?
(*)
// module for display three different views
define([
"js/views/01View",
"js/views/02View",
"js/views/03View"
], function (FirstView, SecondView, ThirdView) {
var MainView = Backbone.View.extend({
initialize: function ()
{
this.render();
},
render: function ()
{
var movie_id = this.options.movie_id;
this.firstView = new FirstView(movie_id);
this.secondView = new SecondView(movie_id);
this.thirdView = new ThirdView(movie_id);
}
});
return MainView;
});
P.S.:
The _id is used to build the url parameter of collections or models
url1: http://localhost/movie/movie_id (model1)
url2: http://localhost/movie/movie_id/followers (collection2)
ulrs: http://localhost/movie/movie_id/feeds (collection3)
When I delete the model1 the view2 and view3 related to collection2 and collection3 should be removed.
To fit your problem based on our comments conversation, Backbone architecture revolves using events, so why not use an event aggregator to send events around, don't limit yourself to the backbone constructs. fire an event from one view to another in backbone This pattern provides an elegant solution to your problem.
Views should not respond to direct method calls but to events. Said that you either create a common EventAggregator accesible from every View (as #20100 has explained in his answer) or you connect the Views through a common Model and make each View to listen to its own more interesting events on it.
In your case you can instantiate the Movie model out of the Views instantiations and connect the three Views around it:
// code simplified and not tested
var MainView = Backbone.View.extend({
initialize: function ( opts ) {
this.movie = new Movie({ id: this.opts.movie_id} )
this.movie.fetch();
this.render();
},
render: function () {
this.firstView = new FirstView( this.movie );
this.secondView = new SecondView( this.movie );
this.thirdView = new ThirdView( this.movie );
}
});
var ThirdView = Backbone.View.extend({
initialize: function( opts ) {
this.movie = opts.movie;
this.movie.on( "destroy", this.cleanUp, this )
this.followers = // fetch the followers as you do now, use this.model.id
}
cleanUp: function(){
// your clean up code when model is detroyed
}
});
Related
I have a Backbone SAP which has two subviews within its main App view. These are interdependent: the top one dispalys a music score rendered using Vexflow (Javascript music notation package), and the other below it displays an analysis of the score, also using Vexflow but with some extra objects (text, lines, clickable elements, etc).
The main problem I have is that a lot of the data I need for the analysis view doesn't come into existence until the score view has been rendered. For example, the x coordinate of a musical note is only available after the note has been drawn (the same isn't true of the y coordinate). Below is (in schematic terms) how my app view is set up:
var AppView = Backbone.View.extend({
//...
initialize: function() {
this.scoreView = new ScoreView();
this.analysisView = new AnalysisView({
data: this.getAnalysisData()
});
},
render: function() {
this.scoreView.render();
this.analysisView.render();
return this;
},
getAnalysisData: function() {
// Performs anaysis of this.scoreView,
// and returns result.
}
});
My work around is to move the analysis view setup into the render method, after the score view has been rendered. I dislike doing this, as the getAnalysisData method can be quite expensive, and I believe the render method should be reserved simply for rendering things, not processing.
So I'm wondering if - since there doesn't seem to be a Vexflow solution - there is a Backbone pattern that might fix this. I am familiar with the 'pub/sub' event aggregator pattern for decoupling views, as in:
this.vent = _.extend({}, Backbone.Events);
So on this pattern the analysis view render method subscribes to an event fired after the score view is rendered. I'm not sure how this would alter my code, however. Or perhaps use listenTo, like this:
// Score subview.
var ScoreView = Backbone.View.extend({
initialize: function() {
this.data = "Some data";
},
render: function() {
alert('score');
this.trigger('render');
}
});
// Analysis subview.
var AnalysisView = Backbone.View.extend({
initialize: function(options) {
this.data = options.data;
},
render: function() {
alert(this.data);
return this;
}
});
// Main view.
var AppView = Backbone.View.extend({
el: "#some-div",
initialize: function() {
this.scoreView = new ScoreView();
var view = this;
this.listenTo(this.scoreView, 'render', this.doAnalysis); // <- listen to 'render' event.
},
render: function() {
this.scoreView.render();
return this;
},
doAnalysis: function() {
this.analysisView = new AnalysisView({
data: this.getAnalysisData()
});
this.analysisView.render();
},
getAnalysisData: function() {
return this.scoreView.data;
}
});
Of course, the analysis step is still effectively being done 'during' the render process, but this seems a better pattern. It seems more like the Backbone way of doing things. Am I right? Or am I missing something?
Edit: I dont necessarily have to create the analysis view in the doAnalysis, I could still do that in the main view initialize (at the moment I'm not). But doAnalysis has to run after the score view has rendered, otherwise it cannot access the relevant score geometry information.
I'm currently developing my first Backbone single page app project and I'm facing an issue.
Basically I have a menu (html select input element) implemented as a View. Its value is used to control pretty much every other data requests since it specifies which kind of data to show in the other Views.
Right now I handle the DOM event and trigger a global event so that every model can catch it and keep track internally of the new value. That's because that value is then needed when requesting new data. But this doesn't look like a good solution because A) I end up writing the same function (event handler) in every model and B) I get several models with the same variable.
var Metrics = Backbone.Collection.extend({
url: "dummy-metrics.json",
model: MetricsItem,
initialize: function () {
this.metric = undefined;
},
setMetric: function (metric) {
this.metric = metric;
globalEvents.trigger("metric:change", this.get(metric));
}
});
var GlobalComplexity = Backbone.Collection.extend({
url: function () {
var url = "http://asd/global.json?metric=" + this.metric;
return url;
}, //"dummy-global.json",
model: GlobalComplexyItem,
initialize: function () {
this.metric = undefined;
this.listenTo(globalEvents, "metric:change", this.updateMetric);
},
updateMetric: function (metric) {
this.metric = metric.get("id");
this.fetch({ reset: true });
}
});
All my other Collections are structured like GlobalComplexity.
What's the cleanest way to solve this problem?
Thank you very much.
Define a global parametersManager. Export an instance (singleton) then require it when you need it.
On "globalupdate" you update the parametersManager then trigger "update" for all your model/collections so they'll look what are the current parameters in the parametersManager.
I am struggling with when to destroy backbone views. I know I need to destroy the view somewhere, but I am not sure where.
I have the following code in router.js
routes: {
"names/search": "nameSearch",
"companies/search": "companySearch"
},
initialize: function(){
Backbone.history.start();
this.navigate("#/", true);
}
nameSearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var obj = {};
obj.Status = [utils.xlate("On Assignment"), utils.xlate("Candidate")];
var view = new RecordSearchView({ model: obj, el: $(".content") }, { "modelName": "Candidate" });
view.delegateEvents();
});
},
companySearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var view = new RecordSearchView({ model: {}, el: $(".content") }, { "modelName": "Company" });
view.delegateEvents();
});
}
And then in RecordSearchView.js I have the following function that is called when a user clicks the search button
doSearch: function () {
require(["app/utils/SearchHelper", "app/models/" + modelName, "app/views/SearchResults"], function (SearchHelper, Model, SearchResultsView) {
var obj = $("#searchForm").serializeArray();
var params = SearchHelper.getQuery(obj);
params["page"] = 1;
params["resultsPerPage"] = 25;
var collection = new Model[modelName + "Collection"]({}, { searchParams: params });
params["Fields"] = collection.getSearchFields();
collection.getPage(params["page"], function (data) {
require(["app/views/SearchResults"], function (SearchResultsView) {
App.Router.navigate(modelName + "/search/results");
var view = new SearchResultsView({ collection: data, el: $(".content") });
view.delegateEvents();
});
});
return false;
});
And SearchResults.js
return BaseView.extend({
init: function () {
this.render();
},
render: function () {
var data = this.collection.convertToSearchResults();
this.$el.html(template(data));
return this;
}
});
The problem is the second time I perform any search (calling the doSearch function from RecordSearch.js). As soon as I perform the second search, the data shown is that belonging to the previous search I performed. (For example I do a name search and it works, then do a company search but the screen shows company search results but then is quickly replaced with name search results).
My questions are
I suspect I need to call some cleanup code on the view before it is re-used. Where is the proper place within a backbone application to run this.
Is there anything wrong with the way I load SearchResults view from within RecordSearch view? SearchResults does not have a path on my router, but it is basically a form post, so I assume it shouldn't?
Any help is appreciated.
This problem is quite common and is known as Zombie Views. Derick Bailey explains this issue very well here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
However unfortunately you can't simply solve it without changing the way you are loading your views.
Because you are loading them inside RequireJS modules that will keep it in the local var scope, you are losing the reference to the views once the route has been fully processed.
In order to solve this problem, you would need to keep the reference of the current view somewhere, and then properly dispose it before calling another view, something like this:
showView: function(view) {
this.currentView && this.currentView.remove();
this.currentView = view;
this.currentView.render();
$('#content').html(this.currentView.el);
}
More about this solution here: http://tiagorg.com/talk-backbone-tricks-or-treats-html5devconf/#/6
I personally suggest you adopting a solution that will take care of this for you, like Marionette.js
It will handle this and quite many other issues, by providing the missing gaps of every Backbone-based architecture.
When developing Backbone applications, I often find myself instantiating models in views when dealing with nested data. Here's some example data:
{
name: Alfred,
age 27,
skills: [
{
name: 'Web development',
level: 'Mediocre'
},
{
name: 'Eating pizza',
level: 'Expert'
]
}
Say I have some view PersonView that takes a person object PersonModel as its model (where Alfred would be an instance). And let's say I want to render the person's skills as subviews. At this point I create a new view and a new model for dealing with the nested skills data. And this is where I suspect I'm doing something wrong, or at least something suboptimal.
var SkillModel = Backbone.Model.extend();
var SkillView = Backbone.View.extend({
tagName: 'li',
initialize: function() {
this.render();
},
render: function() {
this.$el.html(someTemplate(this.model));
}
});
var PersonView = Backbone.View.extend({
el: ...
initialize: ...
renderSkills: function() {
_.each(this.model.get('skills'), function(skill) {
var skillModel = new SkillModel(skill);
var skillView = new SkillView({ model: skillModel });
self.$el.append(skillView.el);
})
}
});
Should I really be doing this or is there some better pattern, that does not involve third-party libraries, that solves the problem? I feel like I'm not really decoupling the views and models in the best way. If you want a concrete question it's: Is this an anti-pattern?
Views are for view logic, Models for data logic.
Consider to have a Model like this:
var SkillModel = Backbone.Model.extend({
});
var SkillsCollection = Backbone.Collection.extend({
model : SkillModel
});
var PersonModel = Backbone.Model.extend({
/*
** Your current model stuff
*/
//new functionality
skills : null,
initialize : function(){
//set a property into the model that stores a collection for the skills
this.skills = new SkillsCollection;
//listen to the model attribute skills to change
this.on('change:skills', this.setSkills);
},
setSkills : function(){
//set the skills of the model into skills collection
this.skills.reset( this.get('skills') );
}
});
So in your view renderSkills would be something like this:
var PersonView = Backbone.View.extend({
renderSkills: function() {
//this.model.skills <- collection of skills
_.each(this.model.skills, function(skill) {
var skillView = new SkillView({ model: skillModel });
self.$el.append(skillView.el);
})
}
});
I did my pseudo code trying to adapt to your sample code. But if you get the point, basically you could have a nested model or collection into your Model, so in the view there is not needed data interaction / set, everything is in the model. Also I answer as your requested, handling things without a third party. However don't mind taking a look to the source of this kind of plugins also:
https://github.com/afeld/backbone-nested
Update: per comments I provided a setup of the Model example:
var m = new PersonModel();
//nested collection is empty
console.log(m.skills.length);
m.set({skills : [{skill1:true}, {skill1:true}]});
//nested collection now contains two children
console.log(m.skills.length);
As you can see the Model usage is as "always" just a set of attributes, and the model is the one handling it. Your view should use skills as how views use any other collection. You could do a .each iterations or worth better to listen for events like add, reset, etc. on that collection(but that is other topic out of the scope of this question).
Example:
http://jsfiddle.net/9nF7R/24/
I have to display the same collection of backbone models in 2 different places on the same page (once in the navigation, once in the main area), and I need to keep the models in the collections in sync but allow each collection to be sorted differently. The nav is always alphabetical ascending but the sorting in the main content area is configurable by the user. What's the best way to do this? Should I have 2 different collections and event bindings to try to make sure their models are always identical (using the add/remove/reset events)? Should I have just 1 collection and change it's sort order on the fly as needed?
I usually consider that the collections should not be altered to represent a transient reordering, so :
the views listen to the collection for the usual events,
the views retrieve a custom list of models sorted as needed when they have to render.
For example,
var C = Backbone.Collection.extend({
comparator: function (model) {
return model.get('name');
},
toJSON_sorted: function (reversed) {
var models = this.toJSON();
return (!reversed) ? models : models.reverse();
}
});
var V = Backbone.View.extend({
initialize: function () {
this.collection.on('reset', this.render, this);
this.collection.on('change', this.render, this);
this.collection.on('add', this.render, this);
this.collection.on('delete', this.render, this);
}
});
var NavView = V.extend({
render : function () {
console.log(this.collection.toJSON());
}
});
var MainView = V.extend({
render : function () {
var data = this.collection.toJSON_sorted(this.reversed);
console.log(data);
}
});
Calling these definitions
var c=new C();
var v1 = new NavView({collection:c});
var v2 = new MainView({collection:c});
v2.reversed=true;
c.reset([
{name:'Z'},
{name:'A'},
{name:'E'}
]);
provides the views with the models in the expected order and lets you change the sort direction at will. You could also tailor toJSON_sorted to handle sorting by other fields.
A Fiddle to play with http://jsfiddle.net/nikoshr/Yu8y8/
If the collections contain exactly the same thing, I think keeping 1 collection and utilizing that for both displays is the most tidy. Just call the sort function before you render whatever visual component you want.
All you have to do is change the comparator function. You have have methods on your collection to do this but the basic idea is
var mycollection = new Backbone.Collection();
mycollection.comparator = function(item) { return item.get('blah') }