Is there a onScroll event for ag-grid - ag-grid

I am looking for a scroll event on ag-grid, I want to know when the scroll reaches the end and load the next set of rows, I know if you set the infinite scroll mode then ag-grid calles the getRows method, but in my application I do not get the next set of rows right away, I make a call to the server and server sends a separate message to the client with the new set of rows

After getting in deep, I found the perfect solution to this problem.
Please note here I am used AngularJS, But very easy to understand.
onBodyScroll:function(params) {
var bottom_px = $scope.gridOptions.api.getVerticalPixelRange().bottom;
var grid_height = $scope.gridOptions.api.getDisplayedRowCount() * $scope.gridOptions.api.getSizesForCurrentTheme().rowHeight;
if(bottom_px == grid_height)
{
alert('Bottom')
}
},

There's a grid event called 'onBodyScroll' which you can attach an event handler to it.
This event is somewhat secret as it was not there on their GridOptions type before version 18, even though it does work.
see this comment: https://github.com/ag-grid/ag-grid-enterprise/issues/89#issuecomment-264477535
They do have this event in document tho: https://www.ag-grid.com/javascript-grid-events/#miscellaneous
BodyScrollEvent
bodyScroll - The body was scrolled horizontally or vertically.
onBodyScroll = (event: BodyScrollEvent) => void;
interface BodyScrollEvent {
// Event identifier
type: string;
api: GridApi;
columnApi: ColumnApi;
direction: ScrollDirection;
left: number;
top: number;
}

You should be able to do that thing (loading the data from the server) as per below example.
First of all, define your dataSource.
const dataSource: IServerSideDatasource = {
getRows: (params: IServerSideGetRowsParams) => this._getRows(params, [])
};
this.gridApi.setServerSideDatasource(dataSource);
Declare _getRows method like this.
private _getRows(params: IServerSideGetRowsParams, data: any[]) {
this.gridApi.showLoadingOverlay();
service.getData(params) // the payload your service understands
.subscribe((result: any[]) => {
params.successCallback(result, -1);
params.failCallback = () => console.log('some error occured while loading new chunk of data');
this.gridApi.hideOverlay();
},
error => this._serverErrorHandler(error)
);
}
This is pretty much self-explanatory. Let's me know if anything is unclear to you.
BTW, I've used typescript for the example, javascript example would be kind of the same for ag-grid-react

Related

How to get data from an API only once (on app creation, outside component or view) in Vue3 SPA, with Pinia store

Is it possible and is it a good practice to avoid fetching data from an API every time the router view is loaded or the component is Mounted?
The thing is that some data rarely changes (like a dropdown options list, imagine allowed animal picks for my app) and it's logical not to send a request every time to a server, instead on app creation would be sufficient.
Tried in App.vue, is that a common thing?
IN APP.vue
import { computed, onMounted, onUpdated, ref } from 'vue';
onMounted(()=>{
axios.get('/data')....
.then((res)=>{
store.property = res.data
...
})
})
I think having it on mount in the App.vue component is acceptable since the App component would not be remounted.
The ideal setup, however, depends on some other parameters like size of application and size of team that's maintaining it. In a large applications you might want to organize things in amore structured and consistent way so you and other folks working on the code know where to find things.
You could consider moving the API call into the pinia action.
store.loadMyData()
// instead of
axios.get('/data')
.then((res)=>{
store.property = res.data;
})
That way you have fewer lines of code in the component. Having "lean" components and moving "business logic" out of components usually makes for better organization which makes it easier to maintain.
Within the action, you can track the state of the API
const STATES = {
INIT: 0,
DONE: 1,
WIP: 2,
ERROR: 3
}
export const useMyApiStore = defineStore('myapi', {
state: () => ({
faves: [],
favesState: STATES.INIT
}),
actions: {
loadMyData() {
this.store.favesState = STATES.WIP;
axios.get('/data')
.then((res) => {
this.store.property = res.data;
this.store.favesState = STATES.DONE;
})
.catch((e) => {
this.store.favesState = STATES.ERROR;
})
},
},
getters: {
isLoaded(){
return this.store.favesState === STATES.DONE;
}
isLoading(){
return this.store.favesState === STATES.WIP;
}
}
})
This is, obviously, more verbose, but allows for the components to be smaller and contain less logic. Then, for example, in your component you can use the getter isLoading to display a loading indicator, or use isLoaded inside another component to determine whether to show it.
Yes, this is a oft used way to load some data into the Vue App.
You could also load data before the Mounting in beforeMount() or created() Lifecycle Hooks (see Vue Lifecycle Diagram) to prevent unnecessary HTML updates.

How to use the nextHandler functionality as shown in the Infinite Ajax Scroll JSON example

I’m hoping to be able to use Infinite Ajax Scroll for a project I’m working on.
I’ve been looking at the Infinite Scroll JSON example here (https://infiniteajaxscroll.com/examples/json/) and I’m finding it difficult to understand how it works. I was wondering if there is any further documentation or code examples on how to use a JS or jQuery handler as shown in the example.
Ultimately what I want to do is load my container "items" using my own ajax function and then have Infinite Ajax Scroll display them. I want to do this because my container "items" are not located at URLs, but are saved as Wordpress transients.
Any help I could get with this would be very much appreciated.
Thanks,
David.
Thank you for your question. The docs on using the nextHandler could indeed use improvement. Regardless, I'll try to explain how it works.
Normally IAS uses a selector to find the url of the next page. Then it loads the page and extracts the elements and appends them to the DOM. If you use the nextHandler, you will completely bypass this behavior. That means you will have to fetch data (in this case JSON) yourself and also insert new elements in the DOM.
Here is an example with comments to explain what it does.
First, let's assume our movie(1..n).json has the following format:
[
{
Title: 'item1',
Plot: 'description1'
}, {
Title: 'item2',
Plot: 'description2'
}
]
Now the implementation of the nextHandler:
import InfiniteAjaxScroll from "#webcreate/infinite-ajax-scroll";
function nextHandler(pageIndex) {
// we use the fetch api to load the next page. Any http client library will do, like axios.
return fetch("./static/movies"+pageIndex+".json")
// use the response as json
.then((response) => response.json())
// process the actual json data
.then((jsonData) => {
// create an array to store our html elements in memory
let elements = [];
// we loop over the items in our json and create an html element for each item
jsonData.forEach(function (item) {
const template = `<div class="item">
<h1>${item.Title}</h1>
<p>${item.Plot}</p>
</div>`;
const element = document.createElement("div");
element.innerHTML = template.trim();
elements.push(element.firstChild);
});
// now use IAS's append method to insert the elements
// it's import that we return the append result, as it's an promise
return this.append(elements);
})
// page 3 returns a 404, returning false here indicates there are no more pages to load
.catch(() => false);
}
window.ias = new InfiniteAjaxScroll(".container", {
item: ".item",
next: nextHandler,
pagination: false
});
I also prepared an interactive demo on Codesandbox:
https://codesandbox.io/s/serene-einstein-f73em

How can I update data within Detail table and don't loose range selection and filters?

I have latest enterprise React agGrid table with Master/Detail grid. My data is fetched on the client every 5 seconds and then put immutably to the redux store. React grid component is using deltaRowDataMode={true} props and deltaRowDataMode: true in Detail options.
My master grid performs normally as expected: if I have range selected, grid would keep selection after the data updates, so would filters and visibility menu would be still opened. But Detail grid behaves differently: on data refresh selections are being removed, visibility menu closes and grid jumps if filters were changed.
I've read in docs that when I open Detail grid it's being created from scratch, but in my case I don't close Detail. Anywhere I've tried keepDetailRows=true flag, which solved problems with jumping on update and selection loss, but Detail grid doesn't update data now.
It seems there are only two possible options according to the docs https://www.ag-grid.com/javascript-grid-master-detail/#changing-data-refresh. The first one is a detail table redraws everytime a data in a master row changes and the second one is a detail row doesn't changes at all if a flag suppressRefresh is enabled. Strange decision, awful beahviour...
Update.
Hello again. I found a coupe of solutions.
The first one is to use a detailCellRendererParams in table's options and set suppressRefresh to true. It gives an opportunity to use getDetailGridInfo to get detail-table's api.
While the detail-table's refreshing is disabled, using detailGridInfo allows to set a new data to a detail-table.
useEffect(() => {
const api = gridApiRef;
api && api.forEachNode(node => {
const { detailNode, expanded } = node;
if (detailNode && expanded) {
const detailGridInfo = api.getDetailGridInfo(detailNode.id);
const rowData = detailNode.data.someData; // your nested data
if (detailGridInfo) {
detailGridInfo.api.setRowData(rowData);
}
}
});
}, [results]);
The second one is to use a custom cellRenderer, wicth is much more flexible and allows to use any content inside a cellRenderer.
In table's options set detailCellRenderer: 'yourCustomCellRendereForDetailTable.
In yourCustomCellRendereForDetailTable you can use
this.state = {
rowData: [],
}
Every cellRenderer has a refresh metod which can be used as follow.
refresh(params) {
const newData = [ ...params.data.yourSomeData];
const oldData = this.state.rowData;
if (newData.length !== oldData.length) {
this.setState({
rowData: newData,
});
}
if (newData.length === oldData.length) {
if (newData.some((elem, index) => {
return !isEqual(elem, oldData[index]);
})) {
this.setState({
rowData: newData,
});
}
}
return true;
}
Using method refresh this way gives a fully customizable approach of using a detailCellRenderer.
Note. To get a better performance with using an immutable data like a redux it needs to set immutableData to true in both main and detail tables.

RxJs Observable with infinite scroll OR how to combine Observables

I have a table which uses infinite scroll to load more results and append them, when the user reaches the bottom of the page.
At the moment I have the following code:
var currentPage = 0;
var tableContent = Rx.Observable.empty();
function getHTTPDataPageObservable(pageNumber) {
return Rx.Observable.fromPromise($http(...));
}
function init() {
reset();
}
function reset() {
currentPage = 0;
tableContent = Rx.Observable.empty();
appendNextPage();
}
function appendNextPage() {
if(currentPage == 0) {
tableContent = getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; });
} else {
tableContent = tableContent.combineLatest(
getHTTPDataPageObservable(++currentPage)
.map(function(page) { return page.content; }),
function(o1, o2) {
return o1.concat(o2);
}
)
}
}
There's one major problem:
Everytime appendNextPage is called, I get a completely new Observable which then triggers all prior HTTP calls again and again.
A minor problem is, that this code is ugly and it looks like it's too much for such a simple use case.
Questions:
How to solve this problem in a nice way?
Is is possible to combine those Observables in a different way, without triggering the whole stack again and again?
You didn't include it but I'll assume that you have some way of detecting when the user reaches the bottom of the page. An event that you can use to trigger new loads. For the sake of this answer I'll say that you have defined it somewhere as:
const nextPage = fromEvent(page, 'nextpage');
What you really want to be doing is trying to map this to a stream of one directional flow rather than sort of using the stream as a mutable object. Thus:
const pageStream = nextPage.pipe(
//Always trigger the first page to load
startWith(0),
//Load these pages asynchronously, but keep them in order
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
//One option of how to join the pages together
scan((pages, p) => ([...pages, p]), [])
)
;
If you need reset functionality I would suggest that you also consider wrapping that whole stream to trigger the reset.
resetPages.pipe(
// Used for the "first" reset when the page first loads
startWith(0),
//Anytime there is a reset, restart the internal stream.
switchMapTo(
nextPage.pipe(
startWith(0),
concatMap(
(_, pageNum) => from($http(...)).pipe(pluck('content'))
),
scan((pages, p) => ([...pages, p]), [])
)
).subscribe(x => /*Render page content*/);
As you can see, by refactoring to nest the logic into streams we can remove the global state that was floating around before
You can use Subject and separate the problem you are solving into 2 observables. One is for scrolling events , and the other is for retrieving data. For example:
let scrollingSubject = new Rx.Subject();
let dataSubject = new Rx.Subject();
//store the data that has been received back from server to check if a page has been
// received previously
let dataList = [];
scrollingSubject.subscribe(function(page) {
dataSubject.onNext({
pageNumber: page,
pageData: [page + 10] // the data from the server
});
});
dataSubject.subscribe(function(data) {
console.log('Received data for page ' + data.pageNumber);
dataList.push(data);
});
//scroll to page 1
scrollingSubject.onNext(1);
//scroll to page 2
scrollingSubject.onNext(2);
//scroll to page 3
scrollingSubject.onNext(3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.js"></script>

Drag an event in fullCalendar component with a specific duration

I've seen the solution to drag and drop external events in fullcalendar. But, in this demo, all the external events have a duration of 2 hours (because defaultEventMinutes parameter is set to 120). I'm trying to change this demo in order to manage events with different durations. Say, "My event 1" is 45min long, "My event 2" is 165min, etc.
At the beginning I though there may be an attribute to store the duration in the eventObject, but according to the documentation, it's not the case.
Then, I thought it would be possible to change the value of 'defaultEventMinutes' when starting dragging the event. But apparently, I can't do it without rebuilding the whole calendar.
According to you, what is the best means to meet this requirement?
Thanks in advance for your advice...
Worked on this as well and have solved the duration shown on fullCalendar this way:
Having a custom "setOptions" function for fullCalendar.
Having a property for fullCalendar called "dragMinutes" that can be set during elements $(this).draggable({start:...}).
Here is the code for the custom setOptions:
...
function Calendar(element, options, eventSources) {
var t = this;
// hack for setting options that updates
function setOptions(new_options, refresh) {
$.extend(options, new_options);
if (refresh) {
var viewName = currentView.name;
changeView(viewName, true);
}
}
// exports ...
t.setOptions = setOptions;
...
Heres the code for handling "dragMinutes" option in fullCalendar:
/* External Dragging
--------------------------------------------------------------------------------*/
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function (cell) {
clearOverlays();
if (cell) {
if (cellIsAllDay(cell)) {
renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
} else {
var d1 = cellDate(cell);
if (opt('dragMinutes'))
var d2 = addMinutes(cloneDate(d1), opt('dragMinutes'));
else
var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
renderSlotOverlay(d1, d2);
}
}
}, ev);
}
And heres how i make event draggable and update the "dragMinutes":
// make the event draggable using jQuery UI
$(this).draggable({
containment: 'document',
// return a custom styled elemnt being dragged
helper: function (event) {
return $('<div class="uv-planning-dragging"></div>').html($(this).html());
},
opacity: 0.70,
zIndex: 10000,
appendTo: 'body',
cursor: 'move',
revertDuration: 0,
revert: true,
start: function (e, ui) {
// set the "dragMinutes" option in fullCalendar so shown interval about to be added is correct.
var data = $(this).data('eventObject');
if (data) {
var min = data.jsonProps.durationMsec / 1000 / 60;
if (macroCalendar.calendar) {
macroCalendar.calendar.fullCalendar('setOptions', { dragMinutes: Math.round(min) }, false);
}
}
},
stop: function (e, ui) {
// further process
}
});
Hope it helps.
If anyone still visits the thread and don't find the solution, the solution would be to set the duration parameter in event div... and then call draggable on that div.
$(this).data('event', {
title: 'new event title', // use the element's text as the event title
id: $(this).attr('id'),
stick: true, // maintain when user navigates (see docs on the renderEvent method)
duration: '03:00:00' // will set the duration during drag of event
});
Currently, the best solution I have found is adding a duration attribute on my event Object, then the code to create my fullCalendar looks like this:
$('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay'
},
editable: true,
droppable: true, // this allows things to be dropped onto the calendar !!!
drop: function(date, allDay) { // this function is called when something is dropped
// retrieve the dropped element's stored Event Object
var originalEventObject = $(this).data('eventObject');
// we need to copy it, so that multiple events don't have a reference to the same object
var copiedEventObject = $.extend({}, originalEventObject);
// assign it the date that was reported
copiedEventObject.start = date;
// HERE I force the end date based on the start date + duration
copiedEventObject.end = new Date(date.getTime() + copiedEventObject.duration * 60 * 1000);
copiedEventObject.allDay = allDay;
// render the event on the calendar
// the last `true` argument determines if the event "sticks" (http://arshaw.com/fullcalendar/docs/event_rendering/renderEvent/)
$('#calendar').fullCalendar('renderEvent', copiedEventObject, true);
// is the "remove after drop" checkbox checked?
if ($('#drop-remove').is(':checked')) {
// if so, remove the element from the "Draggable Events" list
$(this).remove();
}
}
});
The only drawback is when you're dragging the event, the event duration looks like defaultEventMinutes and not the actual duration, but I don't know how to fix it
These special properties can either be specified in the provided event object, or they can be standalone data attributes:
<!-- DURATION OF 3 hours EVENT WILL PROPAGATE TO CALENDAR WHEN DROPPED -->
<div class='draggable' data-event='1' data-duration='03:00' />
https://fullcalendar.io/docs/dropping/eventReceive/
With the latest fullcalendar v2.0.2, if you want the overlay to be of the particular duration, you can update in this function of fullcalendar-arshaw.js
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function(cell) {
clearOverlays();
if (cell) {
var seconds = duration_in_minutes * 1000 * 60 ;
// we need to pass seconds into milli-seconds
if (d1.hasTime()) {
d2.add(seconds);
renderSlotOverlay(d1, d2, cell.col);
}
else {
d2.add(calendar.defaultAllDayEventDuration);
renderDayOverlay(d1, d2, true, cell.col);
}
}
}, ev);
}
Here, pass your duration in the external events object and that object you can fetch in _dragElement and then convert it into milli-seconds and pass it in d2.add(seconds). This will create the shadow of that mili-seconds on that calendar.
For non-external events you can use the fullcalendar settings:
defaultTimedEventDuration: (hours+':00:00'),
forceEventDuration: true,
// defaultEventMinutes: hours*60, // not needed
and in the event data you do not set the end property (or you null it):
eventData = {
title: title,
start: start,
// end: end, // MUST HAVE no end for fixedduration
color: '#00AA00',
editable: true, // for dragging
};
Ref: http://fullcalendar.io/docs/event_data/defaultTimedEventDuration/
Tip: In case you want to prevent the resizing of the events which is possible due to editable: true, you can use CSS to hide the handle: .fc-resizer.fc-end-resizer { display:none; }
Since v4 some of the above options are not working at all. The problem i was facing was as follows:
All day items for me have a duration, but not a start time. When i select a start time by dragging, the start time is set but as soon as i set the end date ( which is done similar as above answers ), the end date is reset again.. there is something buggy going on in the setDate function... the end date is set, this part works, then it does a comparisson on itself to find out the time difference between the dates, but the date is already set by the system itself causing the difference to be 0 which is causing the enddate to be set to null again......
A giant pain in my neck i got to say... it works perfect when staying within the timeline, but that's about it.
I managed to 'fix', more like destroy it by using this line in the eventDrop event, but it will also work in any other events you may use:
update your event with ajax here, since you have the start and end date *
calendar.refetchEvents(); in the success function
This is going to refetch all the events, it sounds pretty killer for performance but it doesn't seem to take up much time, try it for yourself.
This way my titles, times etc are always up to date and the calendar is showing the right end date.