I'm using ag-grid (javascript) to display a large amount of rows (about 3,000 or more) and allow the user to enter values and it should auto-save them as the user goes along. My current strategy is after detecting that a user makes a change to save the data for that row.
The problem I'm running into is detecting and getting the correct values after the user enters a value. The onCellKeyPress event doesn't get fired for Backaspace or Paste. However if I attach events directly to DOM fields to catch key presses, I don't know how to know what data the value is associated with. Can I use getDisplayedRowAtIndex or such to be able to reliably do this reliably? What is a good way to implement this?
EDIT: Additional detail
My current approach is to capture onCellEditingStopped and then getting the data from the event using event.data[event.column.colId]. Since I only get this event when the user moves to a different cell and not just if they finish typing I also handle the onCellKeyPress and get the data from event.event.target (since there is no event.data when handling this event). Here is where I run into a hard-to-reproduce problem that event.event.target is sometimes undefined.
I also looked at using forEachLeafNode method but it returns an error saying it isn't supported when using infinite row model. If I don't use infinite mode the load time is slow.
It looks like you can bind to the onCellKeyDown event. This is sometimes undefined because on first keydown the edit of agGrid will switch from the cell content to the cell editor. You can wrap this around to check if there is a cell value or cell textContent.
function onCellKeyDown(e) {
console.log('onCellKeyDown', e);
if(e.event.target.value) console.log(e.event.target.value)
else console.log(e.event.target.textContent)
}
See https://plnkr.co/edit/XhpVlMl7Jrr7QT4ftTAi?p=preview
As been pointed out in comments, onCellValueChanged might work, however
After a cell has been changed with default editing (i.e. not your own custom cell renderer), the cellValueChanged event is fired.
var gridOptions = {
rowData: null,
columnDefs: columnDefs,
defaultColDef: {
editable: true, // using default editor
width: 100
},
onCellEditingStarted: function(event) {
console.log('cellEditingStarted', event);
},
onCellEditingStopped: function(event) {
console.log('cellEditingStopped', event);
},
onCellValueChanged: function(event) {
console.log('cellValueChanged', event);
}
};
another option could be to craft your own editor and inject it into cells:
function MyCellEditor () {}
// gets called once before the renderer is used
MyCellEditor.prototype.init = function(params) {
this.eInput = document.createElement('input');
this.eInput.value = params.value;
console.log(params.charPress); // the string that started the edit, eg 'a' if letter a was pressed, or 'A' if shift + letter a
this.eInput.onkeypress = (e) => {console.log(e);} // check your keypress here
};
// gets called once when grid ready to insert the element
MyCellEditor.prototype.getGui = function() {
return this.eInput;
};
// focus and select can be done after the gui is attached
MyCellEditor.prototype.afterGuiAttached = function() {
this.eInput.focus();
this.eInput.select();
};
MyCellEditor.prototype.onKeyDown = (e) => console.log(e);
// returns the new value after editing
MyCellEditor.prototype.getValue = function() {
return this.eInput.value;
};
//// then, register it with your grid:
var gridOptions = {
rowData: null,
columnDefs: columnDefs,
components: {
myEditor: MyCellEditor,
},
defaultColDef: {
editable: true,
cellEditor: 'myEditor',
width: 100
},
onCellEditingStarted: function(event) {
console.log('cellEditingStarted', event);
},
onCellEditingStopped: function(event) {
console.log('cellEditingStopped', event);
}
};
Related
Is there a way to manually trigger the Grid-Event onRowClicked programmatically? If I set a node to selected via
node.setSelected(true);
, the event isn't being triggered... Only if I really click on it, but I need to trigger it programmatically too, as a reaction to a service-call.
Seems pretty simple to me. Just call the onRowClicked function on your gridOptions. Seeing as you already have your node, you should be able to get the row and pass it to your onRowClicked function.
var gridOptions = {
columnDefs: columnDefs,
rowData: rowData,
onRowClicked: function(params)
{
console.log('Row Make: ' + params.data.make);
}
};
function clickRowOne()
{
const node = gridOptions.api.getRowNode(0);
gridOptions.onRowClicked(node);
}
Demo.
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.
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.
My fullcalendar duplicates events visually when i drag them to another timeslot. I have simplified my code down to the eventDrop to isolate the issue and yet I'm unable to understand the issue.
If I store the events to my localStorage I don't get a duplicate in the storage and the duplicate disappears when I reload the page. This means the problem is only visual and with Full Calendar itself.
However, this is obviously a huge issue as I don't want to reload the page: I want to stay in the current view changing what I need.
Here's my code for the eventDrop:
eventDrop: function(event, delta, revertFunc, jsEvent, ui, view) {
if (!confirm("Are you sure you want to change " + event.title + " ?")) {
/*If the user cancels the change, the event returns to its original position. Otherwise it saves the event.*/
revertFunc(); /*Revert changes using the predefined function revertFunc of fullCalendar.*/
$("#calendar").fullCalendar("refetchEvents");
/*FullCalendar Method to refetch events from all sources and rerender them on the screen.*/
} else {
updateConfirm(event); /*Use the logic to confirm the event update properties*/
Evento.prototype.Update(event); /*Modify the targeted event on the localStorage using the Update method of the Evento Class*/
$("#calendar").fullCalendar("updateEvent", event);
/*FullCalendar Method to report changes to an event and render them on the calendar.*/
$("#calendar").fullCalendar("refetchEvents");
/*FullCalendar Method to refetch events from all sources and rerender them on the screen.*/
}
}
And here's a gif of the issue:
https://i.imgur.com/rFPvvjE.gif
UPDATE: With slicedtoad's help I isolated the issue to my updateConfirm logic:
var updateConfirm = function(event) {
if (confirm("New title?")) { /*Check if the user wants to change the event title*/
event.title = prompt("Enter the new title: "); /*Set new title for the event*/
} else {
event.title = event.title;
}
if (confirm("New description?")) { /*Check if the user wants to change the event description*/
event.description = prompt("Enter the new description: "); /*Set new description for the event*/
} else {
event.description = event.description;
}
if (confirm("Is the event important?")) { /*Check if the user wants to change the event priority*/
event.overlap = false;
event.backgroundColor = "red"; /*Set new priority for the event*/
} else {
event.overlap = true;
event.backgroundColor = "blue"; /*Set new priority for the event*/
}
};
UPDATE 2:
console.log(event) before updateConfirm(event):
Object {id: "2015-01-27T15:29:11+00:00", title: "título", start: m, end: m, allDay: false…}_allDay: false_end: m_id: "2015-01-27T15:29:11+00:00"_start: mallDay: falsebackgroundColor: "blue"className: Array[0]description: "gt4"end: mid: "2015-01-27T15:29:11+00:00"overlap: truesource: Objectstart: mstoringId: "2015-01-27T15:29:11+00:00"title: "título"__proto__: Object
console.log(event) after updateConfirm(event):
Object {id: "2015-01-27T15:29:11+00:00", title: "título", start: m, end: m, allDay: false…}_allDay: false_end: m_id: "2015-01-27T15:29:11+00:00"_start: mallDay: falsebackgroundColor: "blue"className: Array[0]description: "gt4"end: mid: "2015-01-27T15:29:11+00:00"overlap: truesource: Objectstart: mstoringId: "2015-01-27T15:29:11+00:00"title: "título"__proto__: Object
Since the event is not locally sourced, calling updateEvent isn't necessary since the event will be refetched from the database when you call $("#calendar").fullCalendar("refetchEvents");
I'm not entirely sure why it would duplicate but the event modified by updateEvent seems to persist past the refetch. You must be changing it's ID or replacing it with another event object, but I wasn't able to reproduce it.
So try removing the update line
} else {
updateConfirm(event);
Evento.prototype.Update(event);
$("#calendar").fullCalendar("refetchEvents");
}
If that doesn't work, try deleting the event manually:
$("#calendar").fullCalendar( 'removeEvents', event._id ) //replace the updateEvent call with this
//or event.id if your events have an explicit id
Addendum
You likely want to actually figure out the cause of the problem since the above just patches it. Something in Evento.prototype.Update updateConfirm is modifying the event to the point that FC thinks it is a different event. Is it being copied and replacing itself? Are you playing with it's id?
Do singleton style solution:
This worked for me to stop the duplication which was caused by "new Draggable(container)"
every time reloaded the view
$scope.Draggable = null if ($scope.Draggable == null) {
$scope.Draggable = new Draggable(containerEl, {
itemSelector: '.item',
eventData: function (eventEl) {
return {
title: eventEl.innerText
};
}
});
}
A combobox has a picker (a boundlist instance) which itself has a keynav (BoundListKeyNav).
How can I modify / customise this keynav instance?
Basically, by default it contains bindings for home / end. While this would be useful under normal circumstances, it is not when using a customised combobox. I want my home / end keys to function correctly, as they do before ext decides to hijack them (go to start / end of input contents).
Ideally, I want to do this in the configuration object of the combobx, like so:
{
xtype: 'combobox',
itemId: 'search',
emptyText: 'Search',
editable: true,
typeAhead: false,
hideTrigger: true,
queryMode: 'local',
minChars: 3,
displayField: 'name',
valueField: 'search'
}
It is made to behave in such a way that you can type anything in (to search) but can also choose auto completed searches.
The keynav lives at combo.listKeyNav, but the chunk of code which sets this up in ext fires no events to let us jump in and change it. It appears the combo has no configuration for such a thing either (seeing as the function setting listKeyNav doesn't take any config from our combo object).
FYI
It is the BoundListKeyNav which has these bindings hard coded. The combobox's onExpand creates the instance (taking no config anywhere, allowing for no customisation).
The only way is to override onExpand method of combo.
As Saki wrote customizing of the key navigation is only possible with overriding the onExpand method of the combobox - basically duplicating the original implementation (in case of ExtJS4).
For example:
onExpand: function() {
var me = this,
keyNav = me.listKeyNav,
selectOnTab = me.selectOnTab,
picker = me.getPicker();
// Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab.
if (keyNav) {
keyNav.enable();
} else {
keyNav = me.listKeyNav = new Ext.view.BoundListKeyNav(me.inputEl, {
boundList: picker,
forceKeyDown: true,
tab: function(e) {
if (selectOnTab) {
this.selectHighlighted(e);
me.triggerBlur();
}
// Tab key event is allowed to propagate to field
return true;
},
enter: function(e) {
var selModel = picker.getSelectionModel(),
count = selModel.getCount();
this.selectHighlighted(e);
// Handle the case where the highlighted item is already selected
// In this case, the change event won't fire, so just collapse
if (!me.multiSelect && count === selModel.getCount()) {
me.collapse();
}
},
home: {
fn: Ext.emptyFn,
defaultEventAction: false
},
end: {
fn: Ext.emptyFn,
defaultEventAction: false
}
});
}
// While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
if (selectOnTab) {
me.ignoreMonitorTab = true;
}
Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker
me.inputEl.focus();
}