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.
Related
I have come across a lot of examples where the backbone-view would be like var view1 = Backbone.View.extend( { } ) but unable to get one where the backbone view is returned directly. In the below code I am able to render the default values of the model attribute and display the same in the dust template but when I do model.fetch(), in the success function I am able to see the json response in the console but unable to set the fetched values to the model attributes and render the new values. Do, let me know what I am missing here. Any help is appreciated.
define(function (require) {
'use strict';
var $ = require('jquery');
var Backbone = require('backbone');
var g = require('global/dust-globals');
var template = require('text!/dust/table1.dust');
var SampleModel = Backbone.Model.extend({
initialize: function () {
},
defaults:{
SampleUpdate:'Test date',
SampleCount: 0
},
urlRoot: "/Sample"
});
var obj1 = new SampleModel();
return Backbone.View.extend({
events: {
// 'click .search-btn': 'searchBtnClick',
},
initialize: function(){
this.testfunc();
this.render();
this.model.on("change", this.render, this);
},
render: function () {
this.$el.html(g.renderTemplate('TabView', template, {}));
//template is compiled and rendered successfully
console.log('CHECK:'+obj1.get("lastUpdate"));
return this;
},
testfunc : function () {
obj1.fetch({
success: function (response) {
console.log(JSON.stringify(response));
obj1.set("SampleUpdate", response.get("sampleUpdate"));
obj1.set("SampleCount", response.get("sampleCount"));
console.log('CHECK1:'+obj1.get("SampleUpdate"));
}
});
}
});
});
My JS code calling the above code would be as below.
var TabView = require('/SampleTab');
return Backbone.View.extend({
initialize: function () {
this.tabView = new TabView({el: '#sample-div', model:this.model, appView: this});
this.render();
},
render: function() {
this.tabView.$el.show();
this.tabView.render();
}
});
I'm having trouble understanding what exactly it is you are trying to do with your code, but it doesn't look like you're using Backbone.View.extend({ ... }) correctly. From the documentation for Backbone.View.extend:
Get started with views by creating a custom view class. You'll want to override the render function, specify your declarative events, and perhaps the tagName, className, or id of the View's root element.
[Emphasis mine.]
The Backbone.View.extend is for creating your own Backbone View classes, not instantiating objects.
If you're looking for more information, I highly recommend that you read through Addy Osmani's free e-book, Developing Backbone.js Applications. You might know some of what it teaches already, but it has some good examples of extending Backbone Views and does a much better job of explaining other fundamentals of using Backbone.js than I could here.
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 have a tree view in my Backbone app, I use nested collections and models:
Collection:
define(function(require) {
var Backbone = require('backbone')
, UserListModel = require('app/models/userList');
return Backbone.Collection.extend({
model: UserListModel,
url: '/api/lists',
});
});
Model:
define(function(require) {
var Backbone = require('backbone');
return Backbone.Model.extend({
constructor: function(data, opts) {
opts = _.extend({}, opts, {parse: true});
var UserLists = require('app/collections/userLists');
this.children = new UserLists();
Backbone.Model.call(this, data, opts);
},
parse: function(data) {
if (_.isArray(data.children))
this.children.set(data.children);
return _.omit(data, 'chilren');
}
});
});
Part of The View: (full views here: http://laravel.io/bin/O9oYX)
var UserListTreeItemView = Backbone.View.extend({
render: function() {
var data = this.model.toJSON();
data.hasChildren = !!this.model.get('isFolder');
this.$el.html(this.template(data));
if( this.model.get('isFolder') ) {
var list = new UserListTreeView({
collection: this.model.children
});
this.$el.append(list.render().el);
}
return this;
}
});
And I use two Views to render my collection as a tree view. I want to add a search feature to my tree view, I can’t figure out how. It should be able to search name attributes on all models and their nested ones.
Any ideas?
If you have already the models you want on your collection, just use the inherited Underscore method filter() on the collection itself. It will return an Array of models, not a Backbone Collection, though.
http://underscorejs.org/#filter
Supposing filtering by attribute name:
var nameToSearch = "whatever";
var itemsByName = this.model.children.filter(function(item){
return item.get("name").indexOf(nameToSearch) >=0;
}
What I would do is isolate your getData method to cover both cases: filtering on/off.
You didn't specify how do you search, but I'll suppose you have a text input around and you want to use that value. Will that search in the top items only? A search-in-depth would be a little more complicated, involving each parent item to look for the name on its children. For the simple case that you'll be searching for files in every folder, keep the search filter in you parent View state. For that, I normally use a plain vanilla Backbone Model, just to leverage events.
var MySearchView = Backbone.View.extend({
initialize: function(options){
//I like the idea of having a ViewModel to keep state
this.viewState = new Backbone.Model({
searchQuery: ""
});
//whenever the search query is changed, re-render
this.listenTo(this.viewState, "change:searchQuery", this.render);
},
events: {
"click .js-search-button": "doSearch"
},
doSearch: function(e){
e.preventDefault();
var query = this.$(".js-search-input").val();
this.viewState.set("seachQuery", query);
},
render: function(){
var data = this.model.toJSON();
data.hasChildren = !!this.model.get('isFolder');
this.$el.html(this.template(data));
if( this.model.get('isFolder') ) {
//be careful with this, you're not removing your child views ever
if(this._listView) {
this._listView.remove();
}
this._listView = new UserListTreeView({
collection: this.model.children,
**searchQuery: this.viewState.get("searchQuery")**
});
this.$el.append(this._listView.render().el);
}
return this;
}
});
Now in your UserListTreeView, abstract the data-feeding for the template into a method that takes into account the search query:
var UserListTreeView = Backbone.View.extend({
initialize: function(options){
this.searchQuery = options.searchQuery || "";
},
...
getData: function(){
//filter your collection if needed
var query = this.searchQuery;
if(query !== ""){
return this.collection.filter(function(file){
return file.get("name").indexOf(query) >= 0;
}
else {
return this.collection.toJSON();
}
},
render: function() {
var items = this.getData(),
template = this.template(items);
this.$el.empty().append(template);
return this;
}
});
Voilá, the same view will render either the full collection or a filtered version whose items contain the searchQuery in their name. You can adjust the search method just by changing the comparison inside the filter call: you could do RegExp, search only for files starting with (indexOf(searchQuery) == 0), and so on.
Took it longer than expected, hope it helps. Another option would be to implement this in the collection itself, you can override its toJSON() method to return either all, or some items on it. If you find yourself writing another view that needs filterint, then probably it's a better idea to create a SearchableCollection and inherit both from there. Keep it DRY. :)
As a side note: you should have a look at MarionetteJS or build your own specialized views (Collection, and so on) just to save from typing the same over and over again.
I’m not sure I’ve totally understood your app, but here’s how I’ve done something similar before:
In your model add this:
matches: function(search) {
// a very simple and basic implementation
return this.get('name').indexOf(search) != -1;
}
And use it in UserListTreeView’s render:
render: function() {
var search = $someElement.val();
var _this = this;
_.each(this.collection.models, function(model) {
if (model.matches(search)) {
_this.addItem(model);
}
});
return this;
}
Very simple, yet effective. This is actually the most basic version to transfer the idea. You can improve this approach by extending it to other models and collections, checking for some edge cases, and improving its performance by simple optimizations.
I'm fairly new to Backbone and I'm working on a project that is probably not ideal for Backbone really, but I'm trying anyway! I'm not working with a RESTful API, so all of my data is fetched on page load and that is it, it is then not updated, at all. The data is sent through as a single lump of JSON.
I have set up a new model CookingItem which gets created for every item in the JSON and is then added to the CookingItemList collection, this is fine. However, I need to store information about the data set somewhere so I have created another 'Properties model' that stores this information (state information, start items etc). This properties model will also have several methods that manipulate the data.. these are then called from the view (is this OK?).
This is working fine, but I just want to be sure what I'm doing is considered a good way of doing things. I'm sure there may be a better way to handle the Properties model?
// Models
var CookingItem = Backbone.Model.extend(),
CookingProperties = Backbone.Model.extend({
defaults:{
startItem: 'anything',
currentItems:[]
},
setStartItem:function (val) {
// Code Here to edit this models properties
},
addRemoveItem:function (val) {
// Code Here to edit this models properties
}
}),
// Collection
CookingItemList = Backbone.Collection.extend({
model:CookingItem,
url:function () {
return "json.js";
}
}),
// View
CookingView = Backbone.View.extend({
el:'.page',
events:{
// Buttons in the UI
'click .link1':function () {
this.cookingProperties.addRemoveItem('item name');
},
'click .link2':function () {
this.cookingProperties.addRemoveItem('item name');
}
},
initialize:function () {
// Scope
var _this = this;
// Instantiate
this.cookingItemList = new CookingItemList(); // PlayListItems Collection.
this.cookingProperties = new CookingProperties(); // Properties Model.
// Bindings
this.cookingProperties.on('change:startItem', function () {
_this.customMethod1();
_this.customMethod2();
});
// Render the view
this.render();
},
render:function () {
var _this = this;
this.cookingItemList.fetch({
success:function () {
_this.cookingProperties.setStartItem('item');
_this.customMethod1();
}
});
return this;
},
customMethod1:function () {
// Do something
},
customMethod2:function () {
// Do something
}
}),
// Start
cookingView = new CookingView({
collection:CookingItemList
});