mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-14 18:12:09 +08:00
feat(core): Repeater multiple item templates implementation (#8981)
This commit is contained in:

committed by
GitHub

parent
0bbdeaf0b5
commit
b113f1916d
@ -2,10 +2,10 @@
|
||||
"main": "main.js",
|
||||
"description": "NativeScript Application",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/NativeScript/NativeScript.git"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/NativeScript/NativeScript.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "npx rimraf hooks node_modules platforms package-lock.json && npm i"
|
||||
},
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
```
|
||||
|
@ -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');
|
||||
|
17
apps/ui/src/repeater/main-page.ts
Normal file
17
apps/ui/src/repeater/main-page.ts
Normal 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;
|
||||
}
|
6
apps/ui/src/repeater/main-page.xml
Normal file
6
apps/ui/src/repeater/main-page.xml
Normal 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>
|
13
apps/ui/src/repeater/multi-templates-page.ts
Normal file
13
apps/ui/src/repeater/multi-templates-page.ts
Normal 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' : '';
|
||||
}
|
18
apps/ui/src/repeater/multi-templates-page.xml
Normal file
18
apps/ui/src/repeater/multi-templates-page.xml
Normal 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>
|
15
apps/ui/src/repeater/multi-templates-view-model.ts
Normal file
15
apps/ui/src/repeater/multi-templates-view-model.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -8,7 +8,7 @@
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
minSdkVersion 17
|
||||
targetSdkVersion 29
|
||||
generatedDensities = []
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user