Backbone - Access other views of collection - backbone.js

I have a typical structure of a collection holding models.
In the view, each object has an 'edit' button, that should desactivate all 'edit' buttons of other objects.
I wonder what is the best practice of doing that. Thanks!!

You could add a property editable on your models that is default set to true. Then when you click the 'edit' button on one of the views, you could loop through all the models of the other views and set editable to false. On the view you would listen to model changes, and re-render the view. If editable is false you would disable the edit button.

Ok, so I came up with the following approach:
Assume that model has a property status, and when it is modified to active I want to hide the edit button in other entries (or simply disable it).
My collection view listens to a change in a model:
initialize: function(){
this.listenTo(this.collection, "change:status", this.triggerEditable);
},
The listener callback looks like that:
triggerEditable: function(obj){
var triggerValue = null;
// I am interested in a status which became 'active' or stopped being 'active'
if (obj.get("status") == 'active' && obj.previous("status") != 'active') {
triggerValue = "editable:false";
} else if (obj.get("status") != 'active' && obj.previous("status") == 'active') {
triggerValue = "editable:true";
}
// for any other status change - return
if (!triggerValue) return;
// trigger is fired for all other objects in the collection
_.each(obj.collection.without(obj),function(otherObj) {
otherObj.trigger(triggerValue);
});
}
So, when one object becomes active or stop being active, edidable:false or edidable:true are triggered for all other entries. All I need to do is to add to the model view initializer a listener:
this.listenTo(this.model, "editable:false", this.disableEdit);
this.listenTo(this.model, "editable:true", this.enableEdit);
Here I guess I could combine these two lines into one, first by listening to the editable namespace (how??) and then by passing an argument to the function (again, how exactly?).
From here it is straight forward - implement the listener callback:
disableEdit: function() {
var e = this.$el.find('button.edit')
e.attr('disabled','disabled');
}
If somebody has something to add or to make this solution nicer, I will be glad to hear.
Anyway, hope it will be helpful to others!!

Related

Angular - kendo data binding

I'm using a kendo grid and have a checkbox column with the following template:
"<input class='gridCheckbox' id='gridCheckbox_#=name#' name='Selected' type='checkbox' ng-model='dataItem.checked'/>"
In addition I'm also using an observableArray as the grid's dataSource.
When clicking the chekcbox the data in the observableArray is changed as expected but no "change" event is triggered.
Here is how I define the observableArray:
var obsArray = new kendo.data.ObservableArray(scope.gridData);
this.gridDataSource = new kendo.data.DataSource({
data: obsArray
});
obsArray.bind("change", function (e) {
console.log(e.action, e.field);
});
"scope.gridData" is the original dataModel. When I click the checkbox the observableArray is changed but not the "scope.gridData". In order to change the "scope.gridData" I want to listen to the "change" event and change the "scope.gridData" manually but as I said the "change" event is not triggered.
Any suggestions to what am I doing wrong and maybe there is a better solution.
Read This
your issue is that kendo uses a copy of your scope object
I manually added an event to my input checkbox (in our class we're using Angular so it was on ng-click="doSomething()" but maybe yours is just click="doSomething" and recorded handling the boolean change manually.
We have the Kendo Observables, too - but I got **lucky because we're also using the Breeze JS stuff where we can do data detection and refresh the grid once the data propagates backwards to the right place to be set to dirty. ( grid.dataSource.read(); )
If you want the full row value, make the click="doSomething(this)" and then capture it as the Sender. Just debug in and you should the dataItem attached to the Sender.
This might help you & this is not the correct figure but i did one example like this similar to your problem
var contentData = [
{ organization: 'Nihilent', os: 'Window' }
];
var nihl = contentData[0];
var viewModel = kendo.observable({
gridSource: new kendo.contentData.DataSource({
contentData: contentData
})
});
kendo.bind($(document.body), viewModel);
contentData.push({ organization: 'Dhuaan', os: 'Android' });
nihl.set('os', 'iOS');

In Ext.js4.1.3, how to find a combobox related to its internal item?

I have a ComboBox subclass which has a customized template for rendering its data. Once one internal list item is clicked, there is a listener to the browser "click" event, which needs to make some changes on the original combobox.
How can I find a reference to its related combobox from this listener?
This is what I found, which answers my question.
From the "click" listener 2nd argument, we get the click event target element. We can then navigate to its parent boundlist element, using the .x-boundlist class, get its component and finally reach the combobox through its not-documented pickerField property or its getBubbleTarget() method.
click:{
element:'el',
fn: function(ev, target) { //listener
var el= Ext.get(target),
boundList_el, boundList, combobox;
boundList_el= el && el.findParentNode(".x-boundlist");
boundList= boundList_el && Ext.getCmp(boundList_el.id);
combobox= boundList && boundList.getBubbleParent();
//Do something with the combobox, like changing its value
}
Since the customized combo contents was defined using listConfig and its click listener was defined through the listeners config, this refers to the boundList element in the scope of the listener. A more simple way to reach the same result:
click:{
element:'el',
fn: function(ev, target) { //listener
var boundList= Ext.getCmp(this.id),
combobox= boundList && boundList.getBubbleParent();
//Do something with the combobox, like changing its value
}

Backbone: getting view object from the element

Let's say I have some items to show in a list. The list has a view that aggregates all the items as item views. Now I want to handle the click events on the item views and I delegate the handling to the list view.
Let's see some example code:
ItemView = Backbone.View.extend({
className: 'item',
initialize: function() {
this.$el.data('backbone-view', this);
}
});
Note that I am attaching the view object itself as a property of the root element, which essentially creates a circular reference situation for the view and the element.
ListView = Backbone.View.extend({
initialize: function() {
// contains the item views
this.items = [];
// click event delegation
this.$el.click(_.bind(this._onClick, this));
},
addItem: function(v) {
if ( !(v instanceof ItemView) ) return;
this.items.push(v);
this.$el.append(v.el);
},
_onClick: function(e) {
var el = $(e.target).closest('.item'),
view = el.data('backbone-view');
// do something with the view
}
});
This is a very general pattern whenever one has to deal with any kind of list views.
I am getting the item view back in the handler via the data property that I set on the item on the initialization time. I need to get item view because anything that I want to do on the item as part of handling the click event is based on the view.
Also note that I am using closest because item view may be complex and the actual target of the click event may be a descendant of the root element.
So the question: is this way to binding the view to it's root element via data properties the right approach -- in particular when considering garbage collection and memory leaks? Can there be something better than this?
You should catch the events in the child view. In my opinion, any Backbone view should only handle the DOM events of its element and its children. If views are nested, as yours are, the most specific view should handle the events.
If you want to delegate handling to the parent view, you can trigger a backbone event in the ItemView, and listen to those in the ListView.
ItemView = Backbone.View.extend({
events: {
"click":"onClick"
},
onClick: function() {
//trigger a custom event, passing the view as first argument
this.trigger('click', this);
}
});
ListView = Backbone.View.extend({
addItem: function(v) {
if ( !(v instanceof ItemView) ) return;
//listen to custom event
this.listenTo(v, 'click', this._onClick);
this.items.push(v);
this.$el.append(v.el);
},
_onClick:function(itemView) {
//...
}
});
If the click event represents some "higher level" action, such as select or activate, you should name your custom events as such. This way you can create a logical, robust interface between your views without concerning the parent ListView with the implementation details of its child. Only ItemView should know that whether it's been clicked, hovered, double clicked etc.

In Backbone.js how do you handle Ordinal Numbering and changing numbering based on jQuery Sortable in the view?

Here is my situation. I have a bunch of "Question" model inside a "Questions" collection.
The Question Collection is represented by a SurveyBuilder view.
The Question Model is represented by a QuestionBuilder view.
So basically you have an UL of QuestionBuilder views. The UL has a jQuery sortable attached (so you can reorder the questions). The question is once I'm done reordering I want to update the changed "question_number"s in the models to reflect their position.
The Questions collection has a comparator of 'question_number' so collection should be sorted. Now I just need a way to make their .index() in the UL reflect their question_number. Any ideas?
Another problem is DELETEing a question, I need to update all the question numbers. Right now I handle it using:
var deleted_number = question.get('question_number');
var counter = deleted_number;
var questions = this.each(function(question) {
if (question.get('question_number') > deleted_number) {
question.set('question_number', question.get('question_number') - 1);
}
});
if (this.last()) {
this.questionCounter = this.last().get('question_number') + 1;
} else {
this.questionCounter = 1;
}
But it seems there's got to be a much more straighforward way to do it.
Ideally whenever a remove is called on the collection or the sortstop is called on the UL in the view, it would get the .index() of each QuestionuBuilder view, update it's models's question_number to the .index() + 1, and save().
My Models,Views, and Collections: https://github.com/nycitt/node-survey-builder/tree/master/app/js/survey-builder
Screenshot: https://docs.google.com/file/d/0B5xZcIdpJm0NczNRclhGeHJZQkE/edit
More than one way to do this but I would use Backbone Events. Emit an event either when the user clicks something like done sorting, hasn't sorted in N seconds, or as each sort occurs using a jQuery sortable event such as sort. Listen for the event inside v.SurveyBuilder.
Then do something like this. Not tested obviously but should get you there relatively easily. Update, this should handle your deletions as well becuase it doesn't care what things used to be, only what they are now. Handle the delete then trigger this event. Update 2, first examples weren't good; so much for coding in my head. You'll have to modify your views to insert the model's cid in a data-cid attribute on the li. Then you can update the correct model using your collection's .get method. I see you've found an answer of your own, as I said there are multiple approaches.
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.eventHub = EventHub;
this.questions = new c.Questions();
this.questions.on('add', this.addQuestion, this);
this.eventHub.on('questions:doneSorting', this.updateIndexes)
},
updateIndexes: function(e) {
var that = this;
this.$('li').each(function(index) {
var cid = $(this).attr('data-cid');
that.questions.get(cid).set('question_number', index);
});
}
I figured out a way to do it!!!
Make an array of child views under the parent view (in my example this.qbViews maintains an array of QuestionBuilder views) for the SurveyBuilder view
For your collection (in my case this.questions), set the remove event using on to updateIndexes. That means it will run updateIndexes every time something is removed from this.questions
In your events object in the parent view, add a sortstop event for your sortable object (in my case startstop .question-builders, which is the UL holding the questionBuilder views) to also point to updateIndexes
In updateIndexes do the following:
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
And there is my full code for SurveyBuilder view:
v.SurveyBuilder = v.Page.extend({
template: JST["app/templates/pages/survey-builder.hb"],
initialize: function() {
this.qbViews = []; //will hold all of our QuestionBuilder views
this.questions = new c.Questions(); //will hold the Questions collection
this.questions.on('add', this.addQuestion, this);
this.questions.on('remove', this.updateIndexes, this); //We need to update Question Numbers
},
bindSortable: function() {
$('.question-builders').sortable({
items: '>li',
handle: '.move-question',
placeholder: 'placeholder span11'
});
},
addQuestion: function(question) {
var view = new v.QuestionBuilder({
model: question
});
//Push it onto the Views array
this.qbViews.push(view);
$('.question-builders').append(view.render().el);
this.bindSortable();
},
updateIndexes: function(){
//Go through each of our Views and set the underlying model's question_number to
//whatever the index is in the list + 1
_.each(this.qbViews, function(qbView){
var index = qbView.$el.index();
//Only actually `set`s if it changed
qbView.model.set('question_number', index + 1);
});
},
events: {
'click .add-question': function() {
this.questions.add({});
},
//need to update question numbers when we resort
'sortstop .question-builders': 'updateIndexes'
}
});
And here is the permalink to my Views file for the full code:
https://github.com/nycitt/node-survey-builder/blob/1bee2f0b8a04006aac10d7ecdf6cb19b29de8c12/app/js/survey-builder/views.js

How to notify view when I change value of some property or delete from list?

I need to create model which holds css properties for one element. My model looks like this:
StyleModel = Backbone.Model.extend( {
defaults : {
productName : '',
styles:{
'font-weight':'normal',
'font-style':'normal',
'text-decoration':'none',
'visibility':'visible'
'color':'blue',
'border-width':'1px',
'border-color':'white',
'font-color':'white'
}
},
initialize : function(property, values) {}
...}
How to notify view when I change value of some property or delete from list ?
(For example when user set border-width to 3px or when delete font-weight. Or is it better solution not to hold properties in hash and to set that every property be element in model ?)
Backbone won't recognize settings in your hash, on it's own. But you can create methods that handle this for you:
Backbone.Model.extend({
setCss: function(key, value){
var css = this.get("styles");
css[key] = value;
this.trigger("change", this, key, value);
this.trigger("change:css", key, value);
this.trigger("change:css:" + key, value);
}
});
Then you would call model.setCss("background-color", "#ff0faf") and it would trigger the three "change" events for you to bind to in your views.
In the view, you can bind the change events in the initializer, and have jQuery apply all of the styles to the DOM element that the view controls:
Backbone.View.extend({
initialize: function(){
this.model.on("change:css", this.setCss, this);
},
setCss: function(){
var css = this.model.get("styles");
this.$el.setCss(css);
}
});
You might need to clear existing css before applying the new set, to make sure you get rid of anything that was removed. More likely, though, you'll want to have a deleteCss method on the model, have it raise a css:deleted event from the model, and have the view respond to that event by removing the css attribute in question.
Use the change event that is available on the model http://documentcloud.github.com/backbone/#Model-change
Have your view bind to the event. http://documentcloud.github.com/backbone/#Events-on

Resources