feat(core): Repeater multiple item templates implementation (#8981)

This commit is contained in:
Dimitris - Rafail Katsampas
2020-10-27 05:13:22 +02:00
committed by GitHub
parent 0bbdeaf0b5
commit b113f1916d
12 changed files with 343 additions and 21 deletions

View File

@ -1,6 +1,6 @@
import * as TKUnit from '../../tk-unit';
import * as helper from '../../ui-helper';
import { Application, Label, Page, StackLayout, WrapLayout, LayoutBase, View, GestureTypes, Repeater, ObservableArray } from '@nativescript/core';
import { Application, Label, Page, StackLayout, WrapLayout, LayoutBase, View, KeyedTemplate, GestureTypes, Repeater, ObservableArray } from '@nativescript/core';
var FEW_ITEMS = [0, 1, 2];
var MANY_ITEMS = [];
@ -8,6 +8,18 @@ for (var i = 0; i < 100; i++) {
MANY_ITEMS[i] = i;
}
const ITEM_TEMPLATES_STRING = `
<template key="red">
<Label text='red' style.backgroundColor='red' minHeight='100' maxHeight='100'/>
</template>
<template key='green'>
<Label text='green' style.backgroundColor='green' minHeight='100' maxHeight='100'/>
</template>
<template key='blue'>
<Label text='blue' style.backgroundColor='blue' minHeight='100' maxHeight='100'/>
</template>
`;
export function test_recycling() {
const setters = new Map<string, StackLayout>();
setters.set('itemsLayout', new StackLayout());
@ -309,6 +321,117 @@ export function test_ItemTemplateFactoryFunction() {
helper.buildUIAndRunTest(repeater, testAction);
}
// Multiple item templates tests
export function test_ItemTemplateSelector_WhenWrongTemplateKeyIsSpecified_TheDefaultTemplateIsUsed() {
var repeater = new Repeater();
function testAction(views: Array<View>) {
repeater.itemTemplate = "<Label text='default' minHeight='100' maxHeight='100'/>";
repeater.itemTemplates = ITEM_TEMPLATES_STRING;
repeater.itemTemplateSelector = "age == 20 ? 'wrong' : 'green'";
repeater.items = [{ age: 20 }, { age: 25 }];
TKUnit.waitUntilReady(() => repeater.isLayoutValid);
const firstElement = getChildAt(repeater, 0);
TKUnit.assertEqual((<Label>firstElement).text, 'default', 'first element text');
}
helper.buildUIAndRunTest(repeater, testAction);
}
export function test_ItemTemplateSelector_IsCorrectlyParsedFromString() {
var repeater = new Repeater();
function testAction(views: Array<View>) {
repeater.itemTemplates = ITEM_TEMPLATES_STRING;
repeater.itemTemplateSelector = "age < 25 ? 'red' : 'green'";
repeater.items = [{ age: 20 }, { age: 25 }];
let itemTemplateSelectorFunction = <any>repeater.itemTemplateSelector;
TKUnit.waitUntilReady(() => repeater.isLayoutValid);
let templateKey0 = itemTemplateSelectorFunction(repeater.items[0], 0, repeater.items);
TKUnit.assertEqual(templateKey0, 'red', 'itemTemplateSelector result for first item');
let templateKey1 = itemTemplateSelectorFunction(repeater.items[1], 1, repeater.items);
TKUnit.assertEqual(templateKey1, 'green', 'itemTemplateSelector result for second item');
}
helper.buildUIAndRunTest(repeater, testAction);
}
export function test_ItemTemplateSelector_IsCorrectlyUsedAsAFunction() {
var repeater = new Repeater();
function testAction(views: Array<View>) {
repeater.itemTemplates = ITEM_TEMPLATES_STRING;
repeater.itemTemplateSelector = function (item, index: number, items) {
return item.age < 25 ? 'red' : 'green';
};
repeater.items = [{ age: 20 }, { age: 25 }];
let itemTemplateSelectorFunction = <any>repeater.itemTemplateSelector;
TKUnit.waitUntilReady(() => repeater.isLayoutValid);
let templateKey0 = itemTemplateSelectorFunction(repeater.items[0], 0, repeater.items);
TKUnit.assertEqual(templateKey0, 'red', 'itemTemplateSelector result for first item');
let templateKey1 = itemTemplateSelectorFunction(repeater.items[1], 1, repeater.items);
TKUnit.assertEqual(templateKey1, 'green', 'itemTemplateSelector result for second item');
}
helper.buildUIAndRunTest(repeater, testAction);
}
export function test_ItemTemplateSelector_ItemTemplatesAreCorrectlyParsedFromString() {
var repeater = new Repeater();
function testAction(views: Array<View>) {
repeater.itemTemplates = ITEM_TEMPLATES_STRING;
TKUnit.waitUntilReady(() => repeater.isLayoutValid);
let itemTemplatesArray = <any>repeater.itemTemplates;
TKUnit.assertEqual(itemTemplatesArray.length, 3, 'itemTemplates array length');
let template0 = <KeyedTemplate>itemTemplatesArray[0];
TKUnit.assertEqual(template0.key, 'red', 'template0.key');
TKUnit.assertEqual((<Label>template0.createView()).text, 'red', 'template0 created view text');
let template1 = <KeyedTemplate>itemTemplatesArray[1];
TKUnit.assertEqual(template1.key, 'green', 'template1.key');
TKUnit.assertEqual((<Label>template1.createView()).text, 'green', 'template1 created view text');
let template2 = <KeyedTemplate>itemTemplatesArray[2];
TKUnit.assertEqual(template2.key, 'blue', 'template2.key');
TKUnit.assertEqual((<Label>template2.createView()).text, 'blue', 'template2 created view text');
}
helper.buildUIAndRunTest(repeater, testAction);
}
export function test_ItemTemplateSelector_CorrectTemplateIsUsed() {
var repeater = new Repeater();
function testAction(views: Array<View>) {
repeater.itemTemplates = ITEM_TEMPLATES_STRING;
repeater.itemTemplateSelector = "age == 25 ? 'green' : 'red'";
repeater.items = [{ age: 20 }, { age: 25 }];
TKUnit.waitUntilReady(() => repeater.isLayoutValid);
const firstElement = getChildAt(repeater, 0);
const secondElement = getChildAt(repeater, 1);
TKUnit.assertEqual((<Label>firstElement).text, 'red', 'first element text');
TKUnit.assertEqual((<Label>secondElement).text, 'green', 'second element text');
}
helper.buildUIAndRunTest(repeater, testAction);
}
export function test_BindingRepeaterToASimpleArrayWithExpression() {
var repeater = new Repeater();

View File

@ -16,6 +16,16 @@ Other modules which will be used in the code samples in this article:
{%raw%}<Repeater items="{{ myItems }}" />{%endraw%}
</Page>
```
### Define the Repeater itemsLayout property. Default is StackLayout with orientation="vertical".
``` XML
<Page>
{%raw%}<Repeater items="{{ myItems }}">
<Repeater.itemsLayout>
<StackLayout orientation="horizontal" />
</Repeater.itemsLayout>
</Repeater>{%endraw%}
</Page>
```
### Define the Repeater itemTemplate property.
``` XML
<Page>
@ -26,13 +36,52 @@ Other modules which will be used in the code samples in this article:
</Repeater>{%endraw%}
</Page>
```
### Define the Repeater itemsLayout property. Default is StackLayout with orientation="vertical".
### Define multiple item templates and an item template selector in XML.
The itemTemplateSelector can be an expression specified directly in XML. The context of the expression is the data item for each row.
``` XML
<Page>
{%raw%}<Repeater items="{{ myItems }}">
<Repeater.itemsLayout>
<StackLayout orientation="horizontal" />
</Repeater.itemsLayout>
{%raw%}<Repeater items="{{ myItems }}" itemTemplateSelector="age > 18 ? 'green' : 'red'">
<Repeater.itemTemplates>
<template key="green">
<Label text="{{ age }}" style.backgroundColor="green" />
</template>
<template key="red">
<Label text="{{ age }}" style.backgroundColor="red" />
</template>
</Repeater.itemTemplates>
</Repeater>{%endraw%}
</Page>
```
### Specifying the item template selector as a function in the code-behind file
In case your item template selector involves complicated logic which cannot be expressed with an expression, you can create an item template selector function in the code behind of the page in which the RepeaterRepeater resides. The function receives the respective data item, the row index and the entire Repeater items collection as parameters. It has to return the the key of the template to be used based on the supplied information.
``` XML
<Page>
{%raw%}<Repeater items="{{ myItems }}" itemTemplateSelector="selectItemTemplate">
<Repeater.itemTemplates>
<template key="green">
<Label text="{{ age }}" style.backgroundColor="green" />
</template>
<template key="red">
<Label text="{{ age }}" style.backgroundColor="red" />
</template>
</Repeater.itemTemplates>
</Repeater>{%endraw%}
</Page>
```
{%snippet article-item-template-selector-function%}
### Alternating row colors
You can use the special value '$index' in the item template selector expression which represents the row index.
``` XML
<Page>
{%raw%}<Repeater items="{{ myItems }}" itemTemplateSelector="$index % 2 === 0 ? 'even' : 'odd'">
<Repeater.itemTemplates>
<template key="even">
<Label text="{{ age }}" style.backgroundColor="white" />
</template>
<template key="odd">
<Label text="{{ age }}" style.backgroundColor="gray" />
</template>
</Repeater.itemTemplates>
</Repeater>{%endraw%}
</Page>
```

View File

@ -37,6 +37,7 @@ export function pageLoaded(args: EventData) {
examples.set('text-view', 'text-view/main-page');
examples.set('webview', 'web-view/main-page');
examples.set('progress-bar', 'progress-bar/main-page');
examples.set('repeater', 'repeater/main-page');
examples.set('date-picker', 'date-picker/date-picker-page');
examples.set('nested-frames', 'nested-frames/main-page');
examples.set('screen-qualifiers', 'screen-qualifiers/main-page');

View File

@ -0,0 +1,17 @@
import { EventData } from '@nativescript/core/data/observable';
import { SubMainPageViewModel } from '../sub-main-page-view-model';
import { WrapLayout } from '@nativescript/core/ui/layouts/wrap-layout';
import { Page } from '@nativescript/core/ui/page';
export function pageLoaded(args: EventData) {
const page = <Page>args.object;
const wrapLayout = <WrapLayout>page.getViewById('wrapLayoutWithExamples');
page.bindingContext = new SubMainPageViewModel(wrapLayout, loadExamples());
}
export function loadExamples() {
const examples = new Map<string, string>();
examples.set('multi-templates', 'repeater/multi-templates-page');
return examples;
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Page loaded="pageLoaded">
<ScrollView orientation="vertical" row="1">
<WrapLayout id="wrapLayoutWithExamples"/>
</ScrollView>
</Page>

View File

@ -0,0 +1,13 @@
import { EventData } from '@nativescript/core/data/observable';
import { ObservableArray } from '@nativescript/core/data/observable-array';
import { Page } from '@nativescript/core/ui/page';
import { MultiTemplatesViewModel } from './multi-templates-view-model';
export function pageLoaded(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new MultiTemplatesViewModel();
}
export function getOddEvenTemplate(item: number, index: number, items: ObservableArray<number>): string {
return index % 2 === 0 ? 'even' : '';
}

View File

@ -0,0 +1,18 @@
<Page loaded="pageLoaded">
<ScrollView>
<Repeater items="{{ items }}" itemTemplateSelector="getOddEvenTemplate">
<Repeater.itemTemplate>
<StackLayout backgroundColor="blue" height="40">
<Label text="{{ $value }}" class="p-r-10" color="#fff" fontSize="16" />
</StackLayout>
</Repeater.itemTemplate>
<Repeater.itemTemplates>
<template key="even">
<StackLayout backgroundColor="green" height="40">
<Label text="{{ $value }}" class="p-r-10" color="#fff" fontSize="16" />
</StackLayout>
</template>
</Repeater.itemTemplates>
</Repeater>
</ScrollView>
</Page>

View File

@ -0,0 +1,15 @@
import { Observable } from '@nativescript/core/data/observable';
import { ObservableArray } from '@nativescript/core/data/observable-array';
export class MultiTemplatesViewModel extends Observable {
private _items: ObservableArray<number>;
get items(): ObservableArray<number> {
this._items = new ObservableArray<number>();
for (let i = 0; i < 20; i++) {
this._items.push(i);
}
return this._items;
}
}

View File

@ -1,6 +1,6 @@
import { Label } from '../label';
import { LayoutBase } from '../layouts/layout-base';
import { View, CSSType, CustomLayoutView, Template } from '../core/view';
import { View, CSSType, CustomLayoutView, Template, KeyedTemplate } from '../core/view';
import { Property } from '../core/properties';
import { layout } from '../../utils';
import { StackLayout } from '../layouts/stack-layout';
@ -19,7 +19,12 @@ export interface ItemsSource {
*/
@CSSType('Repeater')
export class Repeater extends CustomLayoutView {
// TODO: get rid of such hacks.
public static knownFunctions = ['itemTemplateSelector']; // See component-builder.ts isKnownFunction
private _isDirty = false;
private _itemTemplateSelector: (item: any, index: number, items: any) => string;
private _itemTemplateSelectorBindable;
public ios;
public android;
@ -29,6 +34,15 @@ export class Repeater extends CustomLayoutView {
this.itemsLayout = new StackLayout();
}
@profile
public onLoaded() {
if (this._isDirty) {
this.refresh();
}
super.onLoaded();
}
/**
* Gets or set the items collection of the Repeater.
* The items property can be set to an array or an object defining length and getItem(index) method.
@ -38,18 +52,44 @@ export class Repeater extends CustomLayoutView {
* Gets or set the item template of the Repeater.
*/
public itemTemplate: string | Template;
/**
* Gets or set the item templates of the Repeater.
*/
public itemTemplates: string | Array<KeyedTemplate>;
/**
* Gets or set the items layout of the Repeater. Default value is StackLayout with orientation="vertical".
*/
public itemsLayout: LayoutBase;
@profile
public onLoaded() {
if (this._isDirty) {
this.refresh();
get itemTemplateSelector(): string | ((item: any, index: number, items: any) => string) {
return this._itemTemplateSelector;
}
set itemTemplateSelector(value: string | ((item: any, index: number, items: any) => string)) {
if (typeof value === 'string') {
if (!this._itemTemplateSelectorBindable) {
this._itemTemplateSelectorBindable = new Label();
}
super.onLoaded();
this._itemTemplateSelectorBindable.bind({
sourceProperty: null,
targetProperty: 'templateKey',
expression: value,
});
this._itemTemplateSelector = (item: any, index: number, items: any) => {
item['$index'] = index;
if (this._itemTemplateSelectorBindable.bindingContext === item) {
this._itemTemplateSelectorBindable.bindingContext = null;
}
this._itemTemplateSelectorBindable.bindingContext = item;
return this._itemTemplateSelectorBindable.get('templateKey');
};
} else if (typeof value === 'function') {
this._itemTemplateSelector = value;
}
}
public _requestRefresh() {
@ -73,8 +113,25 @@ export class Repeater extends CustomLayoutView {
const length = this.items.length;
for (let i = 0; i < length; i++) {
const viewToAdd = this.itemTemplate ? Builder.parse(this.itemTemplate, this) : this._getDefaultItemContent(i);
const dataItem = this._getDataItem(i);
let viewToAdd = null;
if (this._itemTemplateSelector && this.itemTemplates) {
const key = this._itemTemplateSelector(dataItem, i, this.items);
const length2 = this.itemTemplates.length;
for (let j = 0; j < length2; j++) {
const template = <KeyedTemplate>this.itemTemplates[j];
if (template.key === key) {
viewToAdd = template.createView();
break;
}
}
}
if (!viewToAdd) {
viewToAdd = this.itemTemplate ? Builder.parse(this.itemTemplate, this) : this._getDefaultItemContent(i);
}
viewToAdd.bindingContext = dataItem;
this.itemsLayout.addChild(viewToAdd);
}
@ -147,7 +204,7 @@ export class Repeater extends CustomLayoutView {
Repeater.prototype.recycleNativeView = 'auto';
/**
* Represents the item template property of each ListView instance.
* Represents the item template property of each Repeater instance.
*/
export const itemTemplateProperty = new Property<Repeater, string | Template>({
name: 'itemTemplate',
@ -159,7 +216,26 @@ export const itemTemplateProperty = new Property<Repeater, string | Template>({
itemTemplateProperty.register(Repeater);
/**
* Represents the property backing the items property of each ListView instance.
* Represents the items template property of each Repeater instance.
*/
export const itemTemplatesProperty = new Property<Repeater, string | Array<KeyedTemplate>>({
name: 'itemTemplates',
affectsLayout: true,
valueConverter: (value) => {
if (typeof value === 'string') {
return Builder.parseMultipleTemplates(value, null);
}
return value;
},
valueChanged: (target) => {
target._requestRefresh();
},
});
itemTemplatesProperty.register(Repeater);
/**
* Represents the property backing the items property of each Repeater instance.
*/
export const itemsProperty = new Property<Repeater, any[] | ItemsSource>({
name: 'items',

View File

@ -8,7 +8,7 @@
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 21
minSdkVersion 17
targetSdkVersion 29
generatedDensities = []
}

View File

@ -1772,6 +1772,10 @@ export class Repeater extends CustomLayoutView {
itemTemplate: string | Template;
itemTemplates: string | Array<KeyedTemplate>;
itemTemplateSelector: string | ((item: any, index: number, items: any) => string);
refresh();
}