-
Notifications
You must be signed in to change notification settings - Fork 64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Paginated virtual scrolling menu #1010
Comments
Adding pagination to the virtual scrolling via middleware and new propertiesI have put together a POC for this using a pagination middleware and some small changes to the underlying menu. The new properties added to the menu are
This approach works with the existing options array but requires the user to pad the options array with export const paginatedMiddleware = middlewareFactory(({ middleware: { icache } }) => {
return ({ fetcher, pageSize }: { fetcher: (page: number, query?: string) => Promise<{ data: MenuOption[], total: number}>, pageSize: number }) => {
async function loadMore(page: number) {
const requestedPages = icache.getOrSet<number[]>('requestedPages', []);
if (requestedPages.indexOf(page) < 0) {
icache.set('requestedPages', [ ...requestedPages, page]);
let currentOptions: MenuOption[] = [...icache.getOrSet('options', [])];
const { data, total } = await fetcher(page);
if (!requestedPages.length) {
currentOptions = Array(total).fill(null);
}
const beforeSize = (page - 1) * pageSize;
let before = currentOptions.splice(0, beforeSize);
let after = currentOptions.splice(pageSize);
icache.set('total', total);
icache.set('options', [ ...before, ...data, ...after]);
}
}
return {
loadMore,
options: icache.getOrSet('options', []),
total: icache.getOrSet('total', 0)
};
};
}); This middleware is instantiated with a const factory = create({ icache, paginatedMiddleware });
export default factory(function LargeOptionSet({ middleware: { icache, paginatedMiddleware } }) {
const pageSize = 20;
async function fetcher(page: number) {
const response = await fetch(`https://some.endpoint?page=${page}`);
const json = await response.json();
return {
data: json.items,
total: json.total
}
}
const { loadMore, options, total } = paginatedMiddleware({fetcher, pageSize});
return (
<Menu
options={options}
onValue={(value) => {
icache.set('value', value);
}}
total={total}
itemsInView={10}
loadMore={loadMore}
pageSize={pageSize}
/>
);
}); This approach appears to work well with paginated endpoints / fetchers and the virtual scrolling menu |
Making the pagination work with a query stringThe above POC works well for pagination where the options object does not change, ie. the result set is static. The difficulty lies when a filter / query is added. This will be problematic when implementing a paginated typeahead widget. I have worked to amend the above POC to work with a query string by adding a This appears to work as expected with filterable, paginated APIs. The adjusted middleware would look as follows: export const paginatedMiddleware = middlewareFactory(({ middleware: { icache } }) => {
return ({ fetcher, pageSize }: { fetcher: (page: number, query?: string) => Promise<{ data: MenuOption[], total: number}>, pageSize: number }) => {
async function loadMore(page: number) {
const requestedPages = icache.getOrSet<number[]>('requestedPages', []);
if (requestedPages.indexOf(page) < 0) {
icache.set('requestedPages', [ ...requestedPages, page]);
const query = icache.get<string>('query');
let currentOptions: MenuOption[] = [...icache.getOrSet('options', [])];
const { data, total } = await fetcher(page, query);
if (!requestedPages.length) {
currentOptions = Array(total).fill(null);
}
const beforeSize = (page - 1) * pageSize;
let before = currentOptions.splice(0, beforeSize);
let after = currentOptions.splice(pageSize);
icache.set('total', total);
icache.set('options', [ ...before, ...data, ...after]);
}
}
return {
loadMore,
setQuery(query?: string) {
const currentQuery = icache.get('query');
if (query !== currentQuery) {
icache.set('query', query);
icache.set('requestedPages', []);
loadMore(1);
}
},
options: icache.getOrSet('options', []),
total: icache.getOrSet('total', 0)
};
};
}); When using this with the menu, the only change is that you need to call const factory = create({ icache, paginatedMiddleware });
export default factory(function LargeOptionSet({ middleware: { icache, paginatedMiddleware } }) {
const pageSize = 20;
async function fetcher(page: number, query: string) {
const response = await fetch(`https://some.endpoint?page=${page}&query=${query}`);
const json = await response.json();
return {
data: json.items,
total: json.total
}
}
const { loadMore, options, total, setQuery } = paginatedMiddleware({fetcher, pageSize});
return (
<virtual>
<Menu
options={options}
onValue={(value) => {
icache.set('value', value);
}}
total={total}
itemsInView={10}
loadMore={loadMore}
pageSize={pageSize}
/>
<TextInput onValue={(value) => {
icache.set('textvalue', value);
setQuery(value);
}} value={icache.get('textvalue')}/>
</virtual>
);
}); |
Problems with this approachWhen using paginated data, we do not have all of the items loaded into the |
Follow up questions for discussion
Update:This should be possible without splitting the widgets into paginated / non-paginated versions. We can use a type union to specify the menu properties. |
Demo of paginated menu here: Note: filtering via the text box only works right now when you're scrolled to the top |
Closing this discussion issue, issue for changes pending implementation here: #1162 |
Enhancement
Paginated virtual scrolling menu
We have recently added the ability to create a virtual scrolling menu which is used in select / menu / typahead (coming soon) etc...
To further enhance this we should make it paginated, for example if there are 100,000 items to display, the server may not be able to return all of these at once so we will need to request them once they are scrolled into view.
The first part of this has been done already by adding virtual scrolling and a
total
property.This addition will make it easier to do server-side typeahead queries and to work with massive data sets.
Example
Challenges
Ease of use
Using the menu in a paginated fashion should be simple and straightforward for our users. Furthermore it should be possible for all menu-using widgets, ie. select / typeahead etc to be be paginated.
Jumping to the end of the menu
Jumping to the end of the menu via the
end
key will skip the middle pages (if they exist) and load the final page. If using a singleoptions
array for this we may need to pad out the interim pages somehow.Keeping the API consistent
In implementing this enhancement, we do not wish to change the existing APIs as much as possible. For example, we would ideally like to keep the
options
as a single array.We will need to add a loadMore callback and pageSize property to the menu, these should both be required when either is used. This should be typed appropriately.
Loading indicators
Whilst data is loading we should render a placeholder to indicate to the user that data is loading.
Starting with no options
The menu should be capable of initialising with no options or total specified. When this occurs, the menu should fetch the first page automatically.
Requesting pages once
We should ensure that the menu only calls out to load missing pages once. This will stop multiple requests from firing and reduce unnecessary callbacks.
Options data changing completely
In the case of a typeahead for example, the options may change completely, ie. the search params could change. We must ensure that when this occurs, subsequent pages will be requested correctly, ie. any flags to indicate they have been fetched should be removed.
The text was updated successfully, but these errors were encountered: