So I've created a scroller control that I want to use in two different places within the same viewmodel for example:-
define(['common/viewmodels/controls/scroller-nav', 'common/viewmodels/controls/scroller-nav'],
function(mainScrollNav, modalScrollNav))
vm = {
activate: activate,
mainScrollControl: ko.observable(null),
modalScrollControl : ko.observable(null)
}
return vm;
function activate() {
vm.mainScrollControl({ model: mainScrollNav, view: 'common/views/controls/mainScroll' });
vm.modalScrollControl({ model: modalScrollNav, view: 'common/views/controls/modalScroll' });
// load up the data that is to be used for each (should be independent)
mainScrollNav.init();
modalScrollNav.init();
}
}
}
The control loads fine on both instances where mainScrollControl and modalScrollControl is populated however, the data is being shared (modify the scroller position in the modal and its also modified on the main page) even though the controls are defined separately. It seems like mainScrollNav and modalScrollNav link to a single service viewmodel as opposed to being independent viewmodels. Am I going about this right way or should I be using something else?
The solution was not to create a viewmodel, but a function of the view model so...
var control = function(){
vm = {
// Vars and functions
}
return vm;
}
return control;
Then the viewmodel can be resused as many times as needed just by calling the passed reference in the define paramaters. They both work independently as well.
define(['common/viewmodels/controls/scroller-nav'],function(scrollNav)){
vm = {
mainScroller: new scrollNav(),
subPageScroller: new scrollNav()
}
return vm;
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 building an Ember app which uses quite a few components. I'm also using Bootstrap. I've got a layout with tabs, and inside the second tab (which is hidden by default), the component (which contains a list of models which have a hasMany relationship with the main model) won't render.
I think I tracked this down to Ember Data resolving after the view is rendered, because if I click on another model of the list, these relations will show up.
Some info and details:
I have two main models:
Image
Crop
An image can have many crops.
I have an Images/Index controller which has this function:
loadCrops: function() {
var self = this;
this.get('selectedImage').get('crops').then(function(crops) {
self.set('selectedImageCrops', crops);
});
}.on('model.isFulfilled')
I added this method because I tried to manually resolve the relationship and get the crops for the image loaded in a variable but I had no luck with this. I'm passing the variables like this:
{{image-crops image=selectedImage crops=selectedImageCrops}}
This is my Index route:
export default Ember.Route.extend({
model: function () {
return this.store.find('image');
},
setupController: function(controller, model, request) {
controller.set('model', model);
}
});
If anyone needs more details please, ask for them. Thank you all!
When you use function() {}.on() you are telling Ember to execute that function when an event occurs. model.isFulfilled isn't an event though but a property so you need to observe it instead and do a quick check within the method that it really has been fullfilled (so it won't trigger if the promise is restarted for example)
loadCrops: function() {
if(!this.get('model.isFulfilled')) {
return;
}
var self = this;
this.get('selectedImage').get('crops').then(function(crops) {
self.set('selectedImageCrops', crops);
});
}.observes('model.isFulfilled')
Also as a side note I would suggest that you use an ES6 arrow function (which retains the outer this) instead of using var self = this, it's make the code a bit nicer.
loadCrops: function() {
if(!this.get('model.isFulfilled')) {
return;
}
this.get('selectedImage').get('crops').then((crops) => {
this.set('selectedImageCrops', crops);
});
}.observes('model.isFulfilled')
Try changing to a computed property:
selectedImageCrops: function() {
return this.get('selectedImage.crops');
}.property('selectedImage')
What I did in the end was moving the code to load the crops to the route's model, and returning an Ember.RSVP.hash with both the images and the crops, and then assigning it to the controller:
export default Ember.Route.extend({
/**
* Returns an Image instances array
*
* #method model
* #return {Ember.RSVP.hash} Images & Crops
*/
model: function () {
return Ember.RSVP.hash({
images: this.store.find('image'),
crops: this.store.find('crop')
});
},
/**
* Takes care of setting the needed variables in the controller
*
* #method setupController
*/
setupController: function(controller, model, request) {
controller.set('model', model.images);
controller.set('crops', model.crops);
}
});
Then I added a helper function in the controller to get the current image's crops:
selectedImageCrops: function() {
return this.get('crops').filter((obj) => {
return obj.image === this.get('selectedImage');
})[0];
}.property("selectedImage")
Thanks to #Karl-Johan Sjögren for the tip on the arrow function!
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.
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
}
});