Skip to content

Commit

Permalink
feat(template-strategy): diff between tbody and tr
Browse files Browse the repository at this point in the history
  • Loading branch information
bigopon committed Jan 13, 2019
1 parent d48c5a1 commit 8271abe
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 207 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ There are three parameters that are passed to the function (`getMore(topIndex, i
3. `isAtTop` - A boolean value that indicates whether the list has been scrolled to the top of the items list.


## Caveats

- `<template/>` is not supported as root element of a virtual repeat template. This is due to the requirement of aurelia ui virtualization technique: item height needs to be calculatable. With `<tempate/>`, there is no easy and performant way to acquire this value.

## [Demo](http://aurelia.io/ui-virtualization/)

## Platform Support
Expand Down
10 changes: 9 additions & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ module.exports = function(config) {
]
}
},
singleRun: false
singleRun: false,
mochaReporter: {
ignoreSkipped: true
},
webpackMiddleware: {
// webpack-dev-middleware configuration
// i. e.
stats: 'errors-only'
}
});
};

Expand Down
5 changes: 5 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ declare module 'aurelia-binding' {
isAtBottom: boolean;
isAtTop: boolean;
};
$first: boolean;
$last: boolean;
$middle: boolean;
$odd: boolean;
$even: boolean;
}
}

Expand Down
66 changes: 32 additions & 34 deletions src/template-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export interface ITemplateStrategy {
removeBufferElements(element: Element, topBuffer: Element, bottomBuffer: Element): void;
getFirstElement(topBuffer: Element): Element;
getLastElement(bottomBuffer: Element): Element;
getLastView(bottomBuffer: Element): Element;
getTopBufferDistance(topBuffer: Element): number;
}

Expand All @@ -27,6 +26,9 @@ export class TemplateStrategyLocator {
this.container = container;
}

/**
* Selects the template strategy based on element hosting `virtual-repeat` custom attribute
*/
getStrategy(element: Element): ITemplateStrategy {
if (element.parentNode && (element.parentNode as Element).tagName === 'TBODY') {
return this.container.get(TableStrategy);
Expand All @@ -39,18 +41,18 @@ export class TableStrategy implements ITemplateStrategy {

static inject = [DomHelper];

tableCssReset = '\
display: block;\
width: auto;\
height: auto;\
margin: 0;\
padding: 0;\
border: none;\
border-collapse: inherit;\
border-spacing: 0;\
background-color: transparent;\
-webkit-border-horizontal-spacing: 0;\
-webkit-border-vertical-spacing: 0;';
// tableCssReset = '\
// display: block;\
// width: auto;\
// height: auto;\
// margin: 0;\
// padding: 0;\
// border: none;\
// border-collapse: inherit;\
// border-spacing: 0;\
// background-color: transparent;\
// -webkit-border-horizontal-spacing: 0;\
// -webkit-border-vertical-spacing: 0;';

domHelper: DomHelper;

Expand All @@ -63,7 +65,7 @@ export class TableStrategy implements ITemplateStrategy {
}

moveViewFirst(view: View, topBuffer: Element): void {
const tbody = this._getTbodyElement(topBuffer.nextSibling as Element);
const tbody = this._getFirstTbody(topBuffer.nextSibling as HTMLTableElement);
const tr = tbody.firstChild;
const firstElement = DOM.nextElementSibling(tr);
insertBeforeNode(view, firstElement);
Expand Down Expand Up @@ -98,45 +100,41 @@ export class TableStrategy implements ITemplateStrategy {
}

getFirstElement(topBuffer: Element): Element {
const tbody = this._getTbodyElement(DOM.nextElementSibling(topBuffer));
const tr = tbody.firstChild as HTMLTableRowElement;
const tbody = this._getFirstTbody(DOM.nextElementSibling(topBuffer) as HTMLTableElement);
const tr = tbody.firstElementChild as HTMLTableRowElement;
// since the buffer is outside table, first element _is_ first element.
return tr;
}

getLastElement(bottomBuffer: Element): Element {
const tbody = this._getTbodyElement(bottomBuffer.previousSibling as Element);
const trs = tbody.children;
return trs[trs.length - 1];
const tbody = this._getLastTbody(bottomBuffer.previousSibling as HTMLTableElement);
return tbody.lastElementChild as HTMLTableRowElement;
}

getTopBufferDistance(topBuffer: Element): number {
const tbody = this._getTbodyElement(topBuffer.nextSibling as Element);
const tbody = this._getFirstTbody(topBuffer.nextSibling as HTMLTableElement);
return this.domHelper.getElementDistanceToTopOfDocument(tbody) - this.domHelper.getElementDistanceToTopOfDocument(topBuffer);
}

getLastView(bottomBuffer: Element): Element {
throw new Error('Method getLastView() not implemented.');
private _getFirstTbody(tableElement: HTMLTableElement): HTMLTableSectionElement {
let child = tableElement.firstElementChild;
while (child !== null && child.tagName !== 'TBODY') {
child = child.nextElementSibling;
}
return child.tagName === 'TBODY' ? child as HTMLTableSectionElement : null;
}

private _getTbodyElement(tableElement: Element): Element {
let tbodyElement: Element;
const children = tableElement.children;
for (let i = 0, ii = children.length; i < ii; ++i) {
if (children[i].localName === 'tbody') {
tbodyElement = children[i];
break;
}
private _getLastTbody(tableElement: HTMLTableElement): HTMLTableSectionElement {
let child = tableElement.lastElementChild;
while (child !== null && child.tagName !== 'TBODY') {
child = child.previousElementSibling;
}
return tbodyElement;
return child.tagName === 'TBODY' ? child as HTMLTableSectionElement : null;
}
}

export class DefaultTemplateStrategy implements ITemplateStrategy {

getLastView(bottomBuffer: Element): Element {
throw new Error('Method getLastView() not implemented.');
}
getScrollContainer(element: Element): HTMLElement {
return element.parentNode as HTMLElement;
}
Expand Down
21 changes: 11 additions & 10 deletions src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import {View} from 'aurelia-templating';
import { IVirtualRepeat } from './interfaces';

export function calcOuterHeight(element: Element): number {
let height: number;
height = element.getBoundingClientRect().height;
height += getStyleValue(element, 'marginTop');
height += getStyleValue(element, 'marginBottom');
let height = element.getBoundingClientRect().height;
height += getStyleValues(element, 'marginTop', 'marginBottom');
return height;
}

Expand Down Expand Up @@ -49,12 +47,15 @@ export function rebindAndMoveView(repeat: IVirtualRepeat, view: View, index: num
}
}

export function getStyleValue(element: Element, style: string): number {
let currentStyle: CSSStyleDeclaration;
let styleValue: number;
currentStyle = element['currentStyle'] || window.getComputedStyle(element);
styleValue = parseInt(currentStyle[style], 10);
return Number.isNaN(styleValue) ? 0 : styleValue;
export function getStyleValues(element: Element, ...styles: string[]): number {
let currentStyle = window.getComputedStyle(element);
let value: number = 0;
let styleValue: number = 0;
for (let i = 0, ii = styles.length; ii > i; ++i) {
styleValue = parseInt(currentStyle[styles[i]], 10);
value += Number.isNaN(styleValue) ? 0 : styleValue;
}
return value;
}

export function getElementDistanceToBottomViewPort(element: Element): number {
Expand Down
16 changes: 7 additions & 9 deletions src/virtual-repeat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import {
} from 'aurelia-templating-resources';
import {DOM} from 'aurelia-pal';
import {
getStyleValue,
calcOuterHeight,
rebindAndMoveView
rebindAndMoveView,
getStyleValues
} from './utilities';
import {DomHelper} from './dom-helper';
import {VirtualRepeatStrategyLocator} from './virtual-repeat-strategy-locator';
import {TemplateStrategyLocator, ITemplateStrategy} from './template-strategy';
import { DomHelper } from './dom-helper';
import { VirtualRepeatStrategyLocator } from './virtual-repeat-strategy-locator';
import { TemplateStrategyLocator, ITemplateStrategy } from './template-strategy';
import { IVirtualRepeat, IVirtualRepeatStrategy } from './interfaces';

export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
Expand Down Expand Up @@ -628,10 +628,8 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {

/**@internal*/
_calcScrollHeight(element: Element): number {
let height;
height = element.getBoundingClientRect().height;
height -= getStyleValue(element, 'borderTopWidth');
height -= getStyleValue(element, 'borderBottomWidth');
let height = element.getBoundingClientRect().height;
height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth');
return height;
}

Expand Down
84 changes: 84 additions & 0 deletions test/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { VirtualRepeat } from '../src/virtual-repeat';

export type Queue = (func: (...args: any[]) => any) => void;

export function createAssertionQueue(): Queue {
let queue: Array<() => any> = [];
let next = () => {
if (queue.length) {
setTimeout(() => {
let func = queue.shift();
func();
next();
});
}
};

return (func: () => any) => {
queue.push(func);
if (queue.length === 1) {
next();
}
};
}

export function validateState(virtualRepeat: VirtualRepeat, viewModel: any, itemHeight: number) {
let views = virtualRepeat.viewSlot.children;
let expectedHeight = viewModel.items.length * itemHeight;
let topBufferHeight = virtualRepeat.topBuffer.getBoundingClientRect().height;
let bottomBufferHeight = virtualRepeat.bottomBuffer.getBoundingClientRect().height;
let renderedItemsHeight = views.length * itemHeight;
expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(expectedHeight);

if (viewModel.items.length > views.length) {
expect(topBufferHeight + bottomBufferHeight).toBeGreaterThan(0);
}

// validate contextual data
for (let i = 0; i < views.length; i++) {
expect(views[i].bindingContext.item).toBe(viewModel.items[i]);
let overrideContext = views[i].overrideContext;
expect(overrideContext.parentOverrideContext.bindingContext).toBe(viewModel);
expect(overrideContext.bindingContext).toBe(views[i].bindingContext);
let first = i === 0;
let last = i === viewModel.items.length - 1;
let even = i % 2 === 0;
expect(overrideContext.$index).toBe(i);
expect(overrideContext.$first).toBe(first);
expect(overrideContext.$last).toBe(last);
expect(overrideContext.$middle).toBe(!first && !last);
expect(overrideContext.$odd).toBe(!even);
expect(overrideContext.$even).toBe(even);
}
}

export function validateScrolledState(virtualRepeat: VirtualRepeat, viewModel: any, itemHeight: number) {
let views = virtualRepeat.viewSlot.children;
let expectedHeight = viewModel.items.length * itemHeight;
let topBufferHeight = virtualRepeat.topBuffer.getBoundingClientRect().height;
let bottomBufferHeight = virtualRepeat.bottomBuffer.getBoundingClientRect().height;
let renderedItemsHeight = views.length * itemHeight;
expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(expectedHeight);

if (viewModel.items.length > views.length) {
expect(topBufferHeight + bottomBufferHeight).toBeGreaterThan(0);
}

// validate contextual data
let startingLoc = viewModel.items.indexOf(views[0].bindingContext.item);
for (let i = startingLoc; i < views.length; i++) {
expect(views[i].bindingContext.item).toBe(viewModel.items[i]);
let overrideContext = views[i].overrideContext;
expect(overrideContext.parentOverrideContext.bindingContext).toBe(viewModel);
expect(overrideContext.bindingContext).toBe(views[i].bindingContext);
let first = i === 0;
let last = i === viewModel.items.length - 1;
let even = i % 2 === 0;
expect(overrideContext.$index).toBe(i);
expect(overrideContext.$first).toBe(first);
expect(overrideContext.$last).toBe(last);
expect(overrideContext.$middle).toBe(!first && !last);
expect(overrideContext.$odd).toBe(!even);
expect(overrideContext.$even).toBe(even);
}
}
Loading

0 comments on commit 8271abe

Please sign in to comment.