I have a single page web app with multiple backbone.js views. The views must sometimes communicate with each other. Two examples:
When there are two ways views presenting a collection in different ways simultaneously and a click on an item in one view must be relayed to the other view.
When a user transitions to the next stage of the process and the first view passes data to the second.
To decouple the views as much as possible I currently use custom events to pass the data ($(document).trigger('customEvent', data)). Is there a better way to do this?
One widely used technique is extending the Backbone.Events -object to create your personal global events aggregator.
var vent = {}; // or App.vent depending how you want to do this
_.extend(vent, Backbone.Events);
Depending if you're using requirejs or something else, you might want to separate this into its own module or make it an attribute of your Application object. Now you can trigger and listen to events anywhere in your app.
// View1
vent.trigger('some_event', data1, data2, data3, ...);
// View2
vent.on('some_event', this.reaction_to_some_event);
This also allows you to use the event aggregator to communicate between models, collections, the router etc. Here is Martin Fowler's concept for the event aggregator (not in javascript). And here is a more backboney implementation and reflection on the subject more in the vein of Backbone.Marionette, but most of it is applicable to vanilla Backbone.
Hope this helped!
I agree with #jakee at first part
var vent = {};
_.extend(vent, Backbone.Events);
however, listening a global event with "on" may cause a memory leak and zombie view problem and that also causes multiple action handler calls etc.
Instead of "on", you should use "listenTo" in your view
this.listenTo(vent, "someEvent", yourHandlerFunction);
thus, when you remove your view by view.remove(), this handler will be also removed, because handler is bound to your view.
When triggering your global event, just use
vent.trigger("someEvent",parameters);
jakee's answer suggests a fine approach that I myself have used, but there is another interesting way, and that is to inject a reference to an object into each view instance, with the injected object in turn containing references to as many views as you want to aggregate.
In essence the view-aggregator is a sort of "App" object, and things beside views could be attached, e.g. collections. It does involve extending the view(s) and so might not be to everybody's taste, but on the other hand the extending serves as a simple example for doing so.
I used the code at http://arturadib.com/hello-backbonejs/docs/1.html as the basis for my ListView and then I got the following to work:
define(
['./listView'],
function (ListView) {
var APP = {
VIEWS : {}
}
ListView.instantiator = ListView.extend({
initialize : function() {
this.app = APP;
ListView.prototype.initialize.apply(this, arguments);
}
});
APP.VIEWS.ListView = new ListView.instantiator();
console.log(APP.VIEWS.ListView.app);
}
);
Related
I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.
Consider, if you will, an app with a few unique views/states - let's call it a game. You have an overworld screen, a battle screen, a multiplayer interface, and maybe a minigame or two.
For the sake of argument, there isn't a lot of code in common between each view, so it lends itself well to AMD - a central controller/dispatcher, and each game state split into a separate file/view.
dispatcher.core.js
> overworld.view.js
> battle.view.js
> tournament.view.js
> minigame.view.js
Input and key commands get routed to the dispatcher, and trickle down to the current active view, which in turn manipulates the DOM as needed. One-way AMD relationships, so far so good.
The thing I'm getting hung up on is the response flow. The API response data that goes through the system is diverse, often affecting multiple views at the same time. Consider this case:
User presses buttons to move
Key commands gets routed to map view for movement animation
Map sends AJAX request to server for movement result
AJAX returns "battle commence" response to dispatcher
Dispatcher tells map view to disable itself, then battle view to init
The dispatcher was designed for this - to receive instruction and distribute. It seems like the obvious choice, much more than letting views affect each other directly.
However, there's a fundamental flaw here - the one-way relationship between the dispatcher and the views is violated as soon as the AJAX result is sent from the view to the dispatcher. You can either use the dispatcher for your AJAX callback, or you can instruct the dispatcher to make the AJAX call for you - but either way the view requires a way to reference the dispatcher, which as I understand it, violates the core tenet of AMD. For the life of me, I can't figure out how this would be implemented correctly!
My question is this - how would one implement such a structure correctly? Is this a limitation of AMD, or am I misunderstanding it's use on a deeper level?
This question is intended to be for more of the general case, but if it affects answers at all, I'm using Require and jQuery for AMD and AJAX, respectively.
Is this a limitation of AMD, or am I misunderstanding it's use on a deeper level?
AMD does not by any means impose one way relationship between object instances in general. What it does strongly recommend to avoid (because even this is not an absolute requirement) is circular dependencies between modules. And the type of dependencies that matter for AMD are loading dependencies.
You can certainly have a module named dispatcher that goes:
define(function () {
function Dispatcher(views) {
this.views = views;
for (var ix = 0, view; (view = views[ix]); ++ix)
view.init(this);
}
return Dispatcher;
});
And viewA, viewB, that are structured like this:
define(function () {
function View() {
// ...
}
View.prototype.init = function (dispatcher) {
this.dispatcher = dispatcher;
};
// Etc...
return View;
});
Your main module could do:
define(['dispatcher', 'viewA', 'viewB'], function (Dispatcher, ViewA, ViewB) {
var viewA = new ViewA();
var viewB = new ViewB();
var dispatcher = new Dispatcher([viewA, viewB]);
});
The above is meant to be a schematic example of what is possible, not a prescription for a good design. At any rate, the point is that is is perfectly feasible as far as AMD is concerned to have circular references between objects.
There's nothing about AMD that is limiting here; it's entirely about the design of your modules themselves.
A common way to handle this is with an event-emitter.
The dispatcher can call methods directly on a view, but the view emits events which the dispatcher can listen and respond to, removing the need for a circular reference (as the view doesn't care where the events go, so it doesn't require a reference to the dispatcher.)
Fitted to your example workflow, it might look like this:
overworld tracks keypress
overworld animates in response to keypress
overworld emits 'move' event for dispatcher
// overworld.view
this.emit('move', {data});
// dispatcher
overworld.on('move', getMoveResult) // getMoveResult fires AJAX request
response tells dispatcher it's time to battle
dispatcher updates views
overworld.hide()
battle.show()
Just wondering how people deal stopping multiple external server calls? I'm doing everything in the .complete of the fetch because otherwise when I try to call anything the fetch hasn't completed and nothing is populated in the collection.
I'm new to backbone so I'm probably missing a trick.. but is there a way to do a fetch and store that information somewhere so that you never have to fetch again, you just work off the collection as a variable? All of my information comes from an external site, so I don't want to be making lots of unnecessary external calls if I can. I'm not updating the server or anything, its all just read-only.
What do other people do for a similar set up? Am I missing something silly? Or am I set up badly for this? Here's what I have so far (work in progress)
Oh also: I'm doing the fetch in the router.. is that a bad idea?
http://jsfiddle.net/leapin_leprechaun/b8y6L0rf/
.complete(
//after the fetch has been completed
function(){
//create the initial buttons
//pull the unique leagues out
var uniqueLeagues = _.uniq(matches.pluck("league"));
//pull the unique leagues out
var uniqueDates = _.uniq(matches.pluck("matchDate"));
//pass to info to the relative functions to create buttons
getLeagues(uniqueLeagues);
getMatchDates(uniqueDates);
homeBtn();
fetched = true;
}
); //end complete
Thanks for your time!
This is an often recurring question but the answer is rather simple.
Perhaps I'll make some drawings today, if it helps.
I never took the time to learn UML properly, so forgive me for that.
1. The problem
What you currently have is this:
The problem however is that this isn't very dynamic.
If these 3 functions at the right would require to be executed from different ajax callback functions, they need to be added to any of these callbacks.
Imagine that you want to change the name of any of these 3 functions, it means that your code would break instantly, and you would need to update each of these callbacks.
Your question indicates that you feel that you want to avoid every function to perform the async call separately, which is indeed the case because this creates unnecessary overhead.
2. Event aggregation
The solutions is to implement an event driven approach, which works like this:
This pattern is also called pub/sub (or observer pattern) because there are objects that publish events (in this case on the left) and objects that subscribe (on the right).
With this pattern, you don't need to call every function explicitly after the ajax callback is finished; rather, the objects subscribe to certain events, and execute methods when the event gets triggered. This way you are always certain that the methods will be executed.
Note that when triggering an event, parameters can be passed as well, which allows you to access the collection from the subscribing objects.
3. Backbone implementation
Backbone promotes an event driven approach.
Setting up an event aggregator is simple and can be done as follows:
window.APP = {};
APP.vent = _.extend({}, Backbone.Events);
From the ajax callback, you just trigger an event (you give it any name you want, but by convention, a semi colon is used as a separator):
APP.vent.trigger("some:event", collection);
The three receiving objects subscribe to the event as follows:
APP.vent.on("some:event", function(collection){
console.log(collection.toJSON());
});
And that's basically all.
One thing to take into account is to make sure that when you subscribe to events using "on", you also need to un-subscribe by calling "off", if you no longer need the object.
How to handle that is all up to you in Backbone.js but here is one of options you can take
Creating a View which has body as its el and handle everything.(I usually use Coffee so This might has some syntax errors)
$( document ).ready(function() {
mainView = new MainView({el: "body"});
});
MainView = Backbone.View.extend({
initialize : function(){
this.prepareCollection();
},
prepareCollection : function(collection){
_checker = function(){
if (collection.length === _done) {
this.render();
}
};
_.bind(_checker,this);
collection.each(function(item){
item.fetch(
success : function(){
//you can also initialize router here.
_checker();
}
);
});
},
rener : function(){
//make instance of View whichever you want and you can use colleciton just like variable
}
})
Consider the Backbone app listed below. When its started, it creates a restaurants collection and view of local restaurants in the users area. The user can later change their search area and then update the entire collection by submitting a search form.
The Problem: How can I reference the collection in the search method if it isn't related to the view?
Currently, I'm setting the restaurant collection and view as properties of the App view itself so that it can be easily referenced from within the method, but this seems in appropriate because the view isn't solely related to that collection.
Is it better to reference the collection as I have or is it better to reference it as App.collection, as backbone does when you in create views realted to collections with Backbone.View.extend({collection: mycollection})?
App view:
var App = new (Backbone.View.extend({
...
events: {
submit: "search",
},
start: function(){
App.render();
App.restaurants = new App.Collections.Restaurants();
App.restaurantsView = new App.Views.Restaurants({collection: App.restaurants});
App.restaurants.fetch({});
},
search: function(e){
e.preventDefault();
var payload = $( App.targets.searchForm ).serializeObject();
App.restaurants.fetch({data: payload, processData: true,});
},
...
}):
Those are big questions: How to structure a Backbone application, how to define and maintain module boundaries, and how to decouple modules while allowing notifications where needed, like between search and restaurants?
Big-picture answer: Look at something like Marionette. It's a framework built on top of Backbone, it makes it easy to follow a lot of best practices, and it does some heavy lifting for you. David Sulc has an excellent book about Marionette that covers all of those topics & more. Highly recommended.
Immediate-problem answer: Directly referencing objects outside of a single module (like search) is a bad idea. It couples together concerns that should be kept separate. At a minimum, think of adding an application-level event bus: search can trigger a search event, and the restaurant code can listen for that event and act on its collection in response. You could use something like the backbone.wreqr library's EventAggregator class, which is tailor-made for this kind of thing. In a pinch, you could use an instance of Backbone.Events to roll your own:
App.vent = _.extend( {}, Backbone.Events );
Trigger an event from search code like so:
App.vent.trigger('search', payload);
And in your start function, register to listen for that event:
App.vent.on('search', function(payload) {
App.restaurants.fetch({data: payload, processData: true,});
});
when my page opens, I call the collection and populate the view:
var pagColl = new pgCollection(e.models);
var pagView = new pgView({collection: pagColl});
Separately (via a Datepicker), I wish to want to populate the same collection with different models and instantiate the view again.
The problem I have is how to close the original pagView and empty the pagColl before I open the new one, as this "ghost view" is creating problems for me. The variables referred to above are local variables? is it that I need to create a global pagColl and reset() this?
well there has been many discussion on this topic actually,
backbone does nothing for you, you will have to do it yourself and this is what you have to take care of:
removing the view (this delegates to jQuery, and jquery removes it from the DOM)
// to be called from inside your view... otherwise its `view.remove();`
this.remove();
this removes the view from the DOM and removes all DOM events bound to it.
removing all backbone events
// to be called from inside the view... otherwise it's `view.unbind();`
this.unbind();
this removes all events bound to the view, if you have a certain event in your view (a button) which delegates to a function that calls this.trigger('myCustomEvent', params);
if you want some idea's on how to implement a system I suggest you read up on Derrick Bailey's blogpost on zombie views: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/.
another option
another option would be to reuse your current view, and have it re-render or append certain items in the view, bound to the collection's reset event
I was facing the same issue. I call the view.undelegateEvents() method.
Removes all of the view's delegated events. Useful if you want to disable or remove a view from the DOM temporarily.
I use the stopListening method to solve the problem, usually I don't want to remove the entire view from the DOM.
view.stopListening();
Tell an object to stop listening to events. Either call stopListening
with no arguments to have the object remove all of its registered
callbacks ... or be more precise by telling it to remove just the
events it's listening to on a specific object, or a specific event, or
just a specific callback.
http://backbonejs.org/#Events-stopListening
Here's one alternative I would suggest to use, by using Pub/Sub pattern.
You can set up the events bound to the View, and choose a condition for such events.
For example, PubSub.subscribe("EVENT NAME", EVENT ACTIONS, CONDITION); in the condition function, you can check if the view is still in the DOM.
i.e.
var unsubscribe = function() {
return (this.$el.closest("body").length === 0);
};
PubSub.subscribe("addSomething",_.bind(this.addSomething, this), unsubscribe);
Then, you can invoke pub/sub via PubSub.pub("addSomething"); in other places and not to worry about duplicating actions.
Of course, there are trade-offs, but this way not seems to be that difficult.