Skip to content

Commit 8271abe

Browse files
committed
feat(template-strategy): diff between tbody and tr
1 parent d48c5a1 commit 8271abe

File tree

9 files changed

+316
-207
lines changed

9 files changed

+316
-207
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ There are three parameters that are passed to the function (`getMore(topIndex, i
132132
3. `isAtTop` - A boolean value that indicates whether the list has been scrolled to the top of the items list.
133133

134134

135+
## Caveats
136+
137+
- `<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.
138+
135139
## [Demo](http://aurelia.io/ui-virtualization/)
136140

137141
## Platform Support

karma.conf.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,15 @@ module.exports = function(config) {
6868
]
6969
}
7070
},
71-
singleRun: false
71+
singleRun: false,
72+
mochaReporter: {
73+
ignoreSkipped: true
74+
},
75+
webpackMiddleware: {
76+
// webpack-dev-middleware configuration
77+
// i. e.
78+
stats: 'errors-only'
79+
}
7280
});
7381
};
7482

src/interfaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ declare module 'aurelia-binding' {
1717
isAtBottom: boolean;
1818
isAtTop: boolean;
1919
};
20+
$first: boolean;
21+
$last: boolean;
22+
$middle: boolean;
23+
$odd: boolean;
24+
$even: boolean;
2025
}
2126
}
2227

src/template-strategy.ts

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export interface ITemplateStrategy {
1313
removeBufferElements(element: Element, topBuffer: Element, bottomBuffer: Element): void;
1414
getFirstElement(topBuffer: Element): Element;
1515
getLastElement(bottomBuffer: Element): Element;
16-
getLastView(bottomBuffer: Element): Element;
1716
getTopBufferDistance(topBuffer: Element): number;
1817
}
1918

@@ -27,6 +26,9 @@ export class TemplateStrategyLocator {
2726
this.container = container;
2827
}
2928

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

4042
static inject = [DomHelper];
4143

42-
tableCssReset = '\
43-
display: block;\
44-
width: auto;\
45-
height: auto;\
46-
margin: 0;\
47-
padding: 0;\
48-
border: none;\
49-
border-collapse: inherit;\
50-
border-spacing: 0;\
51-
background-color: transparent;\
52-
-webkit-border-horizontal-spacing: 0;\
53-
-webkit-border-vertical-spacing: 0;';
44+
// tableCssReset = '\
45+
// display: block;\
46+
// width: auto;\
47+
// height: auto;\
48+
// margin: 0;\
49+
// padding: 0;\
50+
// border: none;\
51+
// border-collapse: inherit;\
52+
// border-spacing: 0;\
53+
// background-color: transparent;\
54+
// -webkit-border-horizontal-spacing: 0;\
55+
// -webkit-border-vertical-spacing: 0;';
5456

5557
domHelper: DomHelper;
5658

@@ -63,7 +65,7 @@ export class TableStrategy implements ITemplateStrategy {
6365
}
6466

6567
moveViewFirst(view: View, topBuffer: Element): void {
66-
const tbody = this._getTbodyElement(topBuffer.nextSibling as Element);
68+
const tbody = this._getFirstTbody(topBuffer.nextSibling as HTMLTableElement);
6769
const tr = tbody.firstChild;
6870
const firstElement = DOM.nextElementSibling(tr);
6971
insertBeforeNode(view, firstElement);
@@ -98,45 +100,41 @@ export class TableStrategy implements ITemplateStrategy {
98100
}
99101

100102
getFirstElement(topBuffer: Element): Element {
101-
const tbody = this._getTbodyElement(DOM.nextElementSibling(topBuffer));
102-
const tr = tbody.firstChild as HTMLTableRowElement;
103+
const tbody = this._getFirstTbody(DOM.nextElementSibling(topBuffer) as HTMLTableElement);
104+
const tr = tbody.firstElementChild as HTMLTableRowElement;
103105
// since the buffer is outside table, first element _is_ first element.
104106
return tr;
105107
}
106108

107109
getLastElement(bottomBuffer: Element): Element {
108-
const tbody = this._getTbodyElement(bottomBuffer.previousSibling as Element);
109-
const trs = tbody.children;
110-
return trs[trs.length - 1];
110+
const tbody = this._getLastTbody(bottomBuffer.previousSibling as HTMLTableElement);
111+
return tbody.lastElementChild as HTMLTableRowElement;
111112
}
112113

113114
getTopBufferDistance(topBuffer: Element): number {
114-
const tbody = this._getTbodyElement(topBuffer.nextSibling as Element);
115+
const tbody = this._getFirstTbody(topBuffer.nextSibling as HTMLTableElement);
115116
return this.domHelper.getElementDistanceToTopOfDocument(tbody) - this.domHelper.getElementDistanceToTopOfDocument(topBuffer);
116117
}
117118

118-
getLastView(bottomBuffer: Element): Element {
119-
throw new Error('Method getLastView() not implemented.');
119+
private _getFirstTbody(tableElement: HTMLTableElement): HTMLTableSectionElement {
120+
let child = tableElement.firstElementChild;
121+
while (child !== null && child.tagName !== 'TBODY') {
122+
child = child.nextElementSibling;
123+
}
124+
return child.tagName === 'TBODY' ? child as HTMLTableSectionElement : null;
120125
}
121126

122-
private _getTbodyElement(tableElement: Element): Element {
123-
let tbodyElement: Element;
124-
const children = tableElement.children;
125-
for (let i = 0, ii = children.length; i < ii; ++i) {
126-
if (children[i].localName === 'tbody') {
127-
tbodyElement = children[i];
128-
break;
129-
}
127+
private _getLastTbody(tableElement: HTMLTableElement): HTMLTableSectionElement {
128+
let child = tableElement.lastElementChild;
129+
while (child !== null && child.tagName !== 'TBODY') {
130+
child = child.previousElementSibling;
130131
}
131-
return tbodyElement;
132+
return child.tagName === 'TBODY' ? child as HTMLTableSectionElement : null;
132133
}
133134
}
134135

135136
export class DefaultTemplateStrategy implements ITemplateStrategy {
136137

137-
getLastView(bottomBuffer: Element): Element {
138-
throw new Error('Method getLastView() not implemented.');
139-
}
140138
getScrollContainer(element: Element): HTMLElement {
141139
return element.parentNode as HTMLElement;
142140
}

src/utilities.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import {View} from 'aurelia-templating';
33
import { IVirtualRepeat } from './interfaces';
44

55
export function calcOuterHeight(element: Element): number {
6-
let height: number;
7-
height = element.getBoundingClientRect().height;
8-
height += getStyleValue(element, 'marginTop');
9-
height += getStyleValue(element, 'marginBottom');
6+
let height = element.getBoundingClientRect().height;
7+
height += getStyleValues(element, 'marginTop', 'marginBottom');
108
return height;
119
}
1210

@@ -49,12 +47,15 @@ export function rebindAndMoveView(repeat: IVirtualRepeat, view: View, index: num
4947
}
5048
}
5149

52-
export function getStyleValue(element: Element, style: string): number {
53-
let currentStyle: CSSStyleDeclaration;
54-
let styleValue: number;
55-
currentStyle = element['currentStyle'] || window.getComputedStyle(element);
56-
styleValue = parseInt(currentStyle[style], 10);
57-
return Number.isNaN(styleValue) ? 0 : styleValue;
50+
export function getStyleValues(element: Element, ...styles: string[]): number {
51+
let currentStyle = window.getComputedStyle(element);
52+
let value: number = 0;
53+
let styleValue: number = 0;
54+
for (let i = 0, ii = styles.length; ii > i; ++i) {
55+
styleValue = parseInt(currentStyle[styles[i]], 10);
56+
value += Number.isNaN(styleValue) ? 0 : styleValue;
57+
}
58+
return value;
5859
}
5960

6061
export function getElementDistanceToBottomViewPort(element: Element): number {

src/virtual-repeat.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ import {
1717
} from 'aurelia-templating-resources';
1818
import {DOM} from 'aurelia-pal';
1919
import {
20-
getStyleValue,
2120
calcOuterHeight,
22-
rebindAndMoveView
21+
rebindAndMoveView,
22+
getStyleValues
2323
} from './utilities';
24-
import {DomHelper} from './dom-helper';
25-
import {VirtualRepeatStrategyLocator} from './virtual-repeat-strategy-locator';
26-
import {TemplateStrategyLocator, ITemplateStrategy} from './template-strategy';
24+
import { DomHelper } from './dom-helper';
25+
import { VirtualRepeatStrategyLocator } from './virtual-repeat-strategy-locator';
26+
import { TemplateStrategyLocator, ITemplateStrategy } from './template-strategy';
2727
import { IVirtualRepeat, IVirtualRepeatStrategy } from './interfaces';
2828

2929
export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
@@ -628,10 +628,8 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
628628

629629
/**@internal*/
630630
_calcScrollHeight(element: Element): number {
631-
let height;
632-
height = element.getBoundingClientRect().height;
633-
height -= getStyleValue(element, 'borderTopWidth');
634-
height -= getStyleValue(element, 'borderBottomWidth');
631+
let height = element.getBoundingClientRect().height;
632+
height -= getStyleValues(element, 'borderTopWidth', 'borderBottomWidth');
635633
return height;
636634
}
637635

test/utilities.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { VirtualRepeat } from '../src/virtual-repeat';
2+
3+
export type Queue = (func: (...args: any[]) => any) => void;
4+
5+
export function createAssertionQueue(): Queue {
6+
let queue: Array<() => any> = [];
7+
let next = () => {
8+
if (queue.length) {
9+
setTimeout(() => {
10+
let func = queue.shift();
11+
func();
12+
next();
13+
});
14+
}
15+
};
16+
17+
return (func: () => any) => {
18+
queue.push(func);
19+
if (queue.length === 1) {
20+
next();
21+
}
22+
};
23+
}
24+
25+
export function validateState(virtualRepeat: VirtualRepeat, viewModel: any, itemHeight: number) {
26+
let views = virtualRepeat.viewSlot.children;
27+
let expectedHeight = viewModel.items.length * itemHeight;
28+
let topBufferHeight = virtualRepeat.topBuffer.getBoundingClientRect().height;
29+
let bottomBufferHeight = virtualRepeat.bottomBuffer.getBoundingClientRect().height;
30+
let renderedItemsHeight = views.length * itemHeight;
31+
expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(expectedHeight);
32+
33+
if (viewModel.items.length > views.length) {
34+
expect(topBufferHeight + bottomBufferHeight).toBeGreaterThan(0);
35+
}
36+
37+
// validate contextual data
38+
for (let i = 0; i < views.length; i++) {
39+
expect(views[i].bindingContext.item).toBe(viewModel.items[i]);
40+
let overrideContext = views[i].overrideContext;
41+
expect(overrideContext.parentOverrideContext.bindingContext).toBe(viewModel);
42+
expect(overrideContext.bindingContext).toBe(views[i].bindingContext);
43+
let first = i === 0;
44+
let last = i === viewModel.items.length - 1;
45+
let even = i % 2 === 0;
46+
expect(overrideContext.$index).toBe(i);
47+
expect(overrideContext.$first).toBe(first);
48+
expect(overrideContext.$last).toBe(last);
49+
expect(overrideContext.$middle).toBe(!first && !last);
50+
expect(overrideContext.$odd).toBe(!even);
51+
expect(overrideContext.$even).toBe(even);
52+
}
53+
}
54+
55+
export function validateScrolledState(virtualRepeat: VirtualRepeat, viewModel: any, itemHeight: number) {
56+
let views = virtualRepeat.viewSlot.children;
57+
let expectedHeight = viewModel.items.length * itemHeight;
58+
let topBufferHeight = virtualRepeat.topBuffer.getBoundingClientRect().height;
59+
let bottomBufferHeight = virtualRepeat.bottomBuffer.getBoundingClientRect().height;
60+
let renderedItemsHeight = views.length * itemHeight;
61+
expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(expectedHeight);
62+
63+
if (viewModel.items.length > views.length) {
64+
expect(topBufferHeight + bottomBufferHeight).toBeGreaterThan(0);
65+
}
66+
67+
// validate contextual data
68+
let startingLoc = viewModel.items.indexOf(views[0].bindingContext.item);
69+
for (let i = startingLoc; i < views.length; i++) {
70+
expect(views[i].bindingContext.item).toBe(viewModel.items[i]);
71+
let overrideContext = views[i].overrideContext;
72+
expect(overrideContext.parentOverrideContext.bindingContext).toBe(viewModel);
73+
expect(overrideContext.bindingContext).toBe(views[i].bindingContext);
74+
let first = i === 0;
75+
let last = i === viewModel.items.length - 1;
76+
let even = i % 2 === 0;
77+
expect(overrideContext.$index).toBe(i);
78+
expect(overrideContext.$first).toBe(first);
79+
expect(overrideContext.$last).toBe(last);
80+
expect(overrideContext.$middle).toBe(!first && !last);
81+
expect(overrideContext.$odd).toBe(!even);
82+
expect(overrideContext.$even).toBe(even);
83+
}
84+
}

0 commit comments

Comments
 (0)