Skip to content

Commit

Permalink
Merge pull request #9 from Mikescops/feature/externalize-storage-items
Browse files Browse the repository at this point in the history
Externalize items storage
  • Loading branch information
Mikescops authored Oct 13, 2020
2 parents 6b337b3 + 8e4cae3 commit 8f7112b
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 85 deletions.
39 changes: 22 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,29 @@ components: {
In your template you can add:

```html
<VirtualGrid :updateFunction="yourGetDataFunction" />
<VirtualGrid :items="yourDataSet" :updateFunction="yourGetDataFunction" />
```

The `VirtualGrid` takes multiple custom function as properties
The `items` property is requeried and should be an array of the following object:

```js
{
id: string, // binding id (must be unique)
injected?: string, // custom param, pass an object with what you want inside (optional)
height: number, // original height of the item
width?: number, // original width of the item (optional: if not set, height will not be adjusted by getItemRatioHeight)
columnSpan: number, // how many columns will use your item (put 0 if you want the full width)
newRow?: boolean, // if the item should appear on the next row (optional)
renderComponent: Component // A VueJS component (custom template of your choice) to render the item (passed as prop `item`)
}
```

You can update the `items` property at any time (and thus decide what can of storage you want to use) and the grid layout will be recomputed.

The `VirtualGrid` also takes multiple custom optional functions/variables as properties

- **updateFunction**:
An async function that will populate the grid, constructor is the following `updateFunction<P>(params: { offset: number }) => Promise<VirtualGrid.Item<P>[]>`. For synchronous function just return immediately your content with `Promise.resolve([you_content])` for instance.
The offset will be incremented (+1) each time the function is called.
An async function that will populate the grid, constructor is the following `updateFunction() => Promise<boolean>`. For synchronous function just return immediately with `Promise.resolve(boolean)` for instance.
- **getGridGap**:
A function that will define the gap between elements of the grid, constructor is the following `getGridGap(elementWidth: number, windowHeight: number) => number`.
- **getColumnCount**:
Expand All @@ -60,19 +75,7 @@ The `VirtualGrid` takes multiple custom function as properties

Properties are provided with default functions that you can use or get inspired from in `src/utils.ts`.

The function `updateFunction` should return a list of items that will be rendered, each item should look like this object:

```js
{
id: string, // binding id (must be unique)
injected?: string, // custom param, pass an object with what you want inside (optional)
height: number, // original height of the item
width?: number, // original width of the item (optional: if not set, height will not be adjusted by getItemRatioHeight)
columnSpan: number, // how many columns will use your item (put 0 if you want the full width)
newRow?: boolean, // if the item should appear on the next row (optional)
renderComponent: Component // A VueJS component (custom template of your choice) to render the item (passed as prop `item`)
}
```
The function `updateFunction` should update the list of items that will be rendered (each item should look like the `Item` object presented before) and return (with a Promise) a boolean that signify that the last batch was loaded (bottom reached) or not.

The property `injected` does not impact the computation, it is here to pass custom data to the final component.

Expand All @@ -97,6 +100,8 @@ interface Image {
const item: Item<Image>;
```

You can also import the typing for utils methods with `VirtualGridInterface`.

### Live example

If you want a live example, you can take a look at the demo (link at the top) and check the corresponding code in `example/`.
Expand Down
59 changes: 46 additions & 13 deletions example/App.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" /> <br />
<a class="button" v-on:click="$refs.virtualgrid.resetGrid()">Reset Component</a>
<VirtualGrid ref="virtualgrid" :updateFunction="pullData" />
<a class="button" v-on:click="resetList()">Reset Component</a>
<VirtualGrid :v-if="loaded" ref="virtualgrid" :items="list" :updateFunction="pullData" />
</div>
</template>

<script lang="ts">
import { Component, Provide, Vue } from 'vue-property-decorator';
import { Component, Provide, ProvideReactive, Vue } from 'vue-property-decorator';
import VirtualGrid from '../src/VirtualGrid.vue';
import { Item } from '../src/types';
import { Item, VirtualGridInterface } from '../src/types';
// Custom components to render
import * as ImageComponent from './components/Image.vue';
Expand All @@ -24,23 +24,43 @@ type CustomDataTypes = ImageComponent.Image | TitleComponent.Title | MapComponen
},
})
export default class App extends Vue {
@Provide() loaded: boolean = false;
@Provide() batchSize: number = 50;
@ProvideReactive() list: Item<CustomDataTypes>[] = [];
@ProvideReactive() offset: number = 0;
mounted() {
this.initializeList();
}
initializeList() {
this.pullData()
.catch((error) => {
if (error) {
console.error('Failed to load initial data', error);
}
})
.then(() => {
this.loaded = true;
});
}
random(low: number, high: number) {
return Math.floor(Math.random() * high) + low;
}
pullData(params: { offset: number }): Promise<Item<CustomDataTypes>[]> {
pullData(): Promise<boolean> {
// This is to try when we reach end of infinite scroll (only 5 loads)
if (params.offset > 5) {
return Promise.resolve([]);
if (this.offset > 5) {
return Promise.resolve(true);
}
// Add a title at each section
const sectionTitle = {
id: `title-${params.offset}`,
id: `title-${this.offset}`,
injected: {
title: `Welcome to section ${params.offset}`,
title: `Welcome to section ${this.offset}`,
},
width: 500,
height: 250,
Expand All @@ -51,7 +71,7 @@ export default class App extends Vue {
// Add a map sometimes (to test iframes)
const map = {
id: `map-${params.offset}`,
id: `map-${this.offset}`,
injected: {
coordinates: '-11.18408203125%2C39.2832938689385%2C17.819824218750004%2C52.77618568896171',
},
Expand All @@ -60,15 +80,15 @@ export default class App extends Vue {
newRow: true,
renderComponent: MapComponent.default,
};
const sectionMap = params.offset === 0 ? [map] : [];
const sectionMap = this.offset === 0 ? [map] : [];
// Populate random images (for the demo)
const randomImages = Array.from({ length: this.batchSize }, (_, index) => {
const randSize = this.random(1, 2); // just to randomized which images can be big or not
const width = 250 * randSize;
const height = 250; // this can work with random height also
const id = index + params.offset * this.batchSize;
const id = index + this.offset * this.batchSize;
return {
id: `img-${id}`,
injected: {
Expand All @@ -82,7 +102,20 @@ export default class App extends Vue {
};
});
return Promise.resolve([sectionTitle, ...randomImages, ...sectionMap]);
this.list = [...this.list, ...[sectionTitle, ...randomImages, ...sectionMap]];
this.offset += 1;
return Promise.resolve(false);
}
resetList() {
this.loaded = false;
this.list = [];
this.offset = 0;
const grid = this.$refs.virtualgrid as VirtualGridInterface;
grid.resetGrid();
this.initializeList();
}
}
</script>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vue-virtual-grid",
"version": "1.2.3",
"version": "2.0.0",
"author": "Corentin Mors <[email protected]>",
"license": "MIT",
"repository": "github:mikescops/vue-virtual-grid",
Expand Down
75 changes: 22 additions & 53 deletions src/VirtualGrid.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ interface RenderData<P> {
@Component
export default class VirtualGrid<P> extends Vue {
@Prop({ default: () => (): Item<unknown>[] => [] }) updateFunction: (params: {
offset: number;
}) => Promise<Item<P>[]>;
@Prop({ required: true }) items: Item<P>[];
@Prop({ default: () => () => true }) updateFunction: () => Promise<boolean>;
@Prop({ default: () => getGridGapDefault }) getGridGap: (elementWidth: number, windowHeight: number) => number;
@Prop({ default: () => getColumnCountDefault }) getColumnCount: (elementWidth: number) => number;
@Prop({ default: () => getWindowMarginDefault }) getWindowMargin: (windowHeight: number) => number;
Expand All @@ -61,9 +60,6 @@ export default class VirtualGrid<P> extends Vue {
@ProvideReactive() updateLock: boolean = false;
@ProvideReactive() items: Item<P>[] = [];
@ProvideReactive() offset: number = 0;
@ProvideReactive() bottomReached: boolean = false;
@ProvideReactive() ref: Element = null;
Expand All @@ -87,7 +83,7 @@ export default class VirtualGrid<P> extends Vue {
return this.computeRenderData(this.configData, this.containerData, this.layoutData);
}
created() {
mounted() {
window.addEventListener('resize', this.resize);
window.addEventListener('scroll', this.scroll);
this.initializeGridData();
Expand All @@ -104,7 +100,7 @@ export default class VirtualGrid<P> extends Vue {
scroll(): void {
this.computeContainerData();
this.computeInfiniteScroll(this.containerData)
this.loadMoreData(this.containerData)
.catch((error) => {
if (error) {
console.error('Fail to load next data batch', error);
Expand All @@ -114,51 +110,30 @@ export default class VirtualGrid<P> extends Vue {
}
initializeGridData(): void {
this.loadMoreData()
.catch((error) => {
if (error) {
console.error('Failed to load initial data', error);
}
})
.then(() => {
this.ref = this.$refs.virtualGrid as Element;
this.computeContainerData();
});
}
async loadMoreData(): Promise<Item<P>[]> {
if (this.updateLock || this.bottomReached) {
return Promise.resolve([]);
}
this.updateLock = true;
const newItems = await this.updateFunction({ offset: this.offset });
if (newItems.length === 0) {
console.debug('Bottom reached');
this.bottomReached = true;
this.updateLock = false;
return Promise.resolve([]);
}
this.items = [...this.items, ...newItems];
this.offset += 1;
this.updateLock = false;
return Promise.resolve(newItems);
this.ref = this.$refs.virtualGrid as Element;
this.computeContainerData();
}
computeInfiniteScroll(containerData: ContainerData): Promise<Item<P>[]> {
async loadMoreData(containerData: ContainerData): Promise<void> {
const windowTop = containerData.windowScroll.y;
const windowBottom = windowTop + containerData.windowSize.height;
const bottomTrigger =
containerData.elementWindowOffset + containerData.elementSize.height - this.updateTriggerMargin;
if (!this.bottomReached && windowBottom > bottomTrigger && !this.updateLock) {
this.updateLock = true;
if (
!this.bottomReached &&
windowBottom >
containerData.elementWindowOffset + containerData.elementSize.height - this.updateTriggerMargin
) {
console.debug('Loading next batch');
return this.loadMoreData();
const isLastBatch = await this.updateFunction();
if (isLastBatch) {
console.debug('Bottom reached');
this.bottomReached = true;
}
this.updateLock = false;
}
return Promise.resolve([]);
return Promise.resolve();
}
computeContainerData(): void {
Expand Down Expand Up @@ -354,19 +329,13 @@ export default class VirtualGrid<P> extends Vue {
return `${gridRowStart}`;
}
/** For Parent methods */
/** For Parent Component */
resetGrid(): void {
this.offset = 0;
this.bottomReached = false;
this.items = [];
this.initializeGridData();
}
getCurrentItems(): Item<P>[] {
return this.items;
}
/** Utils */
isSameElementSize(a: ElementSize, b: ElementSize) {
Expand Down
6 changes: 5 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Component } from 'vue';
import Vue, { Component } from 'vue';

export interface VirtualGridInterface extends Vue {
resetGrid: () => void;
}

export interface Item<P> {
id: string;
Expand Down

0 comments on commit 8f7112b

Please sign in to comment.