I'm looking for a better equivalent of:
if (view.myRegion.currentView) {
view.myRegion.reset();
} else {
var myOtherView = new Marionette.ItemView();
view.myRegion.show(myOtherView);
}
I went through the docs, but didn't find anything similar unfortunately.
From the docs - showing a view on marionette site, under the preventDestroy section,
If you replace the current view with a new view by calling show,
by default it will automatically destroy the previous view.
So perhaps, this could be helpful to you. Region manager would automatically destroy the existing view which is being held by that region and then render the new one.
If there's anything specific that you would like to do at the time of destroy, I believe you can write onDestroy callback for that view.
Related
Since I'm not totally sure on which level my issue actually is to be solved best, I'd like to summarise the path I went and the things I tried first:
It's more or less about $el (I think).
As most basic backbone examples state, I started with having the $el defined within its view, like
Invoice.InvoiceView = Backbone.View.extend({
el: $('#container'),
template: ..,
..
});
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
Following this article's advice, I switched to passing $el over to the view while calling the render()-method. Example:
$('#container').html( new WineListView({model: app.wineList}).render().el );
So far so good - but now render() gets called, while it maybe shouldn't (yet).
For example the View asynchronously fetches a model in its initialize()-routine. Adding a binding to reset or sync (e.g. like this.model.bind('sync', this.render, this)) makes sure, render() gets definitely called once the model is fetched, however above stated way, render() still might get called while the model isn't fetched yet.
Not nice, but working(TM), I solved that by checking for the model's existence of its primary key:
render: function() {
if(this.model.get('id')) {
...
}
However, what I didn't expect - and if it really isn't documented (at least I didn't find anything about it) I think it really should be - the fetch operation doesn't seem to be atomic. While the primary key ('id') might be already part of the model, the rest might not, yet. So there's no guarantee the model is fetched completely that way. But that whole checking seemed wrong anyway, so I did some research and got pointed to the deferred.done-callback which sounded exactly what I was looking for, so my code morphed into this:
render: render() {
var self = this;
this.model.deferred.done(function() {
self.model.get('..')
};
return this;
}
..
$('#container').html( new WineListView({model: app.wineList}).render().el);
It works! Nice, hu? Ehrm.. not really. It might be nice from the runtime-flow's point of view, but that code is quite cumbersome (to put it mildly..). But I'd even bite that bullet, if there wouldn't be that little, tiny detail, that this code sets (=replaces) the view instantly, but populates it later (due to the deferred).
Imagine you have two (full-page) views, a show and an edit one - and you'd like to instantly switch between the two (e.g. after hitting save in the edit-view it morphs into the show-view. But using above code it sets (=resets) the view immediately and then renders its content, once the deferred fires (as in, once fetching the model is completed).
This could be a short flickering or a long blank transition page. Either way, not cool.
So, I guess my question is: How to implement views, which don't know about their container, involve models which need to be fetched, views which should be rendered on demand (but only once the model is fetched completely), having no need to accept UI/UX trade-offs and - the cherry on the cake - having maintainable code in the end.
First of all, the first method you found is terrible (hard coding selector in view's constructor)
The second: new WineListView({model: app.wineList}).render().el is very common and ok. This requires you to return the reference to view from render method, and everyone seems to follow this, which is unnecessary.
The best method (imo) is to simply attach the views element to the container, like this
$('#container').html(new WineListView({model: app.wineList}).el);
The WineListView doesn't need to know about where it's going to be used, and whatever is initializing WineListView doesn't need to worry about when to render the WineListView view instance:
because the el is a live reference to an HTML Element, the view instance can modify it anytime it wants to, and the changes will reflect wherever it is attached in DOM/ when it gets attached in DOM.
For example,
WineListView = Backbone.View.extend({
initialize: function(){
this.render(); // maybe call it here
this.model.fetch({
success: _.bind(this,function(){
this.render(); // or maybe here
})
});
}
});
Regarding flickering: this hardly has to do anything with rendering or backbone, it's just that you're replacing one element with another and there will be an emptiness for a tiny bit of time even if your new view renders instantly. You should handle this using general techniques like transitions, loaders etc, or avoid having to switch elements (For example convert labels into inputs in the same view, without switching view)
First off, the linked example is outdated. It's using version 0.9.2,
whereas the current version (2016-04-19) is 1.3.3. I recommend
you have look at the change log and note the differences, there are many.
Using the el property is fine. Like everything though, there's a time and place.
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
I wouldn't define an el property on every view, but sometimes it makes sense, such as your example. Which is why, I'm assuming, Backbone allows the use of the el property. If you know container is already in the DOM, why not use it?
You have a few options:
The approach outlined in my original answer, a work-around.
fetch the model, and in the success callback, insert the view element into the DOM:
model.fetch({
success:function() {
$('#container').html(new View({model:model}).render().el);
}
});
Another work-around.
Define an el property on the view and fetch the model in the view initialize function. The new content will be rendered in the container element (also the view), when the content/model data is ready, by ready, I mean when the model has finished fetching from the server.
In short,
If you don't want to define an el property, go with number 1.
If you don't want to let the view fetch the model, go with number 2.
If you want to use the el property, go with number 3.
So, I guess my question is: How to implement views, which don't know about their container
In your example, I would use the el property, it's simple a solution with the least amount of code. Not using the el property here, turns into hacky work-arounds that involve more code (complexity) without adding any value (power).
Here's what the code looks like using el:
var Model = Backbone.Model.extend({url:'/model_url'});
var model = new Model();
// set-up a view
var View = Backbone.View.extend({
el:'#container',
template:'model_template',
initialize:function() {
this.model.fetch();
this.listenTo(this.model,'sync',this.render);
},
render:function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var view = new View({model:model});
Check out the documentation for el.
Here is an updated working example.
If there is an obvious flicker because, your model takes a noticeable amount of time
to be fetched from the server...maybe you should think about displaying a loading bar/variation thereof
while fetching the model. Otherwise instead of seeing the flicker, it will appear the
application is slow, delayed, or hanging..but in reality - it's waiting to render the next view,
waiting for the model to finish fetching from the server. Sitting on old content, just waiting for
the model to load new data to show new content.
In the SAPUI5 / OpenUI5 xmlfragment documentation the third parameter is a controller for handling actions from the fragment.
This is critical for a dialog fragment where there are buttons to press etc.
Most of the time I have seen this instantiated as this or sap.ui.getCore().byId('<element>').getController())
See an example at Fragment not get correct Controller
Because of the complexity in a particular dialog I would like to have a separate controller for it.
I have looked around at this and had a few attempts but so far not successful.
I have put a working example on github of using this.
But I would like to instantiate Dialog.js as the controller for the Dialog.fragment.xml from initial.view.controller
Any takers?
Pull requests gladly received.
The Crux of the example is as follows (this is the initial.controller.js) :
sap.ui.controller("sc.test.view.initial", {
oDialog: null,
openTestDialog: function(){
console.log("in open dialog");
// instantiate the other controller
var oDialogController = new sc.test.view.Dialog();
// this next commented line is the 'normal' way to do it
// oDialog = new sap.ui.xmlfragment( "sc.test.view.Dialog", this); //oDialogController);
// this is what I would like to achieve
oDialog = new sap.ui.xmlfragment( "sc.test.view.Dialog", oDialogController);
oDialog.open();
},
onCartDialogCancel:function(oEvent){
// this function would then be in the other controller but how to get a handle on the dialog?
oDialog.close();
}
});
Thanks.
(Just got to SYD airport)
All you're missing is the
jQuery.sap.require("sc.test.view.Dialog");
in your initial.controller.js.
Pushed a quick fix in a branch to your repo and opened a PR
only example i could find close to yours was in the Material Shortage Fiori app
oCtrl = sap.ui.controller("myapp.fragments.DirectCallDialog");
oDirectCallDialog = sap.ui.xmlfragment("myapp.fragments.DirectCallDialog", oCtrl);
lots of examples of injecting a controller when the fragment was called from a helper class. The helper class promotes reuse, eg same dialog fragment can be called from multiple views/components. The helper class method for the dialog setup is called from within a controller and the oController parameter is set to 'this'.
hth
jsp
I copied an existing controller.js, and renamed it.
Then, instantiated that as a below, and passed it through with the fragment.
var oNewController = new sap.ui.core.mvc.Controller("myProject.DialogController");
this._oDialog = sap.ui.xmlfragment("myPopup","myProject.fragments.myPopup", oNewController);
All eventing is now handled in oNewController, rather than the previously used "this"...
I'll start off with I'm new to these two frameworks but I'm really excited with what I've learned so far with them. Big improvement with the way I've been doing things.
I want to display a collection of items (trip itineraries) in say a table. I only want to display a couple of the itinerary fields in the table because there are many fields. When you edit/add an item, I would like to display a form to edit all the fields in a different region or in a modal and obviously when you save the list/table is updated. I'm unsure of the best way to structure this and am hoping someone could point me in the right direction or even better an example of something similar. My searches so far have came up short. It seems I should use a composite view for the list but how to best pass the selected item off to be edited is where I'm kinda stuck at. Any pointers would be much appreciated.
I'd use a CompositeView for the table, and an ItemView for the form. Clearly this is incomplete, but it should get you started. Actually... I kind of got carried away.
What follows are some ideas for the basic structure & flow. The implementation details, including templates, I'll leave to you.
The table/row views:
// each row in the table
var RowView = Backbone.Marionette.ItemView.extend({
tagName: "tr"
});
// This could be a table tag itself, or a container which holds the table.
// You want to use a CompositeView rather than a CollectionView so you can
// render the containing template (the table tag, headers), and also, it works
// if you have an actual collection model (ItineraryList, etc).
var TableView = Backbone.Marionette.CompositeView.extend({
itemView: RowView,
collectionEvents: {
"change": "render"
}
});
The form view:
// This would be a simple form wrapper that pulls default values from
// its model. There are some plugins in this space to help with
// forms in backbone (e.g. backbone-forms), but they may or may not
// be worth the bloat, or might be tricky to work with Marionette.
var FormView = Backbone.Marionette.ItemView.extend({
events: {
"submit form": "onFormSubmit"
},
data: function () {
// here you'd need to extract your form data, using `serializeArray`
// or some such, or look into a form plugin.
return {};
},
// Clearly this is incomplete. You'd need to handle errors,
// perhaps validation, etc. You probably also want to bind to
// collection:change or something to close the formview on success.
//
// Re-rendering the table should be handled by the collection change
// event handler on the table view
onFormSubmit: function (e) {
e.preventDefault();
if (this.model.isNew()) {
this.collection.create(this.data());
} else {
this.model.save(this.data());
}
}
});
Somewhere in your load process you'd instantiate a collection and show it:
// first off, somewhere on page load you'd instantiate and probably fetch
// a collection, and kick off the tableview
var itineraries = new Itineraries();
itineraries.fetch();
// For simplicity I'm assuming your app has two regions, form and table,
// you'll probably want to change this.
app.table.show(new TableView({collection: itineraries}));
Making routes for the edit and new itinerary links makes sense, but if your form is in a modal you might want to bind to button clicks instead. Either way, you'd open the form something like this:
// in the router (/itineraries/edit/:id) or a click handler...
function editItinerary(id) {
var itinerary = itineraries.get(id);
// then show a view, however you do that. If you're using a typical
// marionette region pattern it might look like
app.form.show(new FormView({
collection: itineraries,
model: itinerary
});
}
// once again, a route (/itineraries/new), or a click handler...
function newItinerary() {
app.form.show(new FormView({
collection: itineraries,
model: new Itinerary()
}));
}
New to using Backbone and have a very simple application. Basically there are Clients and ClientItems. I have a view to show all Clients and if you click on a Client you get taken to their ClientItems. Going to this ClientItems view should just hide the Clients view and going back to Clients should hide ClientItems. Now, in my render() function for each view, it is going through the collections and dynamically adding stuff to the page. When I go back and forth between the two (using the back button) I don't really need to fully render again as all the data is there in the page, just hidden. Where should this logic go? Right now I have it in the render() function but it feels sloppy, what is the preferred way of handling this?
We are using a global variable App with several common function used across application:
var App = {
initialize : function() {
App.views = {
clientView : new ClientsView(),
clientItemView : new ClientsItemsView()
}
},
showView: function(view){
if(App.views.current != undefined){
$(App.views.current.el).hide();
}
App.views.current = view;
$(App.views.current.el).show();
},
...
}
And then I use this App from other parts of application:
App.showView(App.views.clientView);
IntoTheVoid's solution is good – it's nice to have a single place to hide/show views. But how do you activate the logic?
In my experience, routers are the best place for this. When a route changes and the appropriate function is called, you should update the active, visible view(s).
What if you need multiple views to be visible at once? If you have a primary view that always changes when the route changes, and multiple subsidiary sticky views, you need not worry. But if it's more complex than that, think of creating a ComboView that neatly packages all the relevant views into one containing el node. That way the above logic still works, and your router functions are not littered with logic for managing what views are visible at the moment.
I'm wrapping up a Javascript widget in a Wicket component. I want to let the JS side talk to the component. What I've got so far:
Component in question goes like
talker = new GridAjaxBehavior();
this.add(talker);
in constructor
and then, later on, puts something like
"var MyGridTalker = new talker(" + this.talker.getCallbackUrl() + ");";
into the JS.
where GridAjaxBehavior extends AbstractDefaultAjaxBehavior. I want GridAjaxBehavior to spit back some XML when the JS calls it.
Am I doing this the right way? What should GridAjaxBehaviour do to spit back the XML?
Thanks
Spit back some XML for what? Presumably to update the model or the view, yes?
The strength of Wicket is that you don't have to worry about the rendered HTML. In Model-View-Controller terms, you set up the Controller to correctly modify the Model, and Wicket takes care of the View.
The separation is not entirely clear: in fact you can show/hide view components, or change then, and that can be seen as altering the View.
But what you generally don't have to do is directly manage the browser or javascript. Wicket takes care of that, if you take care of making your changes in the Java code.
In Wicket, the Ajax will call a method on your AjaxBehavior with an AjaxRequestTarget target.
In that method (or in methods called from it), you do whatever you need to do, updating models or views, and then you add to the target any view component that that has changed. Wicket takes care of updating the browser.
Here's an example. It's taken from some code I did, but heavily altered just to make explication clearer. The idea is simple: "chained" dropdown choices, where the options in the child change when the select option in the parent changes, as in the series of [State] [County] [District].
(In the actual class, the Model change is passed to the child, which decides for itself if it has changed, and adds itself to the target if it has, then passes the target to its child. I've removed most of that to make a clearer example.)
Here's the ctor, which just adds to itself an anonymous subclass of an AjaxBehavior:
public AjaxChildNotifyingDropDownChoice(...code elided for clarity...) {
this.child = child;
// Ajax won't work without this:
setOutputMarkupId(true);
//
add( new OnChangeAjaxBehavior() {
#Override
public void onUpdate(final AjaxRequestTarget target) {
// tell child to update its list
// based on newly selected value
// when the Ajax is called,
// my owning component's model
// is already updated
// note we could just type getModel()
// I'm making explicit that we're calling it
// on the enclosing class
// (which a non-static inner class has a hidden ref to)
child.setNewModelBasedOnSelectionOf(
AjaxChildNotifyingDropDownChoice.this.getModel());
// now add the child to the target
// Wicket javascript will receive the new
// options and re-render the child dropdown
target.add(child);
}
});
}
We could also have hidden or un-hidden components, or added behaviors like CSS styles, or even swapped one Panel for another. As long as for each changed component we:
1) called setOutputMarkupId(true); so that the javascript can find it, and
2) added it to the AjaxRequestTarget
Note that different types (subclases) of Ajax Behavior have different callback functions, so be sure you're overriding the right one (add an #Override annotation so the compiler can complain if you got the name wrong).
But again, the basic wicket idea is that instead of sending raw data for the client to parse and act on, you update your model and view, and tell Wicket to re-render what you've changed, by adding the chnaged components to the target.
The only reason I can think of to send straight XML would to be to feed it to non-Wicket javascript. Let me know if that's your aim, and I completely missed the point. ;)
I don't really know what Wicket is or what it does, but there is a minor bug in your code (as it appears).
This:
"var MyGridTalker = new talker(" + this.talker.getCallbackUrl();
You seem to be missing your end parens:
"var MyGridTalker = new talker(" + this.talker.getCallbackUrl() + ")";
Anyway, not a big deal, but didn't know if it was intentional.