ExtJs. Setting form values on load and change listeners - extjs

I need "good style" advice. I have a form which is populated from json. The code which populates the form is put inside render listener. The problem is, many form elements have change listeners, so when the form is populated these change listeners are triggered. I want to prevent this unwanted behavior.
// many form elements with change listeners come here
listeners:{
render:function(){
var frm=this.getForm();
Ext.Ajax.request({
url:'../handlers/instruct.handler.php?id='+id,
method:'POST',
params:{action:'params'},
success:function(result,request){
json=Ext.decode(result.responseText,1);
frm.setValues(json); // form population
// triggers change listeners
}
});
}
}
PS. I use ExtJs 4.2

You could suspend events on the fields:
var fields = form.getForm().getFields();
fields.each(function(f) {
f.suspendEvents();
});
form.setValues(json);
fields.each(function(f) {
f.resumeEvents();
});

Related

events not firing after re-render in backbone.js

I am facing a problem while trying to click submit after re-render.
This is my view:
ShareHolderInfoView = Backbone.View.extend( {
template : 'shareholderinfo',
initialize: function() {
this.model = new ShareHolderInfoModel();
},
render : function() {
$.get("shareholderinfo.html", function(template) {
var html = $(template);
that.$el.html(html);
});
//context.loadViews.call(this);
return this;
},
events:{
"change input":"inputChanged",
"change select":"selectionChanged",
"click input[type=submit]":"showModel"
},
inputChanged:function(event){
var field = $(event.currentTarget);
var data ={};
data[field.attr('id')] = field.val();
this.model.set(data);
},
showModel:function(){
console.log(this.model.attributes);
alert(JSON.stringify(this.model.toJSON()));
}
});
This is my Router
var shareholderInfo, accountOwnerInfo;
App.Router = Backbone.Router.extend({
routes:{
'share':'share',
'joint':'joint'
},
share:function(){
$("#subSection").empty();
if(!shareholderInfo){
shareholderInfo = new ShareHolderInfoView();
$("#subSection").append(shareholderInfo.render().el);
} else{
$("#subSection").append(shareholderInfo.$el);
}
},
joint:function(random){
$("#subSection").empty();
if(!accountOwnerInfo){
accountOwnerInfo = new AccountOwnerInfoView();
$("#subSection").append(accountOwnerInfo.render().el);
} else{
$("#subSection").append(accountOwnerInfo.$el);
}
}
});
This is my HTML a div with id='subSection'.
if I check in console, I can able to see the events bound to that view.
Object {change input: "inputChanged", change select: "selectionChanged", click input[type=submit]: "showModel"}
But its not calling that showModel function afer i click submit. Please help.
Your fundamental problem is that you're improperly reusing views.
From the fine manual:
.empty()
Description: Remove all child nodes of the set of matched elements from the DOM.
[...]
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
So when you say:
$("#subSection").empty();
you're not just clearing out the contents of #subSection, you're also removing all event handlers attached to anything inside #subSection. In particular, you'll remove any event handlers bound to accountOwnerInfo.el or shareholderInfo.el (depending on which one is already inside #subSection).
Reusing views is usually more trouble than it is worth, your views should be lightweight enough that you can destroy and recreate them as needed. The proper way to destroy a view is to call remove on it. You could rewrite your router to look more like this:
App.Router = Backbone.Router.extend({
routes: {
'share':'share',
'joint':'joint'
},
share: function() {
this._setView(ShareHolderInfoView);
},
joint: function(random){
this._setView(AccountOwnerInfoView);
},
_setView: function(view) {
if(this.currentView)
this.currentView.remove();
this.currentView = new view();
$('#subSection').append(this.currentView.render().el);
}
});
If your views need any extra cleanup then you can override remove on them to clean up the extras and then chain to Backbone.View.prototype.remove.call(this) to call the default remove.
If for some reason you need to keep your views around, you could call delegateEvents on them:
delegateEvents delegateEvents([events])
Uses jQuery's on function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
and you'd say things like:
$("#subSection").append(shareholderInfo.$el);
shareholderInfo.delegateEvents();
instead of just:
$("#subSection").append(shareholderInfo.$el);
I'd strongly recommend that you treat your views and cheap ephemeral objects: destroy them to remove them from the page, create new ones when they need to go on the page.

Two way data binding in backbone.js

I'm developing a jQuery Backbone.js web application.
As it is in Adobe Flex, I have implemented 2 way data binding in my app for
input elements/widgets.
So, every input element/widget knows its corresponding model and model attribute name.
When the user hits tab or enter, the field value is automatically given to the model.
container.model.set(this.attrName, this.value, options); // command 1
In the other direction, when the model gets updated from the backend, the view of the
input element/widget should automatically get
updated:
container.model.bind("change:"+ this.attrName, this.updateView, this); // command 2
The problem is:
When the user hits enter and the model is automatically updated, also the "change:abc" is
triggered and this.updateView is called, not only when a new model comes from the
backend.
My solution until now was to pass an option "source: gui" when setting the model value when the user pressed enter (command 1), and to check for that in my updateView method. But I am not content with this solution anymore.
Does anybody have a better solution?
Thanks alot in advance
Wolfgang
Update:
When the option silent: true is passed, the validate method of the model is not called, so
that does not help. See Backbone.js source 0.9.2:
_validate: function(attrs, options) {
if (options.silent || !this.validate) return true;
From Backbone.js site:
A "change" event will be triggered, unless {silent: true} is passed as an option
options.silent = true;
container.model.set(this.attrName, this.value, options);
Update:
You added a new comment to your question, so I just complemented my answer to fix the new use case(validation flow) that you mentioned:
var ExtendedModel = Backbone.Model.extend({
uiChange : false,
uiSet: function (attributes, options, optional) {
this.uiChange = true;
this.set(attributes, options, optional);
this.uiChange = false;
}
});
var MyModel = ExtendedModel.extend({
});
var model = new MyModel();
model.on('change:name', function(){
console.log('this.uiChange: ', this.uiChange);
});
//simulates the server side set
model.set({name:'hello'});
//simulates the ui side set you must use it to set from UI
model.uiSet({name:'hello2'});
Two-way binding just means that:
When properties in the model get updated, so does the UI.
When UI elements get updated, the changes get propagated back to the
model.
Backbone doesn't have a "baked-in" implementation of 2 option (although you can certainly do it using event listeners)
In Backbone, we can easily achieve option 1 by binding a view's "render" method to its model's "change" event. To achieve option 2, you need to also add a change listener to the input element, and call model.set in the handler.
check (jsfiddle.net/sunnysm/Xm5eH/16)jsfiddle example with two-way binding set up in Backbone.
Backbone.ModelBinderplugin works great for providing Two-way data binding between your Backbone Views and Models. I wrote a blog post covering some essential features of this plugin Here is the direct link: http://niki4810.github.io/blog/2013/03/02/new-post/
I wanted to see what the bare bones code would be to have two-way binding with Backbone.js. This is what I came up with:
var TwoWayBoundView = Backbone.View.extend({
initialize: function(options) {
this.options = _.defaults(options || {}, this.options);
_.bindAll(this, "render");
this.model.on("change", this.render, this);
this.render();
},
events: {
"change input,textarea,select": "update"
},
// input updated
update: function(e) {
this.model.set(e.currentTarget.id, $(e.currentTarget).val());
},
// model updated...re-render
render: function(e) {
if (e){
var id = Object.keys(e.changed)[0];
$('#'+id).val(e.changed[id]);
}
else{
_.each(this.model.attributes, function(value, key){
$('#'+key).val(value);
});
}
}
});
And the usage:
var model = new Backbone.Model({ prop1: "uno 1", prop2: "dos 2", prop3: "3" });
var view = new TwoWayBoundView({
el: "#myContainer",
model: model
});
Here's a jsbin for it: http://jsbin.com/guvusal/edit?html,js,console,output
I've used libraries that do this, such as Epoxy.js (only 11k minified). And there are several others besides, which I would recommend long before using the proof of concept code above.
I would be interested in potential pitfalls and improvements that could be made with the TwoWayBoundView class above (but nothing beyond basic two-way binding please! i.e. I'm not looking for more features to add.)

Backbone reset event in collection

How does Backbone reset event works?
As far as I understand
Remove all models from collection
Add newly "fetched" models to collection
Fires reset event
In my case each model draw something on SVG so I should call remove function before removing model from collection. Which event is triggered when model is removed from collection?
As #Paul noted, there is no predefined event fired before a reset. However, you can provide your own by overriding the reset method on your collection. For example,
var SVGCollection = Backbone.Collection.extend({
reset: function(models, options) {
options = options || {};
if (!options.silent) {
this.trigger('prereset', this, options);
}
Backbone.Collection.prototype.reset.call(this, models, options);
}
});
And a sample usage
var c = new SVGCollection([
{id: 1},
{id: 2}
]);
c.on('prereset', function() {
console.log(c.pluck('id'));
});
c.on('reset', function() {
console.log(c.pluck('id'));
});
c.reset({id: 3});
See http://jsfiddle.net/nikoshr/8vV7Y/ for a demo
You could also trigger events on each model.
You're correct that reset is fired after the old models have been removed and the new models have been added.
There isn't an event fired for when a model is removed from a collection through the reset method.
You might have to keep a reference to the old models outside of the collection, and then when the reset event is fired, you will have reference to those models so you can call the remove function for them on SVG.

Data arrived event

I am using Extjs 4.1. I need to identify event on grid when data renderred and arrived.
I checked ''afterrender' event. But it fires to early
grid.on('afterrender', function () {
alert(333);
});
Please advice
You're correct grid rendered event would fire after grid was rendered not after the data is displayed. What about subscribing to the store load event?
store.on('load', function() {
...
})
If you simply need to know when the grid has first loaded data, use the Ext.grid.Panel viewready event, it fires when the data has been loaded in and the grid rows have been rendered.
Example:
this.control({
'mygridpanel': {
// select the first record
viewready: function(grid) {
var store = grid.getStore(),
view = grid.getView(),
selModel = grid.getSelectionModel();
if (store.getAt(0)) {
view.focus();
selModel.select(0);
}
},
},
});
This example is configured in the init function of a controller using the MVC pattern, I'm not sure if you are using MVC pattern? The snippet in your comment should work fine either way though.
Also this event passes the Ext.grid.Panel object that fired it as the first argument, as you can see from the example above, you can use that to get a reference to your grid and perform whatever logic you need to do on it in your handler.

Re-rendering view causes input to lose focus

I keep running into this problem over and over. I have a view with an input and I want to set and update things on every keyUp event. The problem is when set is called it triggers a change event which re-renders the view which causes the input to lose focus. So after the user types one character the input loses focus and they can't type anymore.
Another case where this happens is when the user clicks on an input I want to add a class to the div around the input so that it changes color. This of course causes the view to re-render and the input loses focus. I can't simply make a separate view for the input because the input is inside the div I want to re-render.
Here's a simple example.
itemView = Backbone.View.extend({
events: {
"keyup .itemInput": "inputKeyUp"
}
initialize: function(){
this.model.view = this;
this.bind('change', this.render());
this.render();
},
render: function(){
$(this.el).html( $(ich.itemView( this.model.toJSON() )) );
return this;
},
inputKeyUp: function(e) {
this.model.set({name: $(this.view.el).find('input[type=text]').first().val()});
},
});
So far I've gotten around it by using {silent:true} and updating things manually but this creates a mess.
You're basically getting yourself into a sort of infinite loop situation where you're binding your view too tightly to your model, and they're feeding back into each other.
When a user types into a browser text intput, they're already "updating the view". The view already represents the extra text.
So, when you update the model with those changes, you don't need the view to update AGAIN, as it already represents the current state.
So, in these cases, you really do want to use "silent", as you're just syncing the model with the current state of the UI, and don't need the model to inform the view to update.
As to how often to do this, I'm suspecting on keyup is probably excessive. You may want to do it on blur or, even, on some sort of "save" action.
As far as the other issue, I'm not sure why adding a class to an element would cause the view to re-render. Are you simply doing something like
this.$('input[type="text"]').addClass('active')
This shouldn't trigger your model's change event and cause render to run again.
Post comment:
You need to get more granular then.
In terms of rendering, break the individual rendering/updating of elements of the view into separate functions.
Bind property-specific change events ("change:name") to those more granular rendering functions so that they update the part of the view that you wish to change, but do not update the text input.
itemView = Backbone.View.extend({
events: {
"keyup .itemInput": "inputKeyUp"
}
initialize: function(){
this.model.view = this;
this.bind('change:name', this.update_other_stuff());
this.bind('change:selected', this.add_class());
this.render();
},
update_other_stuff: function(){
this.$('.some_other_thing').html("some desired change");
return this;
},
add_class: function(){
this.$('input[type=text]').first().addClass('active');
return this;
},
render: function(){
$(this.el).html( $(ich.itemView( this.model.toJSON() )) );
return this;
},
inputKeyUp: function(e) {
this.model.set({name: $(this.view.el).find('input[type=text]').first().val()});
},
});

Resources