Preventing Marionette CompositeView render until fetch complete - javascript

I'm having a problem where render is being called autimatically in my Marionette CompositeView which is correct, the problem is that I'm fetching collection data in the initialize and want this to be present when the render happens. At the moment I'm running this.render() inside the done method of the fetch which re-renders but this causes problems as now I have 2 views per model. Can anyone recommend how I can properly prevent this initial render or prevent the duplicate views. 1 entry will output view1 and view2.
JS CompositeView
initialize: function() {
var self = this;
this.teamsCollection = new TeamsCollection();
this.teamsCollection.fetch().done(function() {
self.render();
});
},

First of all, I don't believe there is a way to stop rendering outright, but you have a bunch ways around that.
Option 1: fetch data first, then create your view and pass data into it when it's done.
//before view is rendered, this is outside of your view code.
var teamsCollection = new TeamsCollection();
teamsCollection.fetch().done(function(results) {
var options = {res: results};
var myView = new CompositeView(options);
myView.setElement( /* your element here */ ).render();
});
Option 2:
// don't use render method, use your own
initialize: function() {
var self = this;
this.teamsCollection = new TeamsCollection();
this.teamsCollection.fetch().done(function() {
self.doRender();
});
},
render: function(){}, // do nothing
doRender: function(){
// override render here rather than using default
}
Option 3: (if using template)
// if you have a template, then you can simply pass in a blank one on initialization
// then when the fetch is complete, replace the template and call render again
initialize: function() {
var self = this;
this.template = "<div></div"; // or anything else really
this.teamsCollection = new TeamsCollection();
this.teamsCollection.fetch().done(function() {
self.template = /* my template */;
self.render();
});
},
In reality I need more info. How is the view created? is it a region? is it added dynamically on the fly? Do you use templates? Can you provide any more code?

Related

Build view with new child views in regions

I use Marionette 3.0.
I have BlockView which could have variable number of regions.
These regions are filled with BlockRegionViews which are CollectionViews (they would be in 2.x) and these views render further BlockViews.
I would like to write a function that create a BlockView with it's regions already filled with (empty) BlockRegionViews, and than return this BlockView.
So i've written this code:
var blockView = new BlockView({model: blockModel});
var regions = blockModel.get('regions')
for(var i in regions) {
var blockRegionModel = regions[i];
var blockRegionView = new BlockRegionView({model: blockRegionModel});
blockView.addRegion(blockRegionModel.get('position'), '...regiondefinition...');
var region = blockView.getRegion(blockRegionModel.get('position'));
// This is the line where i get the error.
region.show(blockRegionView);
}
return blockView;
Of course my code is bad. Even the show function's name suggests that it's not the right function for me (since i don't want to show any views at this time), but I can't find anything like this in the documentation.
So my question is: How should i build a View with other views initialized in it's regions without rendering any of them?
If I understand you correctly, you want to render views within a view, and not render anything until it all has been initialized.
Your blockModel and region handling in your description was a bit confusing, so I hope you can apply this to your case.
The documentation around a View's lifecycle is helpful.
var ChildView = Mn.View.extend({..});
var ParentView = Mn.View.extend({
template: //..,
onBeforeRender: function() {
this.showChildView('reg1', new ChildView({model: someModel}));
},
regions: {
reg1: //..
}
});
In the above example the ChildView will be instantiated and attached to the ParentView before the ParentView has been rendered. However when the ParentView is rendered, the ChildViews will be instantiated and rendered in it's region.
You mentioned you don't want to render anything. That's a rare case, but you could instantiate child views outside of any rendering events.
var ChildView = Mn.View.extend({..});
var ParentView = Mn.View.extend({
template: //..,
initialize: function() {
this.childView = new ChildView({model: someModel});
},
// You can decide whenever you want to show the childView
// and invoke this method.
showTheChildView: function() {
this.getRegion('reg1').show(this.childView);
},
regions: {
reg1: //..
}
});
Hopefully I understood your question correctly.

How to replace jquery with the mithril equivalent?

Something like :
peer.on('open', function(id){ // this is a non jquery event listener
$('#pid').text(id);
});
With something like...this is not correct:
peer.on('open', function(id){
m('#pid',[id])
});
Is this even the right approach? Should I be establishing a controller and model before I attempt to convert from jquery?
More details:
I am trying to rewrite the connect function in the PeerJS example: https://github.com/peers/peerjs/blob/master/examples/chat.html
If your event listener is something like websockets, then the event happens outside of Mithril, which means you need to manage redrawing yourself. This is what you'll need to do:
Store your data in an independent model
Use that model when rendering your Mithril view
On the open event, update your model, then call m.redraw()
Conceptual example:
var myModel = { id: 'blank' }
var MyComponent = {
view: function () {
return m('#pid', myModel.id)
}
}
m.mount(document.getElementById('app'), MyComponent)
// This happens outside mithril, so you need to redraw yourself
peer.on('open', function(id) {
myModel.id = id
m.redraw()
})
In Mithril, you should not try to touch the DOM directly. Your event handler should modify the View-Model's state, which should be accessed in your View method. If you post more code, I could give a more detailed explanation of how it pieces together.
Here is a bare-bones example that shows the data flowing through Mithril. Your situation will need to be more complicated but I'm not currently able to parse through all of that peer.js code.
http://codepen.io/anon/pen/eNBeQL?editors=001
var demo = {};
//define the view-model
demo.vm = {
init: function() {
//a running list of todos
demo.vm.description = m.prop('');
//adds a todo to the list, and clears the description field for user convenience
demo.vm.set = function(description) {
if (description) {
demo.vm.description(description);
}
};
}
};
//simple controller
demo.controller = function() {
demo.vm.init()
};
//here's the view
demo.view = function() {
return m("html", [
m("body", [
m("button", {onclick: demo.vm.set.bind(demo.vm, "This is set from the handler")}, "Set the description"),
m("div", demo.vm.description())
])
]);
};
//initialize the application
m.module(document, demo);
Notice that the button is calling a method on the View-Model (set), which is setting the value of a property (vm.description). This causes the View to re-render, and the div to show the new value (m("div", demo.vm.description())).

How do I use fetched backbone collection data in another view?

Trying to learn Backbone and hitting a stumbling block when trying to fetch data, I fetch the data fine from with my view SearchBarView but once the data has been fetched I don't know how I can get this data in my SearchResultsView in order to template out each result?
Sorry if this sounds a little vague, struggling to get my head around this at the moment so could do with the guidance!
SearchBarView
performSearch: function(searchTerm) {
// So trim any whitespace to make sure the word being used in the search is totally correct
var search = $.trim(searchTerm);
// Quick check if the search is empty then do nothing
if(search.length <= 0) {
return false;
}
// Make the fetch using our search term
dataStore.videos.getVideos(searchTerm);
},
Goes off to VideoSearchCollection
getVideos: function(searchTerm) {
console.log('Videos:getVideos', searchTerm);
// Update the search term property which will then be updated when the url method is run
// Note make sure any url changes are made BEFORE calling fetch
this.searchTerm = searchTerm;
this.fetch();
},
SearchResultsView
initialize: function() {
// listens to a change in the collection by the sync event and calls the render method
this.listenTo(this.collection, 'sync', this.render);
console.log('This collection should look like this: ', this.collection);
},
render: function() {
var self = this,
gridFragment = this.createItems();
this.$el.html(gridFragment);
return this;
},
createItems: function() {
var self = this,
gridFragment = document.createDocumentFragment();
this.collection.each(function (video) {
var searchResultView = new SearchResultView({
'model': video
});
gridFragment.appendChild(searchResultView.el);
}, this);
return gridFragment;
}
Now I'm not sure how I can get this data within SearchResultView, I think I need to trigger an event from somewhere and listen for the event in the initialize function but I'm not sure where I make this trigger or if the trigger is made automatically.
Solution 1
If dataStore is a global variable then
SearchBarView
dataStore - appears like a global variable
videos - a collection attached to global variable
then in
SearchResultsView
this.listenTo(dataStore.videos, 'sync', this.render);
Solution 2
If dataStore is not a global variable
getVideos: function(searchTerm) {
console.log('Videos:getVideos', searchTerm);
// Update the search term property which will then be updated when the url method is run
// Note make sure any url changes are made BEFORE calling fetch
this.searchTerm = searchTerm;
var coll=this; //this should refer to the collection itself
this.fetch().done(function(){
var searchResultView = new SearchResultsView({collection:coll});
searchResultView.render();
});
},
It is not 100% clear how you are initializing your SearchResultView.
But, in order to have reference to the collection, can't you simply pass in the reference to the constructor of the view. Something like this:
// In your SearchbarView
var myCollection = new Backbone.Collection(); // and you are populating the collection somewhere somehow
var searchResultView = new SearchResultView(myCollection) // you just pass this collection as argument.
myCollection.bind("change", function(){
searchResultView.parentCollection = myCollection;
}
And inside your searchResultView you just refer this collection by parentCollection for instance.
If you make it more explicit as in how these 2 views are connected or related, I may be able to help you more. But, with given info, this seems like the easiest way.

templateHelpers in Marionette.CompositeView

I have no idea why this code is not working.
Reading the documentation,
the templateHelpers should be called.
My goal is to pass the this.collection.length to the template.
Any hints? thanks.
I am using Backbone.Marionette v0.9.5
return Marionette.CompositeView.extend({
className: 'user-board',
template: usersTemplate,
itemView: userItemView,
initialize: function () {
this.collection = new UseList();
this.collection.fetch();
},
appendHtml: function (collectionView, itemView) {
collectionView.$el.find('ul.users-list').append(itemView.el);
},
templateHelpers: function () {
console.log(this.collection.length);
},
serializeData: function () {
return {
weekly: this.options.weekly,
users_length: this.collection.length // here the length is zero
// after the fetch the length is > 0
// but in template remains 0
};
}
});
To fix my issue I have to make the following...
initialize: function () {
_.bindAll(this, 'render');
this.collection = new NewCollection();
this.collection.fetch({
success: this.render
});
}
Is there a better way to make it working?
Reading the Marionette Documentation serializeData method, which is the one using mixinTemplateHelpers, is only called on Item View.render method here and in your current code you do not render at all
UPDATE:
This way everytime the collection receives new data it will update your view the new length
initialize: function () {
_.bindAll(this, 'render');
this.collection = new NewCollection();
this.collection.fetch();
this.collection.bind('reset', this.render);
}
This code only declares the a view. Can you share the code the instantiates the view and displays it? templateHelpers will be called and the data passed to the template when the template is rendered. That is, you either need to show the view in a region which implicitly calls the render method on the view, or explicitly call the render method.
To be useful, templateHelpers should return an object. For instance:
templateHelpers: function() {
return {colLength: this.collection.length};
}
One important thing to keep in mind: fetch trigger an AJAX request that is done asynchronously. If you want to wait for the fetch to succeed before rendering the view, then you need to use Marionette.Async.
Update based on the update question
To avoid calling render from the view's initialize and only do it when render is called externally, change your code to:
return Marionette.CompositeView.extend({
className: 'user-board',
template: usersTemplate,
itemView: userItemView,
initialize: function () {
this.collection = new UseList();
var that = this;
this.defer = $.Deferred();
this.collection.fetch({
success: that.defer.resolve,
error: that.defer.resolve
});
},
appendHtml: function (collectionView, itemView) {
collectionView.$el.find('ul.users-list').append(itemView.el);
},
templateHelpers: function () {
console.log(this.collection.length);
// For greater flexibility and maintainability, don't override `serializeData`.
return {
weekly: this.options.weekly,
users_length: this.collection.length
};
},
render: function() {
var that = this,
args = arguments;
$.when(this.defer).done(function() {
Marionette.CompositeView.prototype.apply(that, args);
});
}
});
I'm resolving this.render both on success and error, otherwise if there is an error the view will never render (unless that's what you want).
Note that if you use Marionette.Async then you would return this.defer in the view's beforeRender and Marionette.Async would take care of delaying the rendering.
Also note that once this.defer is resolved, future renders will run when called as there is nothing to wait for, until this.defer has been reset programmatically.
At least in Marionette v1.0.3, I'm liking the pattern that rendering is handled automatically during a call to Region.show(), so I call that from a controller object which has the collection and passes it to the view on instantiation then shows the view. I don't even have to put this logic in a fetch success callback or explicitly bind to the 'reset' event, because the Marionette composite/collection view already knows to (re-)render itself on fetch success (which a debugger will show you).
After using a setup like has been detailed, you can also use template helpers a bit more usefully than has been described so far.
For example,
If you simply drop in <%= functionName %> into the template where you are trying to get the number to show up visually on the front end page (since you want .length I see), marionette will simply do the work for you.
So like this:
--Template File--
<div id="followerCount"> <%= showCount %> </div>
--Helper Function in View--
templateHelpers: {
showCount: function(){
return this.collection.length;
}
}
Hope that made sense or at least helps someone else perhaps looking for a simpler way to integrate database returned json to their templates.

Swap a view's model?

Basically I'm trying to figure out the best way to swap a model and react to that event.
class View extends Backbone.View
initialize: ()->
#do stuff
swapModel: (newModel)->
#model = newModel
view = new View({model:firstModel})
view.swapModel(newModel)
Is this all I have to do to swap out a view's model? Are there any other side effects I should plan for? What would be the best way to respond to this swap? Should I trigger a swap event in swapModel?
Thanks!
Don't swap models in a view. You'll run in to all kinds of problems related to DOM event, Model events in the view, etc. I've tried to do this a dozen times or more, and in every single case, I re-wrote my code so that I would create a new view instance for each model. The code was cleaner, simpler, easier to understand and easier to maintain and work with.
A very simple example of one way to do it. Why are you trying to swap models though?
MyView = Backbone.View.extend({
initialize: function() {
this.myTrigger = {};
_.extend(this.myTrigger, Backbone.Events);
this.myTrigger.on("modelChange", function(msg) {
alert("Triggered " + msg);
},this);
},
swapModel: function(model) {
// do something with model
// then trigger listeners
this.myTrigger.trigger("modelChange", "a model change event");
}
});
var myview = new MyView()
myview.swapModel()
You could use a collection that only allows one model. This way you don't touch the model and can call render as many times as you want. Something like this:
var SpecialCollection = Backbone.Collection.extend({
swap: function (model) {
//remove all models
this.reset();
//add one model
this.add(model);
}
});
var MyView = Backbone.View.extend({
initialize: function(){
this.listenTo(this.collection, 'add', this.render);
},
render: function() {
this.model = this.collection.first()
//do your normal rendering here
}
});
var c = new SpecialCollection();
var v = new MyView({collection: c});
c.swap({name: 'Sam'});
//view should render
c.swap({name: 'Dave'});
//view should render
You could lock down the Collection rules a bit further but I think it serves as a good example to get you going.

Categories

Resources