Backbone.Collection.reset() => child view is out of sync with parent - backbone.js

I have a list of items. They are stored in backbone pageable collection.
They are displayed like this
|---item1---------------------------|
|---item2---------------------------|
|---item3---------------------------|
|---item4---------------------------|
|---item5---------------------------|
|---item6---------------------------|
|---item7---------------------------|
<< 1,2,3...end >>
User can click on individual item to open detail view in a separate page. Detail view has listeners initialized
when it's created. Those listeners are bound to the item model.
Since the detail view is huge, I cache it in the DOM by toggling the visibility.
The subsequent click on the item will toggle the cached view.
------ here is the problem -----
When item list is switched to another page, the collection is reset (by paginator). And all the models previously stored in the collection is dereferenced and
a new set of models is created. So after the page is switched back and forth, the previously opened item has a different copy of itself stored
in the collection. So when I change the name of the item in the detail view (in the view cache), the name in the item list is not changed.
The views are out of sync! because they are referencing to different models.
Not sure if anyone else encounter this before. If you do, please share with me how you solve it.
Thanks very much.

The most straight-forward way to maintain a fresh reference between your list view items and the corresponding detail view, on page change, is to re-render the detail view. But I'm assuming this options is not acceptable within the scope of your project.
What I often do, when I have the task of forming relationships within logically separate views is use listeners. As long as the views share a unique identifier (for example, they both share a model, or at least identical model ids), I can always send a message that will reach the view I'm interested in.
For this you'll need a centralized event hub, which with Backbone is trivially easy to generate. In some appropiately global variable (like, for example, MyApp) we simply do:
MyApp.EventBus = _.extend({}, Backbone.Events);
Set up the detail view
On the detail view initialize function I would drop this listener,
initialize: function () {
// Listen to a toggle visibility on this view
this.listenTo(MyApp.EventBus, 'detail-view:toggle-view', toggleView);
},
toggleView: function (id) {
if (this.model.id == id) {
// Show this view if I have the passed id
this.$el.show()
// Notify the parent list item view that its detail view exists
MyApp.EventBus.trigger('detail:view:exists', true);
} else {
// Hide all other views
this.$el.hide();
}
},
changeName: function () {
// logic that parses DOM user input to
// local variable name
// We now trigger an event 'detail-view:change:name', and we send as
// parameters our model's id and the new name
MyApp.EventBus.trigger('detail-view:change:name', this.model.id, name);
}
Setting up the list item view
The list item view will want to listen to a name change (or any other model property in the detail view that you want the list item to be aware of). So we'll set up a handler for the 'detail-view:change:name' event.
We'll also want to wire our click handler to toggle the visibility of the list item's detail view. The tricky part is to handle the event that a view has not been rendered yet (I'm assuming you're lazy loading the detail view). So we set up a second listener for the detail:view:exists event the detail view triggers when it catches a detail-view:toggle-view event. If we don't hear the detail:view:exists event from the targeted detail view in a timely manner (I'm using 100 ms, but you can play around with that to suit your needs), then we render the view.
initialize: function () {
// Listen to when the detail associated with this list item changes
// the the list item name
this.listenTo(MyApp.EventBus, 'detail-view:change:name', onNameChange);
// Set a property in this view if its detail view exists
this.listenTo(MyApp.EventBus, 'detail:view:exists',
_.bind(function () { this.detailViewExists = true; }, this));
// Create a debounced function that tests whether this view's
// detail view exists
_.debounce(_.bind(this.handleViewState, this), 100);
},
events {
click: 'toggleDetailView'
},
toggleDetailView: function (id) {
MyApp.EventBus.trigger('detail-view:toggle-view', this.model.id);
this.handleViewState();
},
// Debounced function that will wait 100ms asynchronously for the
// detail view to respond. If the detailViewExists bit is not set to true
// then we assume the view does not exist and we render it
handleViewState: function () {
if (!this.detailViewExists)
// The view does not exist, render and attach the view
// Set the bit to false to allow testing in the event that the detail view
// is destroyed in the future
this.detailViewExists = false;
},
changeName: function (id, newname) {
if (this.model.id == id) {
// Change the name of this list item view
this.$('.item-name').text(newname);
}
The take-away
Now, the reference between these two disparate views is the shared unique identifier. Since, by design, these two identifiers are unique in their scope, and should not change, and assuming the detail view has been rendered and attached to the DOM, then regardless of the rendering its state the list item view will always be able to communicate with its detail view.

Related

AngularJS $scope variable loses value after $rootscope.$on function executes

I have an AngularJS single-page application. We display a list of items in a table and users can drill-down to view the detail for a specific item. Some items have child items that can be drilled down as well. To visualize this consider a list of customers that can be drilled down to view customer detail. Each customer may have a list of orders and we can drill down to view order details.
This is the hierarchy of the data flow:
List of customers -> customer detail -> list of customer orders -> order detail
I have no trouble drilling down through this hierarchy but our users have requested "breadcrumbs" to be able to go back to an item's parent, for example, to be able to jump between a specific customer's order detail to the customer detail view.
This being a single-page application has made this a bit tricky but I have come up with a design which I thought would work but I'm having a bit of trouble with it. I'll spare you the gory details of how I am attempting to do that but the root of the problem is I need to pass data from the main view to the child view. I am using $rootscope.$broadcast to do this.
It all boils down to this. I can use the $rootscope.$on function to listen for the event raised by the $rootscope.$broadcast function and grab the data, in this case is that I want to view the detail for customer XYZ. However, I also need to perform some initialization in the child controller's "setup" function which is triggered by calling $(document).ready(setup); When I place a breakpoint in my code it seems that the $rootscope.$on function executes before the setup function executes. To attempt to get around this I tried setting storing that value as $scope.customerID $on function and reading that value in my setup function. For some reason $scope.customerID is undefined when I attempt to read the value. As a result, the "showDetail" function never executes.
MainController code:
function navigate(viewTemplate, itemID) {
$scope.activeViewTemplate = viewTemplate; // Switches views specified by ng-include
// (e.g. viewTemplate = 'customers.html', itemID = 12345)
// If itemID is specified, show detail for that item
if (itemID)
$rootscope.$broadcast('viewDetail', { id: itemID });
}
CustomersController code:
// Calls the setup function when the view loads
$(document).ready(setup);
// Attach the event handler for the viewDetail event
$rootscope.$on('viewDetail', viewDetail);
// Handle the viewDetail event
function viewDetail(event, data) {
if (data)
$scope.customerID = data.id; // <-- Debugger shows this has a value
}
// Perform initialization and display customer detail of customerID is specified
function setup() {
// initialization code goes here
if ($scope.customerID) // <-- Always undefined!
showDetail($scope.customerID);
}
Sorry if this is a bit complex. Basically I need to figure out why $scope.customerID doesn't retain it's value between the "viewDetail" and "setup". I've checked the rest of the code in my Customers controller and $scope.customerID isn't being set/cleared anywhere else. If I can't get this figured out I'll need to scrap my design and come up with something else.

Collection create function firing add event too quickly

(Using Backbone 0.9.10)
I have a list view of sorts where the user can click a button to show a modal view. The list view has a counter that shows the amount of items contained in the list. Clicking a button in the modal view executes create on a collection that is passed into both views. This fires the add event in the list view, which in turn runs this function:
renderCount: function () {
// Container for model count
var $num = this.$el.find('.num');
if ($num.length > 0) {
// 'count' is a value returned from a service that
// always contains the total amount of models in the
// collection. This is necessary as I use a form of
// pagination. It's essentially the same as
// this.collection.length had it returned all models
// at once.
$num.html(this.collection.count);
}
}
However, add seems to be fired immediately (as it should be, according to the docs), before the collection has a chance to update the model count. I looked into sync but it didn't seem to do much of anything.
How can I make sure the collection is updated before calling renderCount?
Here's the list view initialize function, for reference:
initialize: function (options) {
this.collection = options.collection;
this.listenTo(this.collection, 'add remove reset', this.renderCount);
this.render();
}
EDIT:
Turns out I was forgetting to refetch the collection on success in the modal view.
$num.html(this.collection.count);
shoule be:
$num.html(this.collection.size());
Backbone.Collection uses methods imported from underscore, here is list: http://backbonejs.org/#Collection-Underscore-Methods
Turns out I was forgetting to refetch the collection on success in the modal view.

Backbone.marionnette - Rebinding events vs creating new view

I have a Layout that has several tabs. Clicking one of these tabs will show the appropriate composite view in the page's content region. After navigating back and forth between different tabs I noticed that the composite views have lost their native bindings to render on collection reset and model changes.
Is there a way I should be rebinding the events being used in _initialEvents of a composite view when showing a view for a second time, or should I be creating a new composite view every I show a tab?
Currently I am creating all my views in initialize of my Layout and then using show with the view when a tab is clicked.
initialize: function(){
_.bindAll(this);
// Tabs
this.places_page = new Places_Layout();
},
show_places_page: function(){
this.content.show(this.places_page);
this.places_page.delegateEvents();
},
You don not have to create a Layout/Item/Composite/Collection view each time you switch from tab to tab, on the contrary you can save the content in a variable just the way you are doing, the problem you have is that the variable is being re-declared each time you want to render the content.
The solution is that you have to verify if that variable (this.places_page) is declared if not append it to the view so when you call it more times it will be holding the same layout view without any problem, just note that when you render the main view (the one holding the regions) the nested child views(in regions) will be lost until new navegation through them.
initialize: function(){
_.bindAll(this);
// You can asign a diferent variable for each view so when you call show_places_page it will render with the same view.
if (!this.places_page){
this.places_page = new Places_Layout();
}
// other tab
if (!this.other_page){
this.other_page = new OtherPage_Layout();
}
},
show_places_page: function(){
this.content.show(this.places_page);
this.places_page.delegateEvents();
},
This does not sound like the best approach to me.
You should use the layout's region managers to show views without needing functions like you have defined.
I would go for this approach
var view = new CustomView();
layout.content.show(view);`
then later on:
var newView = new SecondCustomView();
layout.content.show(newView);
If you want to continue down the road that you are on then you would probably be best to use this approach:
initialize: function () {
_.bindAll(this);
},
show_places_page: function () {
var placesLayout = new Places_Layout();
this.content.show(placesLayout);
}
Does that make sense?
Its hard to suggest the best course of action without seeing more structure around this.
Is there a reason that you are creating the views in initialize?
Marionette(v.1) onwords uses Backbone.BabySitter to manage child views .
In your case you do the same.
Just create a containter to store all tab view. Later query the container to return the view you need to display.
this.tabViewsContainer = new Backbone.ChildViewContainer();
this.tabViewContainer.add(new CustomView(),'tab1');
this.tabViewContainer.add(new SecondCustomView(),'tab2');
To Later Show the view just do this
var custv = container.findByCustom("tab1");
this.content.show(custv);
In close method your layout view successfully close all view in container
this.tabViewsContainer.each(function(view){view.close()});
You should not create all the views inside the initialize as this will cause you memory leaks that's why you should do dynamic creation of the views. Also I would suggest create a common function for showing a view in your content region to increase the code re-usability. I would suggest you something like following solution:
//define the regions of your layout view
regions: {
content: '#content'
},
//Maintain a config for the tab content view classes.
contentViews: {
tab1: Tab1View,
tab2: Tab2View,
tab3: Tab3View
},
//keeps all the view instances
viewInstances: {},
/*
* show tab function is called when you click a tab item.
* Consider each tab has a attribute for tab name.
* For example HTML of your one tab is like:
* <div data-tab-name="tab_name">Tab <tab_name></div>
*/
showTab: function (e) {
var tabName = $(e.currentTarget).attr("data-tab-name");
/*
* code for showing selected tab goes here...
*/
//check and create the instance for the content view
if (!this.viewInstances[tabName]) {
this.viewInstances[tabName] = new this.contentViews[tabName]();
}
//Then here you are actually showing the content view
this.content.show(this.viewInstances[tabName]);
this.viewInstances[tabName].delegateEvents(); //this is to rebind events to the view.
}

How should 2 seemingly independent views under the same parent interact

Please consider following scenario:
<ParentView>
<FilterSubview></FilterSubview>
<ListSubview></ListSubview>
</ParentView>
To give you and example: I have a view which in turn shows view with filter (user can select to display books, magazines or both of them) and the list with items.
Both filter and list have corresponding models. Filter - what can we filter. List - list of all items.
Use case: user sees the full list and then can filter results by selecting only desired category.
Questions:
How those two views should interact? Should they know about each other or should parent view handle it?
Who should store filtered list to display? It could be list subview model directly or parent view can filter complete list and then pass it to render.
There is no one correct answer to your questions, but I'll try to explain a common, idiomatic way here.
Two sibling views should not know of each other. Instead they should interact via events through some kind of a mediator. Since in your case both FilterView and ListSubView share a common parent view which is responsible for rendering both of them, you could let the parent view mediate the events:
var ParentView = Backbone.View.extend({
initialize: function() {
this.listenTo(this.filterView, "filter", this.filterChanged);
},
filterChanged: function(filterValue) {
this.listSubView.filter(filterValue);
}
});
var FilterView = Backbone.View.extend({
events: {
"change .filter" : "filterValueChanged"
},
filterValueChanged: function() {
var filterValue = //get filter value...
this.trigger("filter", filterValue);
}
});
Alternatively (preferrably, even) you can cut out a middle man and use the Mediator pattern. For that you need a third component whose job it is to pass messages between parties who should not know of each other. If you're using Backbone 0.9.9, there's just such a mediator built in: the Backbone root object works as a global event bus for this purpose.
So:
//ListSubView
this.listenTo(Backbone, "listfilterchanged", this.filterChanged);
//FilterView
Backbone.trigger("listfilterchanged", filterValue);
Then there's the question of who should be responsible of the list data. I tend to prefer to have the most specialized component be in charge, but so that only one component is in charge. In your case that would mean that the ListSubView should manage the filtered list, but only if the ParentView doesn't need to operate on it. That's just a generalization though, so take it with a grain of salt and do what feels right for your case.

how to handle multiple views within a single view with backbonejs

I'm new to Backbone and I don't fully understand it yet, and I've come across a situation I can't find any documentation on. What I if I have a view that contains multiple views? For example, I have a view called StackView. The purpose of this view is to neatly lay out a set of cards. It manages the animation of adding, removing, and adjusting cards in the stack. Each card is a CardView. How would I handle this? I've seen people talk about views within views by simple creating a variable in the view and assigning the View instance to that variable. Should I just be adding an array of CardViews in a variable of a StackView?
That's what I do, and it works well. Here's a snippet of a View I use in an application. I've re-written it back into regular javascript from my coffeescript, so I apologize for any typos:
render: function() {
var _this = this;
this.$el.html(this.template());
this.listItemViews = [];
// for each model in the collection, create a new sub-view and add
// it to the parent view
this.collection.each(function(model){
var view = new App.Views.Projects.ListItem({model:model}); // create the new sub-view
_this.listItemViews.push(view); // add it to the array
_this.$('#project-table tbody').append(view.render().$el); // append its rendered element to the parent view's DOM
});
return this;
}
This lets my Table view maintain a reference to all the listItemView views.
Of course, if you do this, you should make sure to properly remove these child views and unbind any events when you remove the parent view.

Resources