javascript views and subviews design pattern - javascript

I'm wondering how best to handle views and subviews using a Javascript OOP approach. Here's the basic task: I want to create a horizontal, swipe-able list of panes. The swipe element itself will be a view, and within it I will have many subviews -- one for each pane. My current approach looks something like this:
function Swipe(el){
this.el = el;
var els = el.querySelectorAll('li');
this.subviews = els.map(function(li){
return new Pane(li);
}, this);
}
Swipe.prototype.goto = function(num){
// alter this.el to show the current pane, based
// on index.
}
function Pane(el){
this.el = el;
}
Pane.prototype.bind = function(){
// bind click events
}
This design pattern works decently well for me, but what is the best way to access the parent view from the subview? I've gone so far as to pass this when constructing the subview, like this:
this.subviews = els.map(function(li){
return new Pane(li, this);
}, this);
function Pane(el, parent){
this.el = el;
this.parent = parent;
}
And this allows me to call functions from the parent view -- for example, I could use the goto function in response to a subview button being clicked. But I'm guessing that this isn't really optimal.
So my question: what is the best way to handle views and subviews, specifically the ability of subviews to call functions from their parent views?

Related

Update Knockout-based Form from javascript object

Is it possible to use Knockout ONLY for viewing/using other objects of custom class?
I'm trying to find a way to open knockout with different data but always the same structure.
What I did:
// I have an Event class which looks like that:
function cEvent(id){
this.id = id;
}
// I keep an array of instances of that class in something like:
var arr = [new cEvent(1), new cEvent(2)]
On the html page I have:
Event ID: <span data-bind="text: id"></span>
I created an accessor-like class to get data from a specific event with Knockout
cEvent2 = function cEvent2(baseEvent) {
this.id = ko.computed(function(){
return baseEvent.id;
});
}
When I use ko.applyBindings(arr[0]); it works but what if I want to load another "model" without cleaning nodes and reapplying knockout on the page?
What I want:
I'd like to have something like ko.applyBindings(arr[1]); that would update the interface based on the data I want.
Of course in reality the cEvent class is much more complex, but I am trying to see if we're able to get something done without directly extending these instances of cEvent with knockout.
Maybe I'm just trying to do something wrong and it's not the way knockout want to work? I know that in my case I want knockout to serve as a "simple class reader" even if it could do more.
Any tip would be really appreciated.
Here's what I would do:
function cEvent(id){
this.id = id;
}
function myViewModel() {
var arr = [new cEvent(1), new cEvent(2)]
this.selectedEvent = ko.observable(arr[0]);
}
ko.applyBindings(new myViewModel());
That way, if you bind to selectedEvent.id all you need to do when you want to view a different event is update the selectedEvent property and all of your bindings will be automatically updated.

Test a Backbone.js View

I'm currently testing a backbone view with Jasmine and I am having some trouble. I'm trying to isolate the View from all the other elements (the other view that are instantiated, the collection), but it is nearly impossible.
initialize: function(options) {
if(options.return) {return;}
var view = this;
var name = options.name;
var localizedElements = app.helpers.Locale.l().modules.case[name];
var swap, notification;
this.name = name.capitalizeFirstLetter();
this.collection.on('sort', this.refreshGui, this);
return this.render('/case/' + name + '/' + this.name + 'Box.txt', localizedElements, this.$el).done(function() {
new app.views.Buttons({el: view.$el.find('.Buttons')});
_.each(view.collection.models, function(model) {
new app.views['Folded' + this.name]({model: model, el: this.$('table')});
}, view);
if(!view.collection.findWhere({isPreferred: true}) || !view.collection.findWhere({isPrescribedForms: true})) {
if(!view.collection.findWhere({isPreferred: true})) {
swap = {entity: 'address', preferenceType: 'preferred'};
notification = app.helpers.Locale.l().generic.warningMessages.missingPreference.swap(swap);
var preferredNotification = [notification];
app.helpers.Notification.addNotifications('warnings', {missingPreferred: preferredNotification});
}
if(!view.collection.findWhere({isPrescribedForms: true})) {
swap = {entity: 'address', preferenceType: 'prescribed forms'};
notification = app.helpers.Locale.l().generic.warningMessages.missingPreference.swap(swap);
var prescribedFormsNotification = [notification];
app.helpers.Notification.addNotifications('warnings', {missingPrescribedForms: prescribedFormsNotification});
}
}
});
},
For example, where there are the two "ifs", the view is talking to the collection and a helper: "Notification Helper". How am I suppose to test this part of the code if I mocked the collection and the notification helper? I mean I am testing the VIEW, but now it seems like I have to test other elements of my application in my view...
I'm trying to isolate the View from all the other elements
I'm not sure if this will be helpful for you, but I created my own strategy to handle this problem.
The problem with Backbone is often indeed, that the view objects become cluttered with functionality.
Personally, I like my views to handle DOM interactions, listen to DOM events.
But to execute business logic / interactions with the back end, I prefer to delegate this functionality to other 'external' objects.
Models on the other hand 'may' handle basic data validation but do nothing more than making RESTful interactions with the server in my application.
How I solved the problem is by instantiating custom Javascript ('controller') objects that act as intermediaries between the views and the models.
This object is nothing more than an object that looks approximately like this:
var Some_controller = function(options){
this.makeView();
};
Some_controller.prototype.makeView = function(){
var someView = new Some_view({'controller': this});
someView.render(); //Render, but can also take care of proper view cleanup to avoid zombie objects
};
Some_controller.prototype.getModel = function(){
var someModel = new Some_model();
var promise = model.fetch();
return promise; //Promise be called from the view, because the controller is passed via the options, when the view is instantiated.
};
Well, this is how I try to keep my views clean.
In my experience, it is very easy to find everything back immediately; and note that you can also use helper objects to isolate more specific business functionality.
Not sure if anyone else has a better solution for this.

Passing data from one Backbone view to another

Lets say I have the following Backbone view which loads two links, one with the anchor text "test1" and the other with the anchor text "test2".
I bind a click event and I get the HTML of the link that was clicked and store it inside the clickedHtml variable.
Now, this view is loaded by a Backbone router.
When the user clicks either one of the two links (test1 or test2) another view called "main" will be loaded by the router.
Now, how can I pass the "clickedHtml" variable to that view?
Should I use LocalStorage?
Should I declare it globally like window.clickedHtml?
Is there a better way?
Ty!
// file: views/test.js
define([
'jquery',
'underscore',
'backbone'
], function($, _, Backbone) {
var Test = Backbone.View.extend({
el : '.test',
initialize : function () {
var that = this;
that.$el.html('test1<br />test2');
},
events : {
'click .test a' : 'click'
},
click : function (e) {
var clickedHtml = $(e.target).html();
}
return Test;
});
Here is my router:
// file: router.js
define([
'jquery',
'underscore',
'backbone',
'views/test',
'views/main'
], function ($, _, Backbone, Test, Main) {
var Router = Backbone.Router.extend({
routes: {
'' : 'home',
'test' : 'test'
}
});
var initialize = function () {
var router = new Router();
router.on('route:home', function () {
var main = new Main();
});
router.on('route:test', function () {
var test = new Test();
});
Backbone.history.start();
}
return {
initialize : initialize
}
});
Basicly you should use Backbone.Event:(Or it's equivalents in Marionette)
//Declaration
var notificationService = {};
_.extend(notificationService, Backbone.Events);
//Used by listener
notificationService.on("alert", function(o) {
alert(o);
});
//Used by publisher
notificationService.trigger("alert", {foo:"bar"});
The real question is how does it get passed from one view to another?
The way I see it, you have 2 options:
Bubble notificationService from one view to another in initialization
Wrap the notificationService with a requirejs model that returns it (creates a 'almost global' notificationService that can be passed by requirejs).
Although I don't like singletons a bit, this case a of a singleton notificationService object that can easily get injected by requirejs in every model will come in handy.
EDIT:
Another option, the quick and dirty one, just use jquery to trigger event on the DOM (specifically the body element) and listen to body in the other view
//on Listening view, after DOM is ready
$( "body" ).on( "alert", function( event, param1, param2 ) {
alert( param1 + "\n" + param2 );
});
//on Triggering view, after DOM is ready
$( "body").trigger( "alert", [ "Custom", "Event" ] );
NOTE:
notice that once a listening view is closed, it must removes itself from listening to events (unbind/off), so you wont have memory leak
Architecturally speaking, your aim should be to keep your code generic & reusable.
One of the main things you don't want to do in a situation like this is to pass direct references from one object to another - if you end up changing the setup of one of the objects, or you need to pass data from another object as well, this can get messy really fast.
One design pattern that's widely used in situations like this is a mediator. Also known as "pub/sub" you can have a centralized standalone object that mediates information between objects. Certain objects will publish information and other objects can subscribe to them. The mediator acts as an intermediary so that the objects never have to communicate directly with each other. This makes a much more generic, reusable and maintainable solution.
More info here:
http://addyosmani.com/largescalejavascript/#mediatorpattern,
Javascript Patterns
On the Backbone side of things... If you've used Marionette, you may have come across a complimentary mini-library (also implemented by Derick Bailey) called wreqr. You can use this to create a simple mediator with low-overhead in your Backbone applications.
https://github.com/marionettejs/backbone.wreqr
It basically allows you to use backbone style events across objects. Example below:
First, you need to create a globally accessible mediator object, or add it to your app namespace or use require.js:
var mediator = new Wreqr.EventAggregator();
inside View #1
events : {
'click .test a' : 'click'
},
click : function (e) {
var clickedHtml = $(e.target).html();
// trigger an 'element:click' event, which can be listened to from other
// places in your application. pass var clickedHtml with the event
// (passed to the arguments in your eventhandler function).
mediator.trigger('element:click', clickedHtml);
}
Inside View #2
initialize: function(){
//...
this.listenTo(mediator, 'element:click', this.myEventHandler, this);
}
myEventHandler: function(elem){
// elem = clickedHtml, passed through the event aggregator
// do something with elem...
}
Backbone events are the way to go here.
When you capture the event in the view, I would bubble it up using:
click : function (e) {
var clickedHtml = $(e.target).html();
Backbone.Events.trigger("eventname",clickedHtml);
}
Then, you should be able to capture this in your router initialise function, using:
Backbone.Events.on("eventname", responseFunction); // listen out for this event
And then in the router declare a separate function:
responseFunction : function(clickedHtml)
{
//Do whatever you want here
}
I'm writing this from memory, so hopefully it make sense. I've also not tested catching an event like this i the router, but it should work.
HTH.
In the exact case you outline I would create a temp storage object on your global namespace and use that to transfer the data between your views, its a bit "hacky" but its better than using local storage, or the window object directly, at least with a temp object on your own global namespace the intent of the objects usage is known.
I find it better to use the http://backbonejs.org/#Events for a similar purpose of passing data between two views, though it does depend on how you structure your pages, if you have two views on the page representing a "control" or "component" this approach works really well.
If you post a link to your site or something I can have a look and give you some more help.
Russ
You could perhaps store it as a property on the view:
click : function (e) {
this.clickedHtml = $(e.target).html();
}
If your router can access both views, it can then simply pass the firstView.clickedHtml property to a function in the secondView (or to the initializer)

Is there a way to console.log all event occuring inside Backbone Marionette ItemView

I have a backbone marionette view in which i have defined many events.
I want to store a log for any event which is occuring inside my itemview. I dont want to write function calling for my audit data on every data, instead i had like to override backbone marionette events method so that it is applied on all my ItemViews.
I tried using:
var originalTrigger = Backbone.Events.trigger;
Backbone.Events.trigger = function(){
console.log("Event Triggered:");
console.log(arguments.join(", "));
originalTrigger.apply(this, arguments);
}
But it doesnt do anything for me. Please help
Thank you in advance
If you check the Backbone source you'll see things like this:
_.extend(Model.prototype, Events, {
where Model and Events are local aliases for Backbone.Model and Backbone.Events respectively. That means that the methods from Backbone.Events will have been copied to the model, collection, and view prototypes before you have a chance to wrap Backbone.Events.trigger with your auditing.
You'll want to wrap all four triggers after Backbone is loaded but before anything else (including Marionette) is loaded. Something like this:
(function() {
var trigger = Backbone.Events.trigger;
var wrapper = function() {
console.log("Event Triggered:");
console.log(arguments.join(", "));
trigger.apply(this, arguments);
};
Backbone.Model.prototype.trigger = wrapper;
Backbone.Collection.prototype.trigger = wrapper;
Backbone.View.prototype.trigger = wrapper;
})();
after <script src="backbone.js"> but before <script src="backbone.marionette.js"> should do the trick.

How to render and append sub-views in Backbone.js

I have a nested-View setup which can get somewhat deep in my application. There are a bunch of ways I could think of initializing, rendering and appending the sub-views, but I'm wondering what common practice is.
Here are a couple I've thought of:
initialize : function () {
this.subView1 = new Subview({options});
this.subView2 = new Subview({options});
},
render : function () {
this.$el.html(this.template());
this.subView1.setElement('.some-el').render();
this.subView2.setElement('.some-el').render();
}
Pros: You don't have to worry about maintaining the right DOM order with appending. The views are initialized early on, so there isn't as much to do all at once in the render function.
Cons: You are forced to re-delegateEvents(), which might be costly? The parent view's render function is cluttered with all of the subview rendering that needs to happen? You don't have the ability to set the tagName of the elements, so the template needs to maintain the correct tagNames.
Another way:
initialize : function () {
},
render : function () {
this.$el.empty();
this.subView1 = new Subview({options});
this.subView2 = new Subview({options});
this.$el.append(this.subView1.render().el, this.subView2.render().el);
}
Pros: You don't have to re-delegate events. You don't need a template that just contains empty placeholders and your tagName's are back to being defined by the view.
Cons: You now have to make sure to append things in the right order. The parent view's render is still cluttered by the subview rendering.
With an onRender event:
initialize : function () {
this.on('render', this.onRender);
this.subView1 = new Subview({options});
this.subView2 = new Subview({options});
},
render : function () {
this.$el.html(this.template);
//other stuff
return this.trigger('render');
},
onRender : function () {
this.subView1.setElement('.some-el').render();
this.subView2.setElement('.some-el').render();
}
Pros: The subview logic is now separated from the view's render() method.
With an onRender event:
initialize : function () {
this.on('render', this.onRender);
},
render : function () {
this.$el.html(this.template);
//other stuff
return this.trigger('render');
},
onRender : function () {
this.subView1 = new Subview();
this.subView2 = new Subview();
this.subView1.setElement('.some-el').render();
this.subView2.setElement('.some-el').render();
}
I've kind of mix and matched a bunch of different practices across all of these examples (so sorry about that) but what are the ones that you would keep or add? and what would you not do?
Summary of practices:
Instantiate subviews in initialize or in render?
Perform all sub-view rendering logic in render or in onRender?
Use setElement or append/appendTo?
I have generally seen/used a couple of different solutions:
Solution 1
var OuterView = Backbone.View.extend({
initialize: function() {
this.inner = new InnerView();
},
render: function() {
this.$el.html(template); // or this.$el.empty() if you have no template
this.$el.append(this.inner.$el);
this.inner.render();
}
});
var InnerView = Backbone.View.extend({
render: function() {
this.$el.html(template);
this.delegateEvents();
}
});
This is similar to your first example, with a few changes:
The order in which you append the sub elements matters
The outer view does not contain the html elements to be set on the inner view(s) (meaning you can still specify tagName in the inner view)
render() is called AFTER the inner view's element has been placed into the DOM, which is helpful if your inner view's render() method is placing/sizing itself on the page based on other elements' position/size (which is a common use case, in my experience)
Solution 2
var OuterView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
this.$el.html(template); // or this.$el.empty() if you have no template
this.inner = new InnerView();
this.$el.append(this.inner.$el);
}
});
var InnerView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
this.$el.html(template);
}
});
Solution 2 may look cleaner, but it has caused some strange things in my experience and has affected performance negatively.
I generally use Solution 1, for a couple of reasons:
A lot of my views rely on already being in the DOM in their render() method
When the outer view is re-rendered, views don't have to be re-initialized, which re-initialization can cause memory leaks and also cause freaky issues with existing bindings
Keep in mind that if you are initializing a new View() every time render() is called, that initialization is going to call delegateEvents() anyway. So that shouldn't necessarily be a "con", as you've expressed.
This is a perennial problem with Backbone and, in my experience, there's not really a satisfying answer to this question. I share your frustration, especially since there is so little guidance despite how common this use case is. That said, I usually go with something akin to your second example.
First of all, I would dismiss out of hand anything that requires you to re-delegate events. Backbone's event-driven view model is one of its most crucial components, and to lose that functionality simply because your application is non-trivial would leave a bad taste in any programmer's mouth. So scratch number one.
Regarding your third example, I think it's just an end-run around the conventional rendering practice and doesn't add much meaning. Perhaps if you're doing actual event triggering (i.e., not a contrived "onRender" event), it would be worth just binding those events to render itself. If you find render becoming unwieldy and complex, you have too few subviews.
Back to your second example, which is probably the lesser of the three evils. Here is example code lifted from Recipes With Backbone, found on page 42 of my PDF edition:
...
render: function() {
$(this.el).html(this.template());
this.addAll();
return this;
},
addAll: function() {
this.collection.each(this.addOne);
},
addOne: function(model) {
view = new Views.Appointment({model: model});
view.render();
$(this.el).append(view.el);
model.bind('remove', view.remove);
}
This is only a slightly more sophisticated setup than your second example: they specifiy a set of functions, addAll and addOne, that do the dirty work. I think this approach is workable (and I certainly use it); but it still leaves a bizarre aftertaste. (Pardon all these tongue metaphors.)
To your point on appending in the right order: if you're strictly appending, sure, that's a limitation. But make sure you consider all possible templating schemes. Perhaps you'd actually like a placeholder element (e.g., an empty div or ul) that you can then replaceWith a new (DOM) element that holds the appropriate subviews. Appending isn't the only solution, and you can certainly get around the ordering problem if you care about it that much, but I would imagine you have a design issue if it is tripping you up. Remember, subviews can have subviews, and they should if it's appropriate. That way, you have a rather tree-like structure, which is quite nice: each subview adds all its subviews, in order, before the parent view adds another, and so on.
Unfortunately, solution #2 is probably the best you can hope for using out-of-the-box Backbone. If you're interested in checking out third-party libraries, one that I have looked into (but haven't actually had any time to play with yet) is Backbone.LayoutManager, which seems to have a healthier method of adding subviews. However, even they have had recent debates on similar issues to these.
Surprised this hasn't been mentioned yet, but I'd seriously consider using Marionette.
It enforces a bit more structure to Backbone apps, including specific view types (ListView, ItemView, Region and Layout), adding proper Controllers and a lot more.
Here is the project on Github and a great guide by Addy Osmani in the book Backbone Fundamentals to get you started.
I have, what I believe to be, a quite comprehensive solution to this problem. It allows a model within a collection to change, and have only its view re-rendered (rather than the entire collection). It also handles removal of zombie views through the close() methods.
var SubView = Backbone.View.extend({
// tagName: must be implemented
// className: must be implemented
// template: must be implemented
initialize: function() {
this.model.on("change", this.render, this);
this.model.on("close", this.close, this);
},
render: function(options) {
console.log("rendering subview for",this.model.get("name"));
var defaultOptions = {};
options = typeof options === "object" ? $.extend(true, defaultOptions, options) : defaultOptions;
this.$el.html(this.template({model: this.model.toJSON(), options: options})).fadeIn("fast");
return this;
},
close: function() {
console.log("closing subview for",this.model.get("name"));
this.model.off("change", this.render, this);
this.model.off("close", this.close, this);
this.remove();
}
});
var ViewCollection = Backbone.View.extend({
// el: must be implemented
// subViewClass: must be implemented
initialize: function() {
var self = this;
self.collection.on("add", self.addSubView, self);
self.collection.on("remove", self.removeSubView, self);
self.collection.on("reset", self.reset, self);
self.collection.on("closeAll", self.closeAll, self);
self.collection.reset = function(models, options) {
self.closeAll();
Backbone.Collection.prototype.reset.call(this, models, options);
};
self.reset();
},
reset: function() {
this.$el.empty();
this.render();
},
render: function() {
console.log("rendering viewcollection for",this.collection.models);
var self = this;
self.collection.each(function(model) {
self.addSubView(model);
});
return self;
},
addSubView: function(model) {
var sv = new this.subViewClass({model: model});
this.$el.append(sv.render().el);
},
removeSubView: function(model) {
model.trigger("close");
},
closeAll: function() {
this.collection.each(function(model) {
model.trigger("close");
});
}
});
Usage:
var PartView = SubView.extend({
tagName: "tr",
className: "part",
template: _.template($("#part-row-template").html())
});
var PartListView = ViewCollection.extend({
el: $("table#parts"),
subViewClass: PartView
});
Check out this mixin for creating and rendering subviews:
https://github.com/rotundasoftware/backbone.subviews
It is a minimalist solution that addresses a lot of the issues discussed in this thread, including rendering order, not having to re-delegate events, etc. Note that the case of a collection view (where each model in the collection is represented with one subview) is a different topic. Best general solution I am aware of to that case is the CollectionView in Marionette.
I don't really like any of the above solutions. I prefer for this configuration over each view having to manually do work in the render method.
views can be a function or object returning an object of view definitions
When a parent's .remove is called, the .remove of nested children from the lowest order up should be called (all the way from sub-sub-sub views)
By default the parent view passes it's own model and collection, but options can be added and overridden.
Here's an example:
views: {
'.js-toolbar-left': CancelBtnView, // shorthand
'.js-toolbar-right': {
view: DoneBtnView,
append: true
},
'.js-notification': {
view: Notification.View,
options: function() { // Options passed when instantiating
return {
message: this.state.get('notificationMessage'),
state: 'information'
};
}
}
}
Backbone was intentionally built so that there was no "common" practice in regards to this and many other issues. It is meant to be as unopinionated as possible. Theoretically, you don't even have to use templates with Backbone. You could use javascript/jquery in the render function of a view to manually change all of the data in the view. To make it more extreme, you don't even need one specific render function. You could have a function called renderFirstName which updates the first name in the dom and renderLastName which updates the last name in the dom. If you took this approach, it would be way better in terms of performance and you'd never have to manually delegate events again. The code would also make total sense to someone reading it (although it would be longer/messier code).
However, usually there is no downside to using templates and simply destroying and rebuilding the entire view and it's subviews on each and every render call, as it didn't even occur to the questioner to do anything otherwise. So that's what most people do for pretty much every situation they come across. And that's why opinionated frameworks just make this the default behavior.
You could also inject the rendered subviews as variables into the main template as variables.
first render the subviews and convert them to html like this:
var subview1 = $(subview1.render.el).html();
var subview2 = $(subview2.render.el).html();
(that way you could also dynamically string concatenate the views like subview1 + subview2 when used in loops) and then pass it to the master template which looks like this:
... some header stuff ...
<%= sub1 %>
<%= sub2 %>
... some footer stuff ...
and inject it finally like this:
this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));
Regarding the Events within the subviews: They will be most likely have to be connected in the parent (masterView) with this approach not within the subviews.
I like to use the following approach which also make sure to remove the child views properly. Here is an example from the book by Addy Osmani.
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
this.remove(); };
NewView = Backbone.View.extend({
initialize: function() {
this.childViews = [];
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
this.childViews.push(itemView);
},
onClose: function() {
_(this.childViews).each(function(view) {
view.close();
});
} });
NewChildView = Backbone.View.extend({
tagName: 'li',
render: function() {
} });
There is no need to re-delegate events as it is costly. See below:
var OuterView = Backbone.View.extend({
initialize: function() {
this.inner = new InnerView();
},
render: function() {
// first detach subviews
this.inner.$el.detach();
// now can set html without affecting subview element's events
this.$el.html(template);
// now render and attach subview OR can even replace placeholder
// elements in template with the rendered subview element
this.$el.append(this.inner.render().el);
}
});
var InnerView = Backbone.View.extend({
render: function() {
this.$el.html(template);
}
});

Categories

Resources