ExtJs: determine grid that fires the update event on a store - javascript

i use a livegrid in ExtJs 3.3.1 but believe this question is global to ExtJs.
How does a listener on a store know which grid the event comes from ?
Here why and some code.
I have a listener on a store and on update i would like to know which rows were selected in the grid and also suspend the events. This all so that i can make a selection in the grid, update a field in that range and update that field in the whole selection. Selection is done without a checkbox, just by highlighting the rows. Since this listener is used by many grids i need a way to get it froml what the gridlistener gets as parameters but that is only store, record and action
Ext.override(Ext.ux.grid.livegrid.Store, {
listeners: {
'update': function(store, record, action) {
if (action=='commit'){ //each update has 2 actions, an edit and a commit
var selected = grid.getSelectionModel().getSelections(); //need to know which grid
if (selected.length>1){ //if more than one row selected
grid.suspendEvents();
store.writer.autoSave = false;
for(var i=0; i < selected.length; i++){
if (this.fieldChanged) {
for (var name in this.fieldChanged) {
//get the field changed and update the selection with the value
if (selected[i].get(name)!=this.fieldChanged[name]){
selected[i].set(name, this.fieldChanged[name]);
}
}
}
}
grid.resumeEvents();
store.fireEvent("datachanged", store);
store.writer.autoSave = true;
}
}
if (action=='edit'){
this.fieldChanged = record.getChanges()
}
}
}
});

It would be easier in an extension but it can be done in an override as well.
MyGridPanel = Ext.extend(Ext.ux.grid.livegrid.EditorGridPanel, {
initComponent: function(){
MyGridPanel.superclass.initComponent.call(this);
this.store.grid = this;
}
});
edit --- Showing how to do it in an override, it isn't pretty but it is useful.
var oldInit = Ext.ux.grid.livegrid.EditorGridPanel.prototype.initComponent;
Ext.override(Ext.ux.grid.livegrid.EditorGridPanel, {
initComponent: function(){
oldInit.call(this);
this.store.grid = this;
}
});

There may be more grids using the store. Preferably in Ext Js 4 you observe the Gridpanel class like so:
//Associate all rendered grids to the store, so that we know which grids use a store.
Ext.util.Observable.observe(Ext.grid.Panel);
Ext.grid.Panel.on('render', function(grid){
if (!grid.store.associatedGrids){
grid.store.associatedGrids=[];
}
grid.store.associatedGrids.push(grid);
});

Found a solution myself, i override the livegrid to include a reference to itself in its store like so
Ext.override(Ext.ux.grid.livegrid.EditorGridPanel, {
listeners: {
'afterrender': function(self) {
this.store.grid = this.id;
}
}
});
Then in my store listener i can refer to store.grid

Related

ExtJS : Re-selecting the same value does not fire the select event

Normally, when you select an item in a combobox, you would expect it to fire the select event. However, if you try to select an item that was already selected, the select event is not fired. That is the "normal" behavior of an ExtJs combobox.
I have a specific need for an ExtJS combobox: I need it to fire the select event even if I re-select the same value. But I cannot get it to work. Any help would be much appreciated!
Example here: https://fiddle.sencha.com/#view/editor&fiddle/2n11
Open the dev tools to see when the select event is fired.
I'm using ExtJS Classic 6.6.0.
Edit: I answered my own question and updated the Fiddle with working solution.
try to look at this:
ExtJS 4 Combobox event for selecting selected value
Its for earlier ExtJS version, but catching click event for itemlist may help you out too..
I found the culprit: it all happens in the SelectionModel of the combobox BoundList, in the method doSingleSelect.
So if we extend Ext.Selection.DataViewModel and Ext.form.field.ComboBox, we can force the select event to be fired every time.
Ext.define( "MyApp.selection.DataViewModelExt", {
"extend": "Ext.selection.DataViewModel",
"alias": "selection.dataviewmodelext",
"doSingleSelect": function(record, suppressEvent) {
var me = this,
changed = false,
selected = me.selected,
commit;
if (me.locked) {
return;
}
// already selected.
// should we also check beforeselect?
/*
if (me.isSelected(record)) {
return;
}
*/
commit = function() {
// Deselect previous selection.
if (selected.getCount()) {
me.suspendChanges();
var result = me.deselectDuringSelect([record], suppressEvent);
if (me.destroyed) {
return;
}
me.resumeChanges();
if (result[0]) {
// Means deselection failed, so abort
return false;
}
}
me.lastSelected = record;
if (!selected.getCount()) {
me.selectionStart = record;
}
selected.add(record);
changed = true;
};
me.onSelectChange(record, true, suppressEvent, commit);
if (changed && !me.destroyed) {
me.maybeFireSelectionChange(!suppressEvent);
}
}
});
We also must extend the combobox to force using our extended DataViewModel. The only thing to change is the onBindStore method where it instancies the DataViewModel:
Ext.define( "MyApp.form.field.ComboBoxEx", {
"extend": "Ext.form.field.ComboBox",
"alias": "widget.comboboxex",
"onBindStore": function(store, initial) {
var me = this,
picker = me.picker,
extraKeySpec,
valueCollectionConfig;
// We're being bound, not unbound...
if (store) {
// If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
if (store.autoCreated) {
me.queryMode = 'local';
me.valueField = me.displayField = 'field1';
if (!store.expanded) {
me.displayField = 'field2';
}
// displayTpl config will need regenerating with the autogenerated displayField name 'field1'
if (me.getDisplayTpl().auto) {
me.setDisplayTpl(null);
}
}
if (!Ext.isDefined(me.valueField)) {
me.valueField = me.displayField;
}
// Add a byValue index to the store so that we can efficiently look up records by the value field
// when setValue passes string value(s).
// The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
// are found, they are all returned by the get call.
// This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
// if unique is true, CollectionKey keeps the *last* matching value.
extraKeySpec = {
byValue: {
rootProperty: 'data',
unique: false
}
};
extraKeySpec.byValue.property = me.valueField;
store.setExtraKeys(extraKeySpec);
if (me.displayField === me.valueField) {
store.byText = store.byValue;
} else {
extraKeySpec.byText = {
rootProperty: 'data',
unique: false
};
extraKeySpec.byText.property = me.displayField;
store.setExtraKeys(extraKeySpec);
}
// We hold a collection of the values which have been selected, keyed by this field's valueField.
// This collection also functions as the selected items collection for the BoundList's selection model
valueCollectionConfig = {
rootProperty: 'data',
extraKeys: {
byInternalId: {
property: 'internalId'
},
byValue: {
property: me.valueField,
rootProperty: 'data'
}
},
// Whenever this collection is changed by anyone, whether by this field adding to it,
// or the BoundList operating, we must refresh our value.
listeners: {
beginupdate: me.onValueCollectionBeginUpdate,
endupdate: me.onValueCollectionEndUpdate,
scope: me
}
};
// This becomes our collection of selected records for the Field.
me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
// This is the selection model we configure into the dropdown BoundList.
// We use the selected Collection as our value collection and the basis
// for rendering the tag list.
//me.pickerSelectionModel = new Ext.selection.DataViewModel({
me.pickerSelectionModel = new MyApp.selection.DataViewModelExt({
mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
// There are situations when a row is selected on mousedown but then the mouse is dragged to another row
// and released. In these situations, the event target for the click event won't be the row where the mouse
// was released but the boundview. The view will then determine that it should fire a container click, and
// the DataViewModel will then deselect all prior selections. Setting `deselectOnContainerClick` here will
// prevent the model from deselecting.
ordered: true,
deselectOnContainerClick: false,
enableInitialSelection: false,
pruneRemoved: false,
selected: me.valueCollection,
store: store,
listeners: {
scope: me,
lastselectedchanged: me.updateBindSelection
}
});
if (!initial) {
me.resetToDefault();
}
if (picker) {
me.pickerSelectionModel.on({
scope: me,
beforeselect: me.onBeforeSelect,
beforedeselect: me.onBeforeDeselect
});
picker.setSelectionModel(me.pickerSelectionModel);
if (picker.getStore() !== store) {
picker.bindStore(store);
}
}
}
}
});
Then just use the extended combobox in your app. By doing that, the select event will be fired every time.

How to stop Kendo KO Grid from auto-scrolling

I have a kendo knockout grid inside a kendo window, the grid is pretty basic, has a checkbox column, and 3 other text columns. The check box column is binded with an observable property in the records Model of the grid, like
$model.isChecked = ko.observable(false);
The datasource of the grid is an observable array of a given javascript model.The grid has pagination with a page size of 10 records, and is scrollable.
The problem I'm having is that for some weird reason, when I click on a checkbox that is at the bottom of the grid, the grid scrolls up to the top, hiding the record I just checked.
I have other grids with the same logic behind and this behavior doesn't happen, I've tried different things and it seems every time I change an observable property of record model, the grid does the same. I also tried subscribing to the scroll event of the grid but I wasn't able to find a difference from me triggering the scroll or the grid doing it by itself.
I also tried what is suggested in this: other question but the behavior I got is not good because you see like a flicker, the grid scrolls to the top and then scrolls to the selected row.
So, have any of you faced a similar problem?
Thanks,
Try this it worked for me
In dataBound and dataBinding events of grid
dataBound = function (e) {
var sender = e.sender;
sender.content.scrollTop(sender.options.gridTop);
}
dataBinding = function (e) {
var sender = e.sender;
sender.options.gridTop = sender.content.scrollTop();
};
Well actually, after some more debugging I was able to fix it, it was a combination of 2 things, first I had to remove the type declaration from the datasource:
dataSource: {
type: 'knockout',
pageSize: 10,
page: 1,
watchable: {
filter: dataSourceWithFilters
},
schema: {
model: {
fields: {
'effectiveFrom()': { type: 'date' },
'effectiveTo()': { type: 'date' },
'isChecked()': { type: 'boolean' } // <- this line was removed
}
}
}
}
And then, I had some dates in the model, but I had them as computed "listening" to an observable variable in the same model, and every time that observable variable had a value, I returned the dates
$model.link = ko.observable();
$model.effectiveFrom = ko.computed(function () {
if ($model.link()) {
return $model.link().effectiveFrom();
}
return null;
});
$model.effectiveTo = ko.computed(function () {
if ($model.link()) {
return $model.link().effectiveTo();
}
return null;
});
It seems this was making the grid to rebind itself every time when any of the date values changed, so I changed that code for this:
$model.link = ko.observable();
$model.link.subscribe(function (value) {
if (value) {
$model.effectiveFrom = ko.observable(value.effectiveFrom()).withDateFormat('MMM-DD-YYYY');
$model.effectiveTo = ko.observable(value.effectiveTo()).withDateFormat('MMM-DD-YYYY');
}
});
$model.effectiveFrom = ko.observable().withDateFormat('MMM-DD-YYYY');
$model.effectiveTo = ko.observable().withDateFormat('MMM-DD-YYYY');
And with those changes the grid stopped scrolling to the top.
Thanks for the help.

How to re-run JavaScript when DOM mutates?

I'm using Template.rendered to setup a dropdown replacement like so:
Template.productEdit.rendered = function() {
if( ! this.rendered) {
$('.ui.dropdown').dropdown();
this.rendered = true;
}
};
But how do I re-run this when the DOM mutates? Helpers return new values for the select options, but I don't know where to re-execute my .dropdown()
I think you don't want this to run before the whole DOM has rendered, or else the event handler will run on EVERY element being inserted:
var rendered = false;
Template.productEdit.rendered = function() {rendered: true};
To avoid rerunning this on elements which are already dropdowns, you could give new ones a class which you remove when you make them into dropdowns
<div class="ui dropdown not-dropdownified"></div>
You could add an event listener for DOMSubtreeModified, which will do something only after the page has rendered:
Template.productEdit.events({
"DOMSubtreeModified": function() {
if (rendered) {
var newDropdowns = $('.ui.dropdown.not-dropdownified');
newDropdowns.removeClass("not-dropdownified");
newDropdowns.dropdown();
}
}
});
This should reduce the number of operations done when the event is triggered, and could stop the callstack from being exhausted
Here's my tentative answer, it works but I'm still hoping Meteor has some sort of template mutation callback instead of this more cumbersome approach:
Template.productEdit.rendered = function() {
if( ! this.rendered) {
$('.ui.dropdown').dropdown();
var mutationOptions = {
childList: true,
subtree: true
}
var mutationObserver = new MutationObserver(function(mutations, observer){
observer.disconnect(); // otherwise subsequent DOM changes will recursively trigger this callback
var selectChanged = false;
mutations.map(function(mu) {
var mutationTargetName = Object.prototype.toString.call(mu.target).match(/^\[object\s(.*)\]$/)[1];
if(mutationTargetName === 'HTMLSelectElement') {
console.log('Select Changed');
selectChanged = true;
}
});
if(selectChanged) {
console.log('Re-init Select');
$('.ui.dropdown').dropdown('restore defaults');
$('.ui.dropdown').dropdown('refresh');
$('.ui.dropdown').dropdown('setup select');
}
mutationObserver.observe(document, mutationOptions); // Start observing again
});
mutationObserver.observe(document, mutationOptions);
this.rendered = true;
}
};
This approach uses MutationObserver with some syntax help I found here
Taking ad educated guess, and assuming you are using the Semantic UI Dropdown plugin, there are four callbacks you can define:
onChange(value, text, $choice): Is called after a dropdown item is selected. receives the name and value of selection and the active menu element
onNoResults(searchValue): Is called after a dropdown is searched with no matching values
onShow: Is called after a dropdown is shown.
onHide: Is called after a dropdown is hidden.
To use them, give the dropdown() function a parameter:
$(".ui.dropdown").dropdown({
onChange: function(value, text, $choice) {alert("You chose " + text + " with the value " + value);},
onNoResults: function(searchValue) {alert("Your search for " + searchValue + " returned no results");}
onShow: function() {alert("Dropdown shown");},
onHide: function() {alert("Dropdown hidden");}
});
I suggest you read the documentation of all plugins you use.

Ember.js checkbox for array controller computed property

I have a checkbox that I would like to trigger a simple 'select all' functionality. The problem is that I can't figure out how to connect the checkbox's action to an action in my controller so that I can actually update the records.
App.LanguagesController = Ember.ArrayController.extend({
actions: {
toggleAllVisibility: function() {
var newVisibility = !this.get('allAreVisible');
var needingVisibilityChange = this.filterBy('visible', !newVisibility);
needingVisibilityChange.setEach('visible', newVisibility);
}
},
allAreVisible: function(param) {
return this.filterBy('visible', false).get('length') === 0;
}.property('#each.visible'),
});
In my template, I have the following input helper
{{input type='checkbox' checked=allAreVisible}}
This properly updates the checkbox when I change the elements manually (i.e. if all of them are selected, then checkbox updates), but no actions fire when I toggle the checkbox.
It looks like in older versions of Ember.js I could simply add an action parameter to the input helper but that doesn't work anymore. I'm assuming I need to setup something that observes when the computed property attempts to change, but I couldn't find anything in the docs or other help.
I also tried extending checkbox to send the click event:
App.AllLanguagesCheckbox = Ember.Checkbox.extend(Ember.ViewTargetActionSupport, {
click: function() {
this.triggerAction({
action: 'toggleAllVisibility'
});
}
});
And then loaded that in my template with
{{view App.AllLanguagesCheckbox checkedBinding=allAreVisible}}
That allows the checkbox to trigger the action, but does not update based on the computed property in the controller.
I feel like I'm missing something obvious here.
EDIT
Based on kingpin2k's answer below, here's the working controller code:
App.LanguagesController = Ember.ArrayController.extend({
toggleAllVisibility: function() {
var newVisibility = !this.get('controller').get('allAreVisible');
var needingVisibilityChange = this.get('controller').filterBy('visible', !newVisibility);
needingVisibilityChange.setEach('visible', newVisibility);
},
allAreVisible: function(param) {
return this.filterBy('visible', false).get('length') === 0;
}.property('#each.visible'),
});
It's not called with the normal scope so you have to explicitly go through the controller to get the model array, but it works as expected now.
Here's the accompanying input helper:
{{input type='checkbox' checked=allAreVisible change=toggleAllVisibility}}
The problem is your checkbox is connected to a computed property, the computation should derive the value (aka you shouldn't be setting it), which is what would be happening when someone tries to check.
_allAreVisible:false,
allAreVisible: function(param) {
if(this.filterBy('visible', false).get('length') === 0){
// set to true;
// else set to false
}.observes('#each.visible'),
http://emberjs.jsbin.com/abODIKoj/1/edit

ExtJS 4.2.1 resetting Stores on ViewChange

in my ExtJS application I want to reset the stores when I change the page.
Which means I don't want any filters, groupings or listener from any old view/page.
Currentyl I am setting the store of my view like this:
{
xtype: 'admingrid',
...
columns: [
...
],
store: 'appname.store.administration.User'
}
I am loading the store like that:
onAfterRender: function() {
//load all users
this.setUserStore(this.getUserGrid().getStore());
this.getUserStore().load();
},
and in some cases like this:
onAfterRender: function() {
//load all users
this.setUserStore(Ext.StoreManager.lookup('appname.store.administration.User'));
this.getUserStore().load();
},
all my my pages extends my Base view and so I thought I just do something like this:
Ext.define("appname.view.Base", {
extend: 'Ext.panel.Panel',
ui: 'basepanel',
padding: 15,
contentPaddingProperty: 'padding',
listeners: {
beforedestroy: function() {
Ext.StoreManager.each(function (item, index, len) {
item.clearFilter(true); // param: suppressEvent
item.clearGrouping();
item.clearListeners(); // this will also remove managed listeners
});
}
}
});
this will cause that the grid is sometimes empty when I am entering the view the first time... I don't understand why.. when I am entering the view the second or sometimes the third time it does show the grid with the entries..
Is there a common way to accomplish such a thing? What I am doing wrong? I don't understand why this is happening.
Just clear the store if it's present in your afterrender event. That way, you'll re-use the store and just clear it.
So in your afterrender, use StoreManager to lookup the store. If its present, it's been created before and you can clear it.
var store = Ext.StoreManager.lookup('storeid');
if(store) {
store.clearFilter(true);
store.clearGrouping();
}
This will clear the store whenever it's present.

Categories