Skip to content

Commit c1fe513

Browse files
committed
feat(template-strategy): adjust tbody template strategy
1 parent 8271abe commit c1fe513

File tree

7 files changed

+171
-37
lines changed

7 files changed

+171
-37
lines changed

karma.conf.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ module.exports = function(config) {
77

88
basePath: '',
99
frameworks: ["jasmine"],
10-
files: ["test/*.spec.ts"],
10+
files: ["test/**/*.spec.ts"],
1111
preprocessors: {
12-
"test/*.spec.ts": ["webpack", "sourcemap"]
12+
"test/**/*.spec.ts": ["webpack", "sourcemap"]
1313
},
1414
webpack: {
1515
mode: "development",
@@ -27,7 +27,12 @@ module.exports = function(config) {
2727
{
2828
test: /\.ts$/,
2929
loader: "ts-loader",
30-
exclude: /node_modules/
30+
exclude: /node_modules/,
31+
options: {
32+
compilerOptions: {
33+
sourceMap: true
34+
}
35+
}
3136
},
3237
{
3338
test: /\.html$/i,

src/interfaces.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Repeat, RepeatStrategy } from 'aurelia-templating-resources';
22
import { ViewSlot, View, ViewFactory, BoundViewFactory, Controller } from 'aurelia-templating';
33
import { Scope, Binding } from 'aurelia-binding';
4-
import { ITemplateStrategy } from './template-strategy';
54
import { TaskQueue } from 'aurelia-task-queue';
65

76
/**@internal */
@@ -120,3 +119,15 @@ export interface IVirtualRepeat extends Repeat {
120119
/**@internal*/ __queuedSplices: any[];
121120
/**@internal*/ __array: any[];
122121
}
122+
123+
export interface ITemplateStrategy {
124+
getScrollContainer(element: Element): HTMLElement;
125+
moveViewFirst(view: View, topBuffer: Element): void;
126+
moveViewLast(view: View, bottomBuffer: Element): void;
127+
createTopBufferElement(element: Element): HTMLElement;
128+
createBottomBufferElement(element: Element): HTMLElement;
129+
removeBufferElements(element: Element, topBuffer: Element, bottomBuffer: Element): void;
130+
getFirstElement(topBuffer: Element): Element;
131+
getLastElement(bottomBuffer: Element): Element;
132+
getTopBufferDistance(topBuffer: Element): number;
133+
}

src/template-strategy.ts

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,7 @@ import { DOM } from 'aurelia-pal';
33
import { View } from 'aurelia-templating';
44
import { DomHelper } from './dom-helper';
55
import { insertBeforeNode } from './utilities';
6-
7-
export interface ITemplateStrategy {
8-
getScrollContainer(element: Element): HTMLElement;
9-
moveViewFirst(view: View, topBuffer: Element): void;
10-
moveViewLast(view: View, bottomBuffer: Element): void;
11-
createTopBufferElement(element: Element): HTMLElement;
12-
createBottomBufferElement(element: Element): HTMLElement;
13-
removeBufferElements(element: Element, topBuffer: Element, bottomBuffer: Element): void;
14-
getFirstElement(topBuffer: Element): Element;
15-
getLastElement(bottomBuffer: Element): Element;
16-
getTopBufferDistance(topBuffer: Element): number;
17-
}
6+
import { ITemplateStrategy } from './interfaces';
187

198
export class TemplateStrategyLocator {
209

@@ -30,14 +19,96 @@ export class TemplateStrategyLocator {
3019
* Selects the template strategy based on element hosting `virtual-repeat` custom attribute
3120
*/
3221
getStrategy(element: Element): ITemplateStrategy {
33-
if (element.parentNode && (element.parentNode as Element).tagName === 'TBODY') {
34-
return this.container.get(TableStrategy);
22+
const parent = element.parentElement;
23+
if (parent === null) {
24+
return this.container.get(DefaultTemplateStrategy);
25+
}
26+
const parentTagName = parent.tagName;
27+
// placed on tr, as it is automatically wrapped in a TBODY
28+
// if not wrapped, then it is already inside a thead or tfoot
29+
if (parentTagName === 'TBODY' || parentTagName === 'THEAD' || parentTagName === 'TFOOT') {
30+
return this.container.get(TableRowStrategy);
31+
}
32+
// place on a tbody/thead/tfoot
33+
if (parentTagName === 'TABLE') {
34+
return this.container.get(TableBodyStrategy);
3535
}
36+
// if (element.parentNode && (element.parentNode as Element).tagName === 'TBODY') {
37+
// return this.container.get(TableStrategy);
38+
// }
3639
return this.container.get(DefaultTemplateStrategy);
3740
}
3841
}
3942

40-
export class TableStrategy implements ITemplateStrategy {
43+
export class TableBodyStrategy implements ITemplateStrategy {
44+
45+
46+
getScrollContainer(element: Element): HTMLElement {
47+
return this.getTable(element).parentNode as HTMLElement;
48+
}
49+
50+
moveViewFirst(view: View, topBuffer: Element): void {
51+
insertBeforeNode(view, DOM.nextElementSibling(topBuffer));
52+
}
53+
54+
moveViewLast(view: View, bottomBuffer: Element): void {
55+
const previousSibling = bottomBuffer.previousSibling;
56+
const referenceNode = previousSibling.nodeType === 8 && (previousSibling as Comment).data === 'anchor' ? previousSibling : bottomBuffer;
57+
insertBeforeNode(view, referenceNode as Element);
58+
}
59+
60+
createTopBufferElement(element: Element): HTMLElement {
61+
// append tbody with empty row before the element
62+
return element.parentNode.insertBefore(DOM.createElement('tr'), element);
63+
}
64+
65+
createBottomBufferElement(element: Element): HTMLElement {
66+
return element.parentNode.insertBefore(DOM.createElement('tr'), element.nextSibling);
67+
}
68+
69+
removeBufferElements(element: Element, topBuffer: Element, bottomBuffer: Element): void {
70+
DOM.removeNode(topBuffer);
71+
DOM.removeNode(bottomBuffer);
72+
}
73+
74+
getFirstElement(topBuffer: Element): Element {
75+
return topBuffer.nextElementSibling;
76+
}
77+
78+
getLastElement(bottomBuffer: Element): Element {
79+
return bottomBuffer.previousElementSibling;
80+
}
81+
82+
getTopBufferDistance(topBuffer: Element): number {
83+
return 0;
84+
}
85+
86+
private getFirstTbody(tableElement: HTMLTableElement): HTMLTableSectionElement {
87+
let child = tableElement.firstElementChild;
88+
while (child !== null && child.tagName !== 'TBODY') {
89+
child = child.nextElementSibling;
90+
}
91+
return child.tagName === 'TBODY' ? child as HTMLTableSectionElement : null;
92+
}
93+
94+
private _getLastTbody(tableElement: HTMLTableElement): HTMLTableSectionElement {
95+
let child = tableElement.lastElementChild;
96+
while (child !== null && child.tagName !== 'TBODY') {
97+
child = child.previousElementSibling;
98+
}
99+
return child.tagName === 'TBODY' ? child as HTMLTableSectionElement : null;
100+
}
101+
102+
/**
103+
* `element` is actually a comment, acting as anchor for `virtual-repeat` attribute
104+
* `element` will be placed next to a tbody
105+
*/
106+
private getTable(element: Element): HTMLTableElement {
107+
return element.parentNode as HTMLTableElement;
108+
}
109+
}
110+
111+
export class TableRowStrategy implements ITemplateStrategy {
41112

42113
static inject = [DomHelper];
43114

@@ -80,7 +151,7 @@ export class TableStrategy implements ITemplateStrategy {
80151
createTopBufferElement(element: Element): HTMLElement {
81152
const elementName = /^[UO]L$/.test((element.parentNode as Element).tagName) ? 'li' : 'div';
82153
const buffer = DOM.createElement(elementName);
83-
const tableElement = element.parentNode.parentNode;
154+
const tableElement = this.getTable(element);
84155
tableElement.parentNode.insertBefore(buffer, tableElement);
85156
buffer.innerHTML = ' ';
86157
return buffer;
@@ -89,7 +160,7 @@ export class TableStrategy implements ITemplateStrategy {
89160
createBottomBufferElement(element: Element): HTMLElement {
90161
const elementName = /^[UO]L$/.test((element.parentNode as Element).tagName) ? 'li' : 'div';
91162
const buffer = DOM.createElement(elementName);
92-
const tableElement = element.parentNode.parentNode;
163+
const tableElement = this.getTable(element);
93164
tableElement.parentNode.insertBefore(buffer, tableElement.nextSibling);
94165
return buffer;
95166
}
@@ -131,6 +202,13 @@ export class TableStrategy implements ITemplateStrategy {
131202
}
132203
return child.tagName === 'TBODY' ? child as HTMLTableSectionElement : null;
133204
}
205+
206+
/**
207+
* `element` is actually a comment, acting as anchor for `virtual-repeat` attribute
208+
*/
209+
private getTable(element: Element): HTMLTableElement {
210+
return element.parentNode.parentNode as HTMLTableElement;
211+
}
134212
}
135213

136214
export class DefaultTemplateStrategy implements ITemplateStrategy {

src/virtual-repeat.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ import {
2323
} from './utilities';
2424
import { DomHelper } from './dom-helper';
2525
import { VirtualRepeatStrategyLocator } from './virtual-repeat-strategy-locator';
26-
import { TemplateStrategyLocator, ITemplateStrategy } from './template-strategy';
27-
import { IVirtualRepeat, IVirtualRepeatStrategy } from './interfaces';
26+
import { TemplateStrategyLocator } from './template-strategy';
27+
import {
28+
IVirtualRepeat,
29+
IVirtualRepeatStrategy,
30+
ITemplateStrategy
31+
} from './interfaces';
2832

2933
export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
3034

@@ -499,6 +503,10 @@ export class VirtualRepeat extends AbstractRepeater implements IVirtualRepeat {
499503
}
500504

501505
_adjustBufferHeights(): void {
506+
// let templateStrategy = this.templateStrategy;
507+
// let { topBuffer, _topBufferHeight, bottomBuffer, _bottomBufferHeight } = this;
508+
// templateStrategy.adjustBufferHeight(topBuffer, _topBufferHeight);
509+
// templateStrategy.adjustBufferHeight(bottomBuffer, _bottomBufferHeight);
502510
this.topBuffer.style.height = `${this._topBufferHeight}px`;
503511
this.bottomBuffer.style.height = `${this._bottomBufferHeight}px`;
504512
}

test/utilities.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export function validateState(virtualRepeat: VirtualRepeat, viewModel: any, item
2828
let topBufferHeight = virtualRepeat.topBuffer.getBoundingClientRect().height;
2929
let bottomBufferHeight = virtualRepeat.bottomBuffer.getBoundingClientRect().height;
3030
let renderedItemsHeight = views.length * itemHeight;
31-
expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(expectedHeight);
31+
expect(topBufferHeight + renderedItemsHeight + bottomBufferHeight).toBe(
32+
expectedHeight,
33+
`Top buffer (${topBufferHeight}) + items height (${renderedItemsHeight}) + bottom buffer (${bottomBufferHeight}) should have been correct`
34+
);
3235

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

test/virtual-repeat-integration.table.spec.ts

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ fdescribe('VirtualRepeat Integration', () => {
1111
const itemHeight = 100;
1212
const nq = createAssertionQueue();
1313

14-
describe('iterating table', () => {
14+
describe('<tr virtual-repeat.for>', () => {
1515
let component: ComponentTester;
1616
let virtualRepeat;
1717
let viewModel;
@@ -52,7 +52,7 @@ fdescribe('VirtualRepeat Integration', () => {
5252
});
5353
});
5454

55-
fdescribe('<tbody virtual-repeat.for>', () => {
55+
describe('<tbody virtual-repeat.for>', () => {
5656
let component: ComponentTester;
5757
let virtualRepeat: VirtualRepeat;
5858
let viewModel;
@@ -85,17 +85,47 @@ fdescribe('VirtualRepeat Integration', () => {
8585
}
8686
});
8787

88-
it('works', async (done) => {
88+
it('creates right structure', async (done) => {
89+
try {
90+
component.inView('<table><tbody virtual-repeat.for="item of items"><tr><td>\${item}</td></tr></tbody>');
91+
await component.create().then(() => {
92+
virtualRepeat = component.sut;
93+
viewModel = component.viewModel;
94+
});
95+
const element = virtualRepeat['element'];
96+
const { topBuffer, bottomBuffer } = virtualRepeat;
97+
expect(topBuffer.nextElementSibling.tagName).toBe('TBODY');
98+
expect(topBuffer.tagName).toBe('TR');
99+
expect(topBuffer.childNodes.length).toBe(0);
100+
expect(bottomBuffer.previousSibling.nodeType).toBe(Node.COMMENT_NODE);
101+
expect(bottomBuffer.previousElementSibling.tagName).toBe('TBODY');
102+
expect(bottomBuffer.tagName).toBe('TR');
103+
expect(bottomBuffer.childNodes.length).toBe(0);
104+
done();
105+
} catch (ex) {
106+
done.fail(ex);
107+
}
108+
});
89109

90-
component.inView('<table><tbody virtual-repeat.for="item of items"><tr><td>\${item}</td></tr></tbody>');
91-
await component.create().then(() => {
92-
virtualRepeat = component.sut;
93-
viewModel = component.viewModel;
94-
});
95-
const element = virtualRepeat['element'];
96-
expect(virtualRepeat.topBuffer.nextElementSibling.tagName).toBe('TABLE');
97-
expect(virtualRepeat.bottomBuffer.previousElementSibling.tagName).toBe('TABLE');
98-
done();
110+
it('works', async (done) => {
111+
try {
112+
component.inView(
113+
// there is a small border spacing between tbodies, rows that will add up
114+
// need to add border spacing 0 for testing purposes
115+
`<table style="border-spacing: 0">
116+
<tbody virtual-repeat.for="item of items">
117+
<tr style="height: ${itemHeight}px;"><td>\${item}</td></tr>
118+
</tbody>
119+
</table>`);
120+
await component.create().then(() => {
121+
virtualRepeat = component.sut;
122+
viewModel = component.viewModel;
123+
});
124+
nq(() => validateState(virtualRepeat, viewModel, itemHeight));
125+
nq(() => done());
126+
} catch (ex) {
127+
done.fail(ex);
128+
}
99129
});
100130
});
101131

tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
"dist",
1616
"build",
1717
"doc",
18-
"test",
1918
"config.js",
2019
"gulpfile.js",
2120
"karma.conf.js"

0 commit comments

Comments
 (0)