Backbone: How to have a view subscribe to itself - backbone.js

In backbone I want to have a child view that is used, destroyed and cleans up after itself.
The 'cleans up after itself' I'm having a problem with understanding the correct (if there is one) way of doing. So a contrived example is:
var B = Backbone.View.extend({
sayHello: function () {
this.trigger('hello');
}
});
var A = Backbone.View.extend({
initialize: function () {
var b = new B();
b.listenTo(b, 'hello', function () {
console.log('hello');
});
b.sayHello();
b.remove();
},
});
var a = new A();
This part in particular looks weird is:
b.listenTo(b, 'hello', function () {
console.log('hello');
});
Is it normal to have a view listenTo itself? Is there a better way to do this?

Technically your code should work.
Having a view listen to itself has MANY legitimate uses (in fact I wrote a whole framework on it). The primary purpose is when a View is built to be abstracted (sometimes multiple times) and needs a way to communicate with it's child views. This becomes SUPER important when you have multiple programmers who use each other's code (one is writing core components, the other is implementing the core components, yet another is taking that implementation and further extending it for his own purposes) For example:
//The Abstract Model. Programmer shouldn't have to know how animal works. The programmer
//can create models extended off of this and just subscribe it's triggers (and trigger it's own)
var Animal = Backbone.View.extend({
initialize: function() {
//A lot of stuff happens when an animal gets scared. But these are the
//things ALL Animals do
this.listenTo(this, 'scared', function(){
this.run();
this.hide();
});
},
//This is how ALL Animals run
run: function (){
console.log('Running away with tail between legs');
//Just in case an animal does something in ADDITION to what's above they can
//subscribe to running and do their own extra stuff
this.trigger('running');
}
});
//So a programmer decided to create a cat
var Cat = Animal.extend({
initialize: function(){
//A cat listens for barks (from some external source like a mediator).
this.listenForBarks();
//it addition to whatever happens when an Animal is scared (and any other external factors/object that choose to subscribe to it) a cat will hiss
this.listenTo(this, 'scared', this.hiss);
//when a cat runs it does something specific to a cat (like jumping on a garbage cans)
this.listenTo(this, 'running', this.jumpOnGarbageCan);
},
//When it heads barks, it gets scared
listenForBarks: function(){
this.listenTo(Backbone, 'bark', function(){
//As a programmer I don't really know how Animals react when they're scared.
//But just in case anyone (like by Parent Object) knows or somebody else in the outside world (like a Human object) wants to know I'll broadcast it
this.trigger('scared');
});
},
hiss: function(){
this.trigger('hiss');
console.log('hiss');
},
jumpOnGarbageCan: function(){
console.log('jump!');
}
});
var Dog = Animal.extend({
bark: function(){
//a dog barks for some reason. It doesn't know what a cat is (or that the cat even exists). Just decided to bark at some point. It broadcasts it to the world (via the Mediator)
Backbone.trigger('bark');
}
});
The only thing I would change in your code is moving
b.listenTo(b, 'hello', function () {
console.log('hello');
});
part into B's initialize function and replacing b (the first argument) with this (similar to how I do in my examples. If a view is listening to itself it should do it in it's own initialize function.

Related

Is it good practice to combine CREATE and EDIT controllers in AngularJS?

There are many duplicated code among CREATE and EDIT controllers.
These controllers could be combined into one for minimizing repetitive code.
The problem: I need to distinguish which method to use on form submitting - create() or edit() for example.
The solution: I could add $scope.mode for example and set $scope.mode='edit' if user clicked 'EDIT' button or set $scope.mode='add' if user clicked 'ADD' button.
I could use services for minimizing repetitive code, but there still will be duplicated code. For example in both controllers I have cancel() method which clears the form and hide it. I could store clearForm() and hideForm() in the service, but this code will be duplicated in both controllers:
$scope.cancel = function() {
Service.clearForm();
Service.hideForm();
};
Questions:
Is it good practice to combine CREATE and EDIT controllers in AngularJS?
Is there any good practices to minimize repetitive code?
Yes. Use 1 controller.
Here is the reason why use 1 controller
The job of the controller is to support the View. Your create view and the edit view is exactly same - just that one has data pre-populated (edit) and another does not (create).
Moreover the "purpose" of this View is to have the user change or enter new values in the form. Your only difference should be something like reset(). But even there you could start with an empty model object e.g. $scope.entity = {} in case of CREATE and you will start with $scope.entity = $http.get().
Repetition Problem with 2 Controllers
With 2 different controllers and services you are going to incur at least the following duplication:
$scope.cancel = function() {
Service.cancel();
};
$scope.validate = function() {
ValidtionSvc.validate();
}
.
.
.//other stuff similar
but the problem is why even this duplication like you stated.
(UDATED here onwards since above was the answer to the 1st question)
How to use 1 controller with repetition ?
Is there any good practices to minimize repetitive code?
Question redefined: Is there a good practice of eliminating repetitive code in CREATE and EDIT forms ?
No formal 'best practice' exist to my knowledge to avoid repetitive code in this specific situation. However I am advising against mode=edit/create. The reason being for controllers in this situation there should be almost no difference since their job is to purely to fetch/update the model as the user interacts.
Here are the difference you will encounter in this situation and how you can avoid if/then/else with mode=create/edit:
1) Populating the form with existing values vs. empty form for Create.
To fetch a existing entities you need some key/query data. If such key data is present you could do
var masterEntity = {};
if(keyData) {
masterEntity = MyEntityResourceFactory.getEntity(keyData);
}
$scope.entity = masterEntity;//for Create this would be {}
2) reset() form
should be simply
$scope.reset = function() {
$scope.entity = masterEntity;
}
3) Update/Create
$http.post()//should not be different in today's world since we are treating PUT as POST
4) Validation - this is a perfect reuse - there should be no differences.
5) Initial / Default Values
You can use masterEntity = Defaults instead of {}.
Is it good practice to combine CREATE and EDIT controllers in
AngularJS?
In my experience, yes it is a good idea for 99.9% of the time. I typically inject a formType variable into my controller via the $routeProvider resolve feature. So I would have something like the following:
$routeProvider
.when('/item/create', {
templateUrl: '/app/item/itemForm.html',
controller: 'itemFormController',
resolve: {
item: ['$route', 'itemRepository', function ($route, itemRepository) {
return itemRepository.getNew();
}],
formType: function () { return Enums.FormType.CREATE; }
},
})
.when('/item/edit/:itemId', {
templateUrl: '/app/item/itemForm.html',
controller: 'itemFormController',
resolve: {
item: ['$route', 'itemRepository', function ($route, itemRepository) {
return itemRepository.get($route.current.params.itemId);
}],
formType: function () { return Enums.FormType.EDIT; },
},
});
That way you get your entity and type of form action injected into the controller. I also share the same templates, so saving a form I can either rely on my repository/service to determine what REST endpoint to call, or I can do a simple check inside the controller depending on what formType was injected.
Is there any good practices to minimize repetitive code?
Some of the things I'm using to keep things DRY:
If you keep a common convention on your server API you can go a very long way with a base factory/repository/class (whatever you want to call it) for data access. For instance:
GET -> /{resource}?listQueryString // Return resource list
GET -> /{resource}/{id} // Return single resource
GET -> /{resource}/{id}/{resource}view // Return display representation of resource
PUT -> /{resource}/{id} // Update existing resource
POST -> /{resource}/ // Create new resource
etc.
We then use a AngularJs factory that returns a base repository class, lets call it abstractRepository. Then for each resource I create a concrete repository for that specific resource that prototypically inherits from abstractRepository, so I inherit all the shared/base features from abstractRepository and define any resource specific features to the concrete repository. This way the vast majority of data access code can be defined in the abstractRepository. Here's an example using Restangular:
abstractRepository
app.factory('abstractRepository', [function () {
function abstractRepository(restangular, route) {
this.restangular = restangular;
this.route = route;
}
abstractRepository.prototype = {
getList: function (params) {
return this.restangular.all(this.route).getList(params);
},
get: function (id) {
return this.restangular.one(this.route, id).get();
},
getView: function (id) {
return this.restangular.one(this.route, id).one(this.route + 'view').get();
},
update: function (updatedResource) {
return updatedResource.put();
},
create: function (newResource) {
return this.restangular.all(this.route).post(newResource);
}
// etc.
};
abstractRepository.extend = function (repository) {
repository.prototype = Object.create(abstractRepository.prototype);
repository.prototype.constructor = repository;
};
return abstractRepository;
}]);
Concrete repository, let's use customer as an example:
app.factory('customerRepository', ['Restangular', 'abstractRepository', function (restangular, abstractRepository) {
function customerRepository() {
abstractRepository.call(this, restangular, 'customers');
}
abstractRepository.extend(customerRepository);
return new customerRepository();
}]);
What you'll find if you use this base repository pattern is that most of your CRUD controllers will also share a lot of common code, so I typically create a base CRUD controller that my controllers inherit from. Some people dont like the idea of a base controller, but in our case it has served as well.
The answer to your first question probably depends on the specific circumstances.
If the two controllers share a substantial amount of operations, and the behavior of just one or two functions needs to be altered - why not! Maybe not the most elegant solution but hey, whatever works.
If the behavior of many or all controller operations is going to depend on '$scope.mode'...I'd say be careful. That seems like a dangerous path.
Angular services have always served me well when it comes to minimizing code replication between controllers. If there is a "good practice to minimizing repetitive code," I would say it would be services. They are global to your app and can be injected into multiple controllers without issue.
I hope that helps!

Not able to remove the variables declared inside backbone view

I have read many posts about the issue of multiple instances of the same backbone view being instantiated every time and the view hangs around in the DOM even after it's not used any more, and how to fix this by using this.remove() and this.unbind()
But how to remove the variables declared inside the view, like so:
var myview = Backbone.View.extend({
el : '#somediv',
var1 : '',
var2 : '',
array1 : [],
initialize : function() { //init code here
},
render : function() { //rendering code here
}
});
So my question is, how do i remove instances of those variables declared there: var1, var2, array1. I have to call this view every time i click on a button. And every time i see the previous values of these variables still there. this.remove() and this.unbind() might just remove the view from DOM and undelegate its events bindings.
The properties you define inside the Backbone.View.extend call are attached to the prototype and thus are shared by all instances of your view (i.e. they're sort of like class properties rather than instance properties). This should be fine with your var1 and var2 as you'd just be assigning new values per-instance; the array1 array and similar properties can be problematic though; suppose you do this:
var v = new myview;
v.array1.push('pancakes');
Creating a new instance won't deep-copy everything out of the prototype so v.array1 will refer to the array in the prototype. That means that the next new myview will already have 'pancakes'.
The usual solution is to initialize instance properties in the constructor. For Backbone, the constructor is initialize:
var myview = Backbone.View.extend({
el: '#somediv',
initialize: function() {
this.var1 = '';
this.var2 = '';
this.array1 = [ ];
},
//...
});
You can also run into problems with your el: '#somediv' as that uniquely identifies a single DOM element. As long as you're removing and recreating that element then you should be okay; I'd recommend letting the view create and destroy its own el though, you run into fewer zombies and leaks that way.

Passing arguments to a Backbone.View's constructor

I create a Backbone View
App.MyView = Backbone.View.extend({...});
then instance it like
new App.MyView();
but if my object needs some initial values i
new App.MyView({name:"Beautiful"});
I then would access this options from MyView like that
App.MyView= Backbone.View.extend({
initialize: function(){
console.log(this.options.name);
}
});
This works good, however is not a self-explanatory object: i have to know before hand the need of passing in the initial values in a literal way ({this.name:"Beautiful"}) in order to make MyView work..
How can i pass in arguments like initialize(name) or something like that to make it clear that the values are needed in the View?
Javascript functions don't have a defined number of arguments, so basically, you can't.
Writing:
function(name) {...}
is simply a shortcut for:
function() {
var name = arguments[0];
...
}
You could eventually throw an error is your argument is undefined:
function(name) {
if(typeof name === 'undefined') throw 'Error: missing argument';
...
}
But yeah, other than that, you can't force the use of an argument.
Edit:
If you're problem is just clarity, you cannot do that either I fear.
Edit2:
Well, you could if you really want to, but I'd really avoid walking those paths.

advice on how to design an extendable backbone view

I have the following backbone application.
It's a generic crud view, with the following template:
<div id="<%= crudId %>">
<div class="header-view"></div>
<div class="table-view"></div>
<div class="form-view"></div>
</div>
You can see the crud live here: http://bbbootstrap.com.ar/index.html#Wine
The view itself has subviews, to be rendered in the table-view and the form-view.
The thing is I want it to be a base crud view, and to be easily entendable, adding new subviews, for example, adding a new panel to issue some bulk operations.
These are the possible solutions I came out with so far
1- inheritance: create a new CrudBulkView inheriting from CrudView, modify the template to have a bulk-view place holder.
pro: inheritance can provide quite an elegant and simple solution
cons: it's a bit limiting, I'd like to just be able to compose the BulkView and add it to the CrudView.
2- add a method to crudview like addView(view, place) with place being something like 'beforeForm', 'afterForm', 'beforeTable', etc... (it's much too hardcoded...
cons: too hardcoded
3- pass a function with each subview I want to add, that takes care of creating the dom and attaching to it, right after CrudView has rendered the container. the method could be called setEl and return the newly created el.
pro: really flexible
cons: adds some complexity to the process of attaching the subview to the dom
4-modify the crudView template and then attach to it, something like this:
<div id="<%= crudId %>">
<div class="header-view"></div>
<div class="table-view"></div>
<div class="form-view"></div>
<div class="bulk-view"></div
</div>
then bulkView.el would be '.bulk-view'
pro: simple approach
cons: have to mess around with strings, instead of dealing with the dom
I think it's not so strange what I'm trying to achieve. I just want to add a view to a container view, being as much decoupled as possible, and being able to establish where it should be rendered.
After reading your response to my previous answer I went through and modified my example to hopefully give you an idea of how you can implement a system with named views that allows you to control the ordering as you desire. Let me know if this helps or if you have any questions about how it works.
var viewCtor = Backbone.View.prototype.constructor;
// Assuming we have a reference to the subviews already
var BaseCrudView = Backbone.View.extend({
// This is null for an important reason, see comment in constructor
subViews: null,
// Override the constructor instead of initialize since this is meant to be a base object, so things that
// inherit don't have to remember to call the parent inialize every time.
constructor: function() {
viewCtor.apply(this, arguments);
// It is important this is initialized when instantiating the view rather than in the prototype.
// Backbone's extend() will "copy" the prototype properties of the parent when extending, which really
// just performs an assignment. If this were initialized above in the prototype then all children
// that inherit from that prototype would share the exact same instance of the array/object. If a child
// adds something to the array, it would be changed for all instances that inherit from the parent.
this.subViews = {
header: new HeaderView(),
table: new TableView
};
this.subViewOrder = [
'header',
'table'
];
},
addBefore: function(subView, name, beforeView) {
this.subViews[name] = subView;
var viewLoc = this.subViewOrder.indexOf(beforeView);
if(viewLoc == -1) {
viewLoc = 0;
}
this.subViewOrder.splice(viewLoc, 0, name);
},
addAfter: function(subView, name, afterView) {
this.subViews[name] = subView;
var viewLoc = this.subViewOrder.indexOf(afterView);
if(viewLoc == -1) {
viewLoc = this.subViewOrder.length - 1;
}
this.subViewOrder.splice(viewLoc + 1, 0, name);
},
moveBefore: function(name, beforeView) {
this.addBefore(this.subViews[name], name, this.subViewOrder.splice(this.subViewOrder.indexOf(name), 1));
},
moveAfter: function(name, afterView) {
this.addAfter(this.subViews[name], name, this.subViewOrder.splice(this.subViewOrder.indexOf(name), 1));
},
render: function() {
var that = this;
_.each(this.subViewOrder, function(viewName) {
// Assumes the render() call on any given view returns 'this' to get 'el'
that.$el.append(this.subViews[viewName].render().el);
});
return this;
}
});
var BulkCrudView = BaseCrudView.extend({
inialize: function() {
// Skipping the last parameter causes it to insert at the end
this.addAfter(new BulkView(), 'bulkView');
}
});
With this you could easily extend the BulkCrudView and modify its subViews array in initialize to add/insert whatever you want. Though, it'd work just as well to instantiate a BaseCrudView and work with the view methods. Just whatever feels cleaner and/or floats your boat.

How to update attribute of an existing model?

I wanted to update the rank attribute of an existing model which I passed from another view. However, I get the error Uncaught TypeError: Object # has no method 'set'.
In the initialize part of the view, I have :
this.collection = new tgcollection({model : this.options.model });
I define a function updateModel intended to update the attribute value as:
updateModel: function(){
var val= $("#textbox_id").val();
console.log(val);
console.log(JSON.stringify(this.options.model));
JSON.stringify(this.options.model);
this.options.model.set({"rank": val});
this.render();
//
},
Where am I going wrong?
I can see the value and the model with its previous attribute values in the console.
The model:
define(['jquery','underscore', 'backbone', 'deepmodel'],
function($,_, Backbone) {
var model = Backbone.DeepModel.extend({
// Default attributes for the model.
defaults : {
id: null,
rank: null,
},
initialize: function(){
_.bindAll(this,"update");
this.bind('change : cost', this.update);
},
update: function(){
console.log(this.get("cost"));
},
// Remove this model from *localStorage*.
clear : function() {
this.destroy();
},
});
return model;
});
Just do
this.model.set({"rank": val});
instead of
this.options.model.set({"rank": val});
The model within a view is accessed via this.model not this.options.model
I love a good mystery. Here is my best guess based on what I see. The problem is probably even further back. Where you call:
this.collection = new tgcollection({model : this.options.model });
this.options.model is probably not what you think it is. It would be helpful to see the view BEFORE this view that is instantiating and passing in this.options.model. BTW, with models and collections passed into the view, you can always shorten it to this.model Model, Collection and a handful of others are special in that they get attached directly to the View once passed in.
I'm assuming that in your updateModel() the following SEEM to work:
console.log(JSON.stringify(this.options.model));
JSON.stringify(this.options.model);
The error is coming up on the set(), not the lines above. So the assumption is that you passed in a model. Or did you? My wild guess is that what this.options.model actually is, is just a json object of your model. This might explain why you "see" the model in your console when you stringify it, but then Backbone protests when you call set() on it.
Instead of JSON.stringify to test this.options.model try just console.log(this.options.model). Well, you don't have to test really. The fact that Backbone can't find set() on this object is a tell tale sign. If you're not seeing the complexity of a Backbone model in your console - it's not a model.
Also, for testing and debugging particularly models, I tend to use the model.toJSON() function as a quick check that it's a model and I'm seeing attributes I expect.
Let us know if you have more clues.

Resources