Build view with new child views in regions - javascript

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.

Related

Preventing Marionette CompositeView render until fetch complete

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?

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())).

Unable to create a new instance of a Backbone View

I have a "master" view, a layout if you will, that loads other views through the loadView method. My problem is that these view classes perform some initialisation logic (in initialize) that can only be performed once (to do with templating). If, however, I try and instantiate these classes more than once, I get an error symptomatic of calling initialize on the same instance.
I have tried, in the console, instantiating them separately by loading the class and creating two new instances using var x = new MyViewClass(); but each time the first one instantiates and the second one fails because of this error caused by the templates already being initialised.
This really shouldn't be happening, but I cannot for the life of me see what is causing the problem.
The layout's loading code is below.
loadView: function(name, bootstrap_function) {
this.unloadView();
var _class = require('View/'+name), // Let's load the view file
pretty = name.replace('/', ''), // Prettify the name by removing slashes (should end up with some camelcased niceness)
bs_name = '__bootstrap'+pretty, // Generate the name of the bootstrap function
view = new _class(); // Pass the event aggregator in
// If there is a bootstrap function, bootstrap
if(typeOf(bootstrap_function) == 'function') { // Check if one has been passed in
bootstrap_function.call(this, view); // Bootstrap: function(AppView, LoadedView)
}
this._loaded = view; // Store the view in _loaded
// Now that we have a view to play with
// we should insert it into our container object
view.$el.appendTo(this.$container);
// And render!
view.render();
},
unloadView: function() {
if(this._loaded !== null) {
this._loaded.remove();
this._loaded.unbind();
this._loaded = null;
}
}
EDIT
The templating code that is having the errors is this:
processTemplates: function() {
if(this.templates === undefined) return this;
console.log(this.templates);
if(Object.getLength(this.templates) > 0) {
Object.each(this.templates, function(template, name) {
this.templates[name] = _.template(template);
}, this);
}
return this;
},
The console.log(this.templates) output shows that on the first initialisation, this.templates contains strings, as it should, but on second initialisation it shows template functions (which should only be the case after processTemplates() is called.
I wonder if it could have anything to do with the way my class is defined, for example:
define(
['backbone', 'View/Kords', 'text!Template/Pages/Landing.html', 'Collection/TenantTypes'],
function(Backbone, KordsView, landing_html, TenantTypesCollection) {
var LandingView = KordsView.extend({
tagName: 'div',
className: 'tiled-light',
templates: {
'main': landing_html
},
landing_html is defined like this in the class, but could there be a reference problem? _.template should not be affecting the value of landing_html within the scope, should it?
EDIT #2
It is not to do with the reference to landing_html. I tried just setting templates.main to a string in the class definition but I still got the errors as before.

Delegating events to a parent view in Backbone

My view, TuneBook, has several child views of type ClosedTune. I also have separate full page views for each tune, OpenTune. The same events are bound within ClosedTune and OpenTune, so I've designed my app so that they both inherit from a shared 'abstract' view Tune.
To make my app more scaleable I would like the events for each ClosedTune to be delegated to TuneBook, but for maintainability I would like the same handlers (the ones stored in Tune) to be used by TuneBook (although they'd obviously need to be wrapped in some function).
The problem I have is, within TuneBook, finding the correct ClosedTune to call the handler on. What's a good way to architect this, or are there other good solutions for delegating events to a parent view?
Note - not a duplicate of Backbone View: Inherit and extend events from parent (which is about children inheriting from a parent class, whereas I'm asking about children which are child nodes of the parent in the DOM)
In your parent view (extending also from Backbone.Events), I would bind onEvent to the DOM event. On trigger, it would fire a backbone event including some "id" attribute that your child views know (presumably some row id?).
var TuneBook = Backbone.View.extend(_.extend({}, Backbone.Events, {
events: {
"click .tune .button": "clickHandler"
},
clickHandler: function (ev) {
this.trigger('buttonClick:' + ev.some_id_attr, ev);
},
}));
Child views would then naturally subscribe to the parent views event that concerns them. Below I do it in initialize passing the parent view as well as that special id attribute you used before in options.
var ClosedTune = Backbone.View.extend({
initialize: function (options) {
options.parent.on('buttonClick:' + options.id, this.handler, this);
},
handler: function (ev) {
...
},
});
You can of course also set up similar subscribers on Tune or OpenTune.
Here are a couple of possibilities.
1. Centralized: store ClosedTune objects in the TuneBook instance
Store a reference to each ClosedTune in tune_book.tunes. How you populate tune_book.tunes is up to you; since you mentioned an adder method on TuneBook, that's what I've illustrated below.
In the TuneBook event handler, retrieve the ClosedTune from tune_book.tunes by using something like the id attribute of the event target as the key. Then call the Tune or ClosedTune handler.
http://jsfiddle.net/p5QMT/1/
var Tune = Backbone.View.extend({
className: "tune",
click_handler: function (event) {
event.preventDefault();
console.log(this.id + " clicked");
},
render: function () {
this.$el.html(
'' + this.id + ''
);
return this;
}
});
var ClosedTune = Tune.extend({});
var OpenTune = Tune.extend({
events: {
"click .button" : 'click_handler'
}
});
var TuneBook = Backbone.View.extend({
events: {
"click .tune .button" : 'click_handler'
},
click_handler: function (event) {
var tune = this.options.tunes[
$(event.target).closest(".tune").attr('id')
];
tune.click_handler( event );
},
add_tune: function (tune) {
this.options.tunes[tune.id] = tune;
this.$el.append(tune.render().el);
},
render: function () {
$("body").append(this.el);
return this;
}
});
var tune_book = new TuneBook({
tunes: {}
});
[1, 2, 3].forEach(function (number) {
tune_book.add_tune(new ClosedTune({
id: "closed-tune-" + number
}));
});
tune_book.render();
var open_tune = new OpenTune({
id: "open-tune-1"
});
$("body").append(open_tune.render().el);
2. Decentralized: associate the view object with the DOM object using jQuery.data()
When you create a ClosedTune, store a reference to it, e.g. this.$el.data('view_object', this).
In the event listener, retrieve the ClosedTune, e.g. $(event.target).data('view_object').
You can use the same exact handler for ClosedTune (in TuneBook) and OpenTune, if you want.
http://jsfiddle.net/jQZNF/1/
var Tune = Backbone.View.extend({
className: "tune",
initialize: function (options) {
this.$el.data('view_object', this);
},
click_handler: function (event) {
event.preventDefault();
var tune =
$(event.target).closest(".tune").data('view_object');
console.log(tune.id + " clicked");
},
render: function () {
this.$el.html(
'' + this.id + ''
);
return this;
}
});
var ClosedTune = Tune.extend({
initialize: function (options) {
this.constructor.__super__.initialize.call(this, options);
}
});
var OpenTune = Tune.extend({
events: {
"click .button" : 'click_handler'
}
});
var TuneBook = Backbone.View.extend({
events: {
"click .tune .button": Tune.prototype.click_handler
},
add_tune: function (tune) {
this.$el.append(tune.render().el);
},
render: function () {
$("body").append(this.el);
return this;
}
});
var tune_book = new TuneBook({
tunes: {}
});
[1, 2, 3].forEach(function (number) {
tune_book.add_tune(new ClosedTune({
id: "closed-tune-" + number
}));
});
tune_book.render();
var open_tune = new OpenTune({
id: "open-tune-1"
});
$("body").append(open_tune.render().el);
Response to comment
I considered option 1 but decided against it as I already have a collection of tune models in the tunebook and didn't want another object I'd need to keep in sync
I guess it depends what kind of housekeeping / syncing you feel the need to do, and why.
(e.g. in TuneModel.remove() I would need to remove the view from tunebook's list of views... would probably need events to do this, so an event only solution starts to look more attractive).
Why do you feel that you "need to remove the view from tunebook's list of views"? (I'm not suggesting you shouldn't, just asking why you want to.) Since you do, how do you think #ggozad's approach differs in that respect?
Both techniques store ClosedTune objects in the TuneBook instance. In #ggozad's technique it's just hidden behind an abstraction that perhaps makes it less obvious to you.
In my example they're stored in a plain JS object (tune_book.tunes). In #ggozad's they're stored in the _callbacks structure used by Backbone.Events.
Adding a ClosedTune:
1.
this.options.tunes[tune.id] = tune;
2.
this.on('buttonClick:' + tune.id, tune.handler, tune);
If you want to get rid of a ClosedTune (say you remove it from the document with tune.remove() and you want the view object gone completely), using #ggozad's approach will leave an orphaned reference to the ClosedTune in tune_book._callbacks unless you perform the same kind of housekeeping that would make sense with the approach I suggested:
1.
delete this.options.tunes[tune.id];
tune.remove();
2.
this.off("buttonClick:" + tune.id);
tune.remove();
The first line of each example is optional -- depending if you want to clean up the ClosedTune objects or not.
Option 2 is more or less what I'm doing right now, but (for other reasons) I also store the model as a data attribute on view.$el, and I can't help feeling that there's got to be a better way than storing references all over the place.
Well, it ultimately comes down to your preference for how to structure things. If you prefer storing the view objects in a more centralized fashion, you can store them in the TuneBook instance instead of using jQuery.data. See #1: Centralized.
One way or another you're storing references to the ClosedTune objects: using jQuery.data, or in a plain object in the TuneBook, or in _callbacks in the TuneBook.
If you like #ggozad's approach for reasons that you understand, go for it, but it's not magic. As it's presented here I'm not sure what advantage is supposed to be provided by the extra level of abstraction compared to the more straightforward version I present in #1. If there is some advantage, feel free to fill me in.
Great solution I have taken from this article (#dave-cadwallader comment).
Extend an general backbone events object and store it in a reference vent:
var vent = _.extend({}, Backbone.Events);
Pass it to parent view:
var parentView = new ParentView({vent: vent});
The child view will trigger an event:
ChildView = Backbone.View.extend({
initialize: function(options){
this.vent = options.vent;
},
myHandler: function(){
this.vent.trigger("myEvent", this.model);
}
});
And the parent view is listening to the child event:
ParentView = Backbone.View.extend({
initialize: function(options){
this.vent = options.vent;
this.vent.on("myEvent", this.onMyEvent);
let childView = new ChildView({vent: this.vent});
},
onMyEvent: function(){
console.log("Child event has been ");
}
});
Disclaimer - pay attention that the vent object has to be injected to every view so you will find in this article better design patterns to make use of.

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