FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(listview): correct item template resolution in sectioned ListView by VeinDevTtv · Pull Request #11184 · NativeScript/NativeScript · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .ts  (5) .xml  (2) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
63 changes: 63 additions & 0 deletions apps/automated/src/ui/list-view/list-view-tests.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,69 @@ export class ListViewTest extends UITest<ListView> {
}
}

// Sectioned ListView + multiple item templates tests (fix for #11133)
public test_SectionedListView_ItemTemplateSelector_CorrectTemplatePerSection() {
// Verifies that _getItemTemplateInSection resolves the template using the
// actual row data item (not the section wrapper object).
const listView = this.testView;
listView.sectioned = true;

// Section 0 items have age=0 (even → 'red'), section 1 items have age=1 (odd → 'green')
listView.items = [
{ title: 'Section A', items: [{ age: 0 }, { age: 2 }] },
{ title: 'Section B', items: [{ age: 1 }, { age: 3 }] },
];
listView.itemTemplates = this._itemTemplatesString;
listView.itemTemplateSelector = (item: any) => (item.age % 2 === 0 ? 'red' : 'green');

// Section 0, row 0: age=0 → 'red'
const template00 = listView._getItemTemplateInSection(0, 0);
TKUnit.assertEqual(template00.key, 'red', 'section 0 row 0 should use red template');

// Section 0, row 1: age=2 → 'red'
const template01 = listView._getItemTemplateInSection(0, 1);
TKUnit.assertEqual(template01.key, 'red', 'section 0 row 1 should use red template');

// Section 1, row 0: age=1 → 'green'
const template10 = listView._getItemTemplateInSection(1, 0);
TKUnit.assertEqual(template10.key, 'green', 'section 1 row 0 should use green template');

// Section 1, row 1: age=3 → 'green'
const template11 = listView._getItemTemplateInSection(1, 1);
TKUnit.assertEqual(template11.key, 'green', 'section 1 row 1 should use green template');
}

public test_SectionedListView_ItemTemplateSelector_DifferentTemplatesWithinSameSection() {
// Verifies mixed templates within a single section are resolved correctly.
const listView = this.testView;
listView.sectioned = true;

listView.items = [{ title: 'Mixed', items: [{ age: 0 }, { age: 1 }, { age: 2 }] }];
listView.itemTemplates = this._itemTemplatesString;
listView.itemTemplateSelector = (item: any) => (item.age % 2 === 0 ? 'red' : 'green');

TKUnit.assertEqual(listView._getItemTemplateInSection(0, 0).key, 'red', 'row 0 (age=0) → red');
TKUnit.assertEqual(listView._getItemTemplateInSection(0, 1).key, 'green', 'row 1 (age=1) → green');
TKUnit.assertEqual(listView._getItemTemplateInSection(0, 2).key, 'red', 'row 2 (age=2) → red');
}

public test_SectionedListView_ItemTemplateSelector_UnknownKeyFallsBackToDefault() {
// Mirrors test_ItemTemplateSelector_WhenWrongTemplateKeyIsSpecified_TheDefaultTemplateIsUsed
// but for the sectioned path.
const listView = this.testView;
listView.sectioned = true;

listView.items = [{ title: 'Section A', items: [{ age: 0 }] }];
listView.itemTemplate = "<Label text='default' />";
listView.itemTemplates = this._itemTemplatesString;
// Selector always returns a key that does not exist in _itemTemplatesInternal
listView.itemTemplateSelector = (_item: any) => 'nonexistent';

const template = listView._getItemTemplateInSection(0, 0);
// Should fall back to the first (default) template
TKUnit.assertEqual(template.key, 'default', 'unknown template key should fall back to default');
}

private _itemTemplatesString = `
<template key="red">
<Label text='red' style.backgroundColor='red' minHeight='100' maxHeight='100'/>
Expand Down
1 change: 1 addition & 0 deletions apps/toolbox/src/main-page.xml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Button text="labels" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="list-page" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="list-page-sticky" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="list-page-sticky-templates" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="multiple-scenes" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="root-layout" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
<Button text="scroll-view" tap="{{ viewDemo }}" class="btn btn-primary btn-view-demo" />
Expand Down
90 changes: 90 additions & 0 deletions apps/toolbox/src/pages/list-page-sticky-templates.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { EventData, Observable, Page } from '@nativescript/core';

interface CountryItem {
name: string;
code: string;
flag: string;
favorite?: boolean;
}
type CountryGroups = Array<{ title: string; items: CountryItem[] }>;

export class ListPageStickyTemplatesModel extends Observable {
// Section lengths intentionally vary (including single-item sections) so a
// flat-index template lookup cannot line up with the correct per-section result.
groups: CountryGroups = [
{
title: 'A',
items: [
{ name: 'Argentina', code: '(AR)', flag: '🇦🇷' },
{ name: 'Australia', code: '(AU)', flag: '🇦🇺', favorite: true },
{ name: 'Austria', code: '(AT)', flag: '🇦🇹' },
{ name: 'Azerbaijan', code: '(AZ)', flag: '🇦🇿' },
],
},
{
title: 'B',
items: [
{ name: 'Belgium', code: '(BE)', flag: '🇧🇪' },
{ name: 'Brazil', code: '(BR)', flag: '🇧🇷' },
],
},
{
title: 'C',
items: [
{ name: 'Canada', code: '(CA)', flag: '🇨🇦', favorite: true },
{ name: 'Chile', code: '(CL)', flag: '🇨🇱' },
{ name: 'Colombia', code: '(CO)', flag: '🇨🇴' },
{ name: 'Croatia', code: '(HR)', flag: '🇭🇷' },
{ name: 'Cyprus', code: '(CY)', flag: '🇨🇾' },
],
},
{
title: 'D',
items: [{ name: 'Denmark', code: '(DK)', flag: '🇩🇰' }],
},
{
title: 'E',
items: [{ name: 'Ecuador', code: '(EC)', flag: '🇪🇨', favorite: true }],
},
{
title: 'F',
items: [
{ name: 'Fiji', code: '(FJ)', flag: '🇫🇯' },
{ name: 'Finland', code: '(FI)', flag: '🇫🇮' },
{ name: 'France', code: '(FR)', flag: '🇫🇷', favorite: true },
],
},
{
title: 'G',
items: [
{ name: 'Germany', code: '(DE)', flag: '🇩🇪' },
{ name: 'Ghana', code: '(GH)', flag: '🇬🇭', favorite: true },
{ name: 'Greece', code: '(GR)', flag: '🇬🇷' },
{ name: 'Guyana', code: '(GY)', flag: '🇬🇾' },
],
},
];

selectItemTemplate(item: CountryItem, index: number, items: Array<CountryItem>) {
if (item?.favorite) {
return 'favorite';
}
return index === items.length - 1 ? 'last' : 'main';
}

onItemTap(args): void {
const group = this.groups[args.section];
const item = group?.items[args.index];
if (!item) {
console.log(`Tapped section ${args.section} row ${args.index}: no data item found!`);
return;
}
const expected = this.selectItemTemplate(item, args.index, group.items);
console.log(`Tapped section ${args.section} row ${args.index}: ${item.name} — expected template '${expected}'`);
}
}

export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = new ListPageStickyTemplatesModel();
}
37 changes: 37 additions & 0 deletions apps/toolbox/src/pages/list-page-sticky-templates.xml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" actionBarHidden="false" androidOverflowEdge="bottom">
<ActionBar title="Sectioned Templates">
</ActionBar>

<GridLayout rows="auto,*" backgroundColor="#efefef">
<Label text="Each row's badge must match its rule: ⭐ favorite → blue 'favorite' card, last row of a section → dark 'last' card, everything else → white 'main' card. A wrong badge anywhere means template selection is broken." textWrap="true" fontSize="12" color="#666" padding="10 12 10 12" />
<ListView row="1" class="list-group" items="{{ groups }}" itemTap="{{ onItemTap }}" separatorColor="#00000000" itemTemplateSelector="{{ selectItemTemplate }}" sectioned="true" stickyHeader="true" stickyHeaderTopPadding="false" stickyHeaderTemplate="<GridLayout><Label text='{{ title }}' fontSize='18' fontWeight='bold' color='#009bff' padding='8 0 8 12' borderBottomWidth='1' borderBottomColor='#ccc' borderTopWidth='1' borderTopColor='#ccc' backgroundColor='#fff' /></GridLayout>" stickyHeaderHeight="45">
<ListView.itemTemplates>
<template key="main">
<GridLayout columns="auto,auto,*,auto" padding="14 12 14 12" margin="2 6 2 6" borderRadius="10" backgroundColor="#fff" boxShadow="0px 1px 2px rgba(0,0,0,0.2)">
<Label text="{{ flag }}" marginLeft="4"></Label>
<Label col="1" text="{{ name }}" marginLeft="6" color="black"></Label>
<Label col="2" text="{{ code }}" marginLeft="4" color="#999"></Label>
<Label col="3" text="main" fontSize="11" color="#999"></Label>
</GridLayout>
</template>
<template key="favorite">
<GridLayout rows="auto,auto" columns="auto,auto,*,auto" padding="16 12 16 12" margin="2 6 2 6" borderRadius="10" backgroundColor="#dff0ff" borderWidth="1" borderColor="#009bff" boxShadow="0px 1px 2px rgba(0,0,0,0.2)">
<Label rowSpan="2" text="⭐" marginLeft="4" verticalAlignment="center"></Label>
<Label col="1" text="{{ name }}" marginLeft="6" color="#005b96" fontWeight="bold"></Label>
<Label row="1" col="1" text="{{ code }}" marginLeft="6" fontSize="12" color="#4a80a5"></Label>
<Label rowSpan="2" col="3" text="favorite" fontSize="11" color="#009bff" fontWeight="bold" verticalAlignment="center"></Label>
</GridLayout>
</template>
<template key="last">
<GridLayout columns="auto,auto,*,auto" padding="14 12 14 12" margin="2 6 10 6" borderRadius="10" backgroundColor="#37474f" boxShadow="0px 1px 2px rgba(0,0,0,0.3)">
<Label text="{{ flag }}" marginLeft="4"></Label>
<Label col="1" text="{{ name }}" marginLeft="6" color="#fff"></Label>
<Label col="2" text="{{ code }}" marginLeft="4" color="#b0bec5"></Label>
<Label col="3" text="last" fontSize="11" color="#ffd54f" fontWeight="bold"></Label>
</GridLayout>
</template>
</ListView.itemTemplates>
</ListView>
</GridLayout>

</Page>
8 changes: 4 additions & 4 deletions packages/core/ui/list-view/index.android.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -996,8 +996,8 @@ function ensureListViewAdapterClass() {
// Header view type is the last index (after all item template types)
return this.owner._itemTemplatesInternal.length;
} else {
// Get template for the actual item
const template = this.owner._getItemTemplate(positionInfo.itemIndex);
// Get template for the actual item using section-aware lookup
const template = this.owner._getItemTemplateInSection(positionInfo.section, positionInfo.itemIndex);
return this.owner._itemTemplatesInternal.indexOf(template);
}
} else {
Expand Down Expand Up @@ -1124,8 +1124,8 @@ function ensureListViewAdapterClass() {
}

private _createItemView(section: number, itemIndex: number, convertView: android.view.View, parent: android.view.ViewGroup): android.view.View {
// Use existing item creation logic but with sectioned data
const template = this.owner._getItemTemplate(itemIndex);
// Use section-aware template lookup when in sectioned mode, flat lookup otherwise
const template = section >= 0 && this.owner.sectioned ? this.owner._getItemTemplateInSection(section, itemIndex) : this.owner._getItemTemplate(itemIndex);
let view: View;

// convertView is of the wrong type
Expand Down
40 changes: 20 additions & 20 deletions packages/core/ui/list-view/index.ios.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const infinity = layout.makeMeasureSpec(0, layout.UNSPECIFIED);

interface ViewItemIndex {
_listViewItemIndex?: number;
_listViewSectionIndex?: number;
}

type ItemView = View & ViewItemIndex;
Expand Down Expand Up @@ -162,7 +163,7 @@ class DataSource extends NSObject implements UITableViewDataSource {
const owner = this._owner?.deref();
let cell: ListViewCell;
if (owner) {
const template = owner._getItemTemplate(indexPath.row);
const template = owner.sectioned ? owner._getItemTemplateInSection(indexPath.section, indexPath.row) : owner._getItemTemplate(indexPath.row);
cell = <ListViewCell>(tableView.dequeueReusableCellWithIdentifier(template.key) || ListViewCell.initWithEmptyBackground());
owner._prepareCell(cell, indexPath);

Expand All @@ -172,7 +173,7 @@ class DataSource extends NSObject implements UITableViewDataSource {
// from 'tableViewHeightForRowAtIndexPath' method too (in iOS 7.1) and we don't want to arrange the fake cell.
const width = layout.getMeasureSpecSize(owner.widthMeasureSpec);
const rowHeight = owner._effectiveRowHeight;
const cellHeight = rowHeight > 0 ? rowHeight : owner.getHeight(indexPath.row);
const cellHeight = rowHeight > 0 ? rowHeight : owner.getHeight(indexPath.row, indexPath.section);
cellView.iosOverflowSafeAreaEnabled = false;
View.layoutChild(owner, cellView, 0, 0, width, cellHeight);
}
Expand Down Expand Up @@ -232,10 +233,10 @@ class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate {
return tableView.estimatedRowHeight;
}

let height = owner.getHeight(indexPath.row);
let height = owner.getHeight(indexPath.row, indexPath.section);
if (height === undefined) {
// in iOS8+ after call to scrollToRowAtIndexPath:atScrollPosition:animated: this method is called before tableViewCellForRowAtIndexPath so we need fake cell to measure its content.
const template = owner._getItemTemplate(indexPath.row);
const template = owner.sectioned ? owner._getItemTemplateInSection(indexPath.section, indexPath.row) : owner._getItemTemplate(indexPath.row);
let cell = this._measureCellMap.get(template.key);
if (!cell) {
cell = <any>tableView.dequeueReusableCellWithIdentifier(template.key) || ListViewCell.initWithEmptyBackground();
Expand Down Expand Up @@ -437,7 +438,8 @@ export class ListView extends ListViewBase {
// tslint:disable-next-line
private _dataSource;
private _delegate;
private _heights: Array<number>;
// Measured row heights indexed [section][row]; non-sectioned lists use section 0.
private _heights: Array<Array<number>>;
private _preparingCell: boolean;
private _isDataDirty: boolean;
private _map: Map<ListViewCell, ItemView>;
Expand All @@ -453,7 +455,7 @@ export class ListView extends ListViewBase {
super();
this._map = new Map<ListViewCell, ItemView>();
this._headerMap = new Map<ListViewHeaderCell, View>();
this._heights = new Array<number>();
this._heights = new Array<Array<number>>();
}

createNativeView() {
Expand Down Expand Up @@ -727,12 +729,15 @@ export class ListView extends ListViewBase {
return indexes.some((visIndex) => visIndex.row === itemIndex);
}

public getHeight(index: number): number {
return this._heights[index];
public getHeight(index: number, section = 0): number {
return this._heights[section]?.[index];
}

public setHeight(index: number, value: number): void {
this._heights[index] = value;
public setHeight(index: number, value: number, section = 0): void {
if (!this._heights[section]) {
this._heights[section] = new Array<number>();
}
this._heights[section][index] = value;
}

public _onRowHeightPropertyChanged(oldValue: CoreTypes.LengthType, newValue: CoreTypes.LengthType) {
Expand Down Expand Up @@ -790,7 +795,7 @@ export class ListView extends ListViewBase {

this._map.forEach((childView, listViewCell) => {
const rowHeight = this._effectiveRowHeight;
const cellHeight = rowHeight > 0 ? rowHeight : this.getHeight(childView._listViewItemIndex);
const cellHeight = rowHeight > 0 ? rowHeight : this.getHeight(childView._listViewItemIndex, childView._listViewSectionIndex ?? 0);
if (cellHeight) {
const width = layout.getMeasureSpecSize(this.widthMeasureSpec);
childView.iosOverflowSafeAreaEnabled = false;
Expand All @@ -811,7 +816,7 @@ export class ListView extends ListViewBase {
const heightMeasureSpec: number = rowHeight >= 0 ? layout.makeMeasureSpec(rowHeight, layout.EXACTLY) : infinity;
const measuredSize = View.measureChild(this, cellView, this.widthMeasureSpec, heightMeasureSpec);
const height = measuredSize.measuredHeight;
this.setHeight(indexPath.row, height);
this.setHeight(indexPath.row, height, indexPath.section);

return height;
}
Expand All @@ -826,13 +831,7 @@ export class ListView extends ListViewBase {
let view: ItemView = cell.view;
if (!view) {
if (this.sectioned) {
// For sectioned data, we need to calculate the absolute index for template selection
let absoluteIndex = 0;
for (let i = 0; i < indexPath.section; i++) {
absoluteIndex += this._getItemsInSection(i).length;
}
absoluteIndex += indexPath.row;
view = this._getItemTemplate(absoluteIndex).createView();
view = this._getItemTemplateInSection(indexPath.section, indexPath.row).createView();
} else {
view = this._getItemTemplate(indexPath.row).createView();
}
Expand Down Expand Up @@ -862,7 +861,7 @@ export class ListView extends ListViewBase {
if (this.sectioned) {
this._prepareItemInSection(view, indexPath.section, indexPath.row);
view._listViewItemIndex = indexPath.row; // Keep row index for compatibility
(view as any)._listViewSectionIndex = indexPath.section;
view._listViewSectionIndex = indexPath.section;
} else {
this._prepareItem(view, indexPath.row);
view._listViewItemIndex = indexPath.row;
Expand Down Expand Up @@ -895,6 +894,7 @@ export class ListView extends ListViewBase {
this._preparingCell = true;
view.parent._removeView(view);
view._listViewItemIndex = undefined;
view._listViewSectionIndex = undefined;
this._preparingCell = preparing;
this._map.delete(cell);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/core/ui/list-view/list-view-common.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,24 @@ export abstract class ListViewBase extends ContainerView implements ListViewDefi
return this._itemTemplatesInternal[0];
}

public _getItemTemplateInSection(section: number, index: number): KeyedTemplate {
let templateKey = 'default';
if (this.itemTemplateSelector) {
const dataItem = this._getDataItemInSection(section, index);
const sectionItems = this._getItemsInSection(section);
templateKey = this._itemTemplateSelector(dataItem, index, sectionItems);
}

for (let i = 0, length = this._itemTemplatesInternal.length; i < length; i++) {
if (this._itemTemplatesInternal[i].key === templateKey) {
return this._itemTemplatesInternal[i];
}
}

// This is the default template
return this._itemTemplatesInternal[0];
}

public _prepareItem(item: View, index: number) {
if (item) {
item.bindingContext = this._getDataItem(index);
Expand Down
Loading

Back | FazBrowse Home | New Git URL