Skip to content

Commit 688bdf8

Browse files
authored
Implement is last item (#256)
* Implement is last * Updated readme file * Bump version
1 parent 7b5650e commit 688bdf8

File tree

5 files changed

+102
-62
lines changed

5 files changed

+102
-62
lines changed

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const data = [{title: 'Node 1', items: [{title: 'Node 1.1'}, {title: 'Node 1.2'}
3737
data={data}
3838
getChildrenName={(node) => 'items'}
3939
onNodePressed={(node) => alert('Selected node')}
40-
renderNode={(node, level) => (
40+
renderNode={(node, level, isLastLevel) => (
4141
<NestedRow
4242
level={level}
4343
style={styles.row}
@@ -52,14 +52,14 @@ const data = [{title: 'Node 1', items: [{title: 'Node 1.1'}, {title: 'Node 1.2'}
5252

5353
### NestedListView
5454

55-
| Prop | Description | Type | Default |
56-
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------ |
57-
| **`data`** | Array of nested items | Array | **Required** |
58-
| **`renderNode`** | Takes a node from data and renders it into the NestedlistView. The function receives `{node, level}` (see [Usage](#usage)) and must return a React element. | Function | **Required** |
59-
| **`getChildrenName`** | Function to determine in a node where are the children, by default NestedListView will try to find them in **items** | Function | **items** |
60-
| **`onNodePressed`** | Function called when a node is pressed by a user | Function | Not required |
61-
| **`extraData`** | A marker property for telling the list to re-render | Boolean | Not required |
62-
| **`keepOpenedState`** | Prop for keeping the opened state of each node when data passed to the list changes | Boolean | Not required |
55+
| Prop | Description | Type | Default |
56+
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------ |
57+
| **`data`** | Array of nested items | Array | **Required** |
58+
| **`renderNode`** | Takes a node from data and renders it into the NestedlistView. The function receives `{node, level, isLastLevel}` (see [Usage](#usage)) and must return a React element. | Function | **Required** |
59+
| **`getChildrenName`** | Function to determine in a node where are the children, by default NestedListView will try to find them in **items** | Function | **items** |
60+
| **`onNodePressed`** | Function called when a node is pressed by a user | Function | Not required |
61+
| **`extraData`** | A marker property for telling the list to re-render | Boolean | Not required |
62+
| **`keepOpenedState`** | Prop for keeping the opened state of each node when data passed to the list changes | Boolean | Not required |
6363

6464
### NestedRow
6565

@@ -74,12 +74,12 @@ const data = [{title: 'Node 1', items: [{title: 'Node 1.1'}, {title: 'Node 1.2'}
7474

7575
You can find examples [here](https://github.com/fjmorant/react-native-nested-listview-examples).
7676

77-
| Version App | React Native | Library |
78-
| ------------ | ------------ | ------- |
79-
| 1.0.0 | 0.63.2 | 0.9.2 |
77+
| Version App | React Native | Library |
78+
| ----------- | ------------ | ------- |
79+
| 1.0.0 | 0.63.2 | 0.9.2 |
8080

8181
## Roadmap
8282

83-
- Autoscrolling optionally
84-
- Expand/contract nodes programatically
85-
- Support animations
83+
- Autoscrolling optionally
84+
- Expand/contract nodes programatically
85+
- Support animations

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-nested-listview",
3-
"version": "0.9.2",
3+
"version": "0.10.0",
44
"description": "Nested Listview for React native",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

src/NestedListView.test.tsx

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
2-
import { Text, View } from 'react-native';
3-
import NestedListView, { NestedRow } from '.';
2+
import { Pressable, Text, View } from 'react-native';
3+
import NestedListView, { INode, NestedRow } from '.';
44
import { render, waitFor, fireEvent } from '@testing-library/react-native';
55

66
let mockCounter = 0;
@@ -126,13 +126,13 @@ describe('NestedListView', () => {
126126

127127
const { queryByText } = render(
128128
<NestedListView
129-
getChildrenName={(node: any) => {
129+
getChildrenName={(node: INode) => {
130130
if (node.title === 'child2') {
131131
return 'descendants';
132132
}
133133
return 'items';
134134
}}
135-
renderNode={(node: any) => (
135+
renderNode={(node: INode) => (
136136
<View>
137137
<Text>{node.title}</Text>
138138
</View>
@@ -168,13 +168,13 @@ describe('NestedListView', () => {
168168

169169
const { queryByText } = render(
170170
<NestedListView
171-
getChildrenName={(node: any) => {
171+
getChildrenName={(node: INode) => {
172172
if (node.title === 'child2') {
173173
return 'children';
174174
}
175175
return 'items';
176176
}}
177-
renderNode={(node: any) => (
177+
renderNode={(node: INode) => (
178178
<View>
179179
<Text>{node.title}</Text>
180180
</View>
@@ -237,7 +237,7 @@ describe('NestedListView', () => {
237237
const { queryByText } = render(
238238
<NestedListView
239239
getChildrenName={() => 'children'}
240-
renderNode={(node: any) => (
240+
renderNode={(node: INode) => (
241241
<View>
242242
<Text>{node.name}</Text>
243243
</View>
@@ -277,7 +277,7 @@ describe('NestedListView', () => {
277277
const { queryByText } = render(
278278
<NestedListView
279279
onNodePressed={mockOnNodePressed}
280-
renderNode={(node: any) => (
280+
renderNode={(node: INode) => (
281281
<View>
282282
<Text>{node.title}</Text>
283283
</View>
@@ -308,7 +308,7 @@ describe('NestedListView', () => {
308308
const { UNSAFE_queryAllByType } = render(
309309
<NestedListView
310310
onNodePressed={mockOnNodePressed}
311-
renderNode={(node: any, level?: number) => (
311+
renderNode={(node: INode, level?: number) => (
312312
<NestedRow level={level}>{node.name}</NestedRow>
313313
)}
314314
data={data}
@@ -350,4 +350,36 @@ describe('NestedListView', () => {
350350
const component = queryByText('prop data has not been passed');
351351
expect(component).toBeDefined();
352352
});
353+
354+
test('renders with isLast renderNode', async () => {
355+
const data = [
356+
{
357+
title: 'child1',
358+
items: [{ title: 'subchild 1.1' }, { title: 'subchild 1.2' }],
359+
},
360+
];
361+
362+
const mockIsTheLast = jest.fn();
363+
364+
const { UNSAFE_getByType } = render(
365+
<NestedListView
366+
renderNode={(item: INode, level: number, isLastItem: boolean) => {
367+
mockIsTheLast(isLastItem);
368+
369+
return <NestedRow level={level}>{item.title}</NestedRow>;
370+
}}
371+
data={data}
372+
/>,
373+
);
374+
375+
await waitFor(() => {
376+
const child1 = UNSAFE_getByType(Pressable);
377+
378+
if (child1) {
379+
fireEvent.press(child1);
380+
}
381+
382+
expect(mockIsTheLast).toHaveBeenLastCalledWith(true);
383+
});
384+
});
353385
});

src/NestedListView.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import hashObjectGenerator from 'object-hash';
2-
import React, { useCallback, useEffect, useState } from 'react';
2+
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
33
import isEqual from 'react-fast-compare';
44
import { StyleSheet, Text, View } from 'react-native';
55
import shortid from 'shortid';
@@ -24,9 +24,13 @@ const styles = StyleSheet.create({
2424
export interface IProps {
2525
data: any;
2626
extraData?: any;
27-
renderNode: (elem: INode, level?: number) => any;
28-
onNodePressed?: (node?: INode) => void;
29-
getChildrenName?: (elem: any) => any;
27+
renderNode: (
28+
item: INode,
29+
level: number,
30+
isLastLevel: boolean,
31+
) => ReactElement;
32+
onNodePressed?: (item: INode) => void;
33+
getChildrenName?: (item: INode) => string;
3034
style?: StyleSheet;
3135
keepOpenedState?: boolean;
3236
}
@@ -35,6 +39,14 @@ export interface IState {
3539
root: INode;
3640
}
3741

42+
const defaultRootNode = {
43+
_internalId: 'root',
44+
items: [],
45+
name: 'root',
46+
opened: true,
47+
hidden: true,
48+
} as INode;
49+
3850
const NestedListView = React.memo(
3951
({
4052
getChildrenName,
@@ -100,12 +112,7 @@ const NestedListView = React.memo(
100112
);
101113

102114
const [_root, setRoot]: [INode, (_root: INode) => void] = useState(
103-
generateRootNode({
104-
getChildrenName,
105-
renderNode,
106-
data,
107-
onNodePressed,
108-
}),
115+
defaultRootNode,
109116
);
110117

111118
useEffect(() => {
@@ -156,7 +163,6 @@ const NestedListView = React.memo(
156163
getChildrenName={_getChildrenName}
157164
node={_root}
158165
onNodePressed={onNodePressed}
159-
generateIds={generateIds}
160166
level={0}
161167
renderNode={renderNode}
162168
extraData={extraData}

src/NodeView.tsx

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React, { useEffect, useState } from 'react';
1+
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
22
import isEqual from 'react-fast-compare';
3-
import { FlatList, TouchableWithoutFeedback, View } from 'react-native';
3+
import { FlatList, Pressable } from 'react-native';
44
import globalHook from 'use-global-hook';
55

66
export interface INode {
@@ -11,14 +11,16 @@ export interface INode {
1111
}
1212

1313
export interface IProps {
14-
generateIds?: (node?: INode) => any;
15-
getChildren?: () => any;
16-
getChildrenName: (item: INode) => any;
14+
getChildrenName: (item: INode) => string;
1715
node: INode;
1816
level: number;
19-
onNodePressed?: (item: any) => any;
20-
renderNode: (item: any, level: number) => any;
21-
renderChildrenNode?: (item: any) => any;
17+
onNodePressed?: (item: INode) => void;
18+
renderNode: (
19+
item: INode,
20+
level: number,
21+
isLastLevel: boolean,
22+
) => ReactElement;
23+
renderChildrenNode?: (item: INode) => ReactElement;
2224
extraData?: any;
2325
keepOpenedState?: boolean;
2426
}
@@ -57,13 +59,12 @@ const NodeView = React.memo(
5759
}: IProps) => {
5860
const [globalState, globalActions]: [any, any] = useGlobal();
5961

60-
// tslint:disable-next-line:variable-name
6162
const [_node, setNode]: [INode, any] = useState({
6263
...node,
6364
opened:
6465
keepOpenedState && globalState.nodesState[node._internalId]
65-
? globalState.nodesState[node._internalId]
66-
: node.opened,
66+
? !!globalState.nodesState[node._internalId]
67+
: !!node.opened,
6768
});
6869

6970
useEffect(() => {
@@ -73,7 +74,6 @@ const NodeView = React.memo(
7374
});
7475
}, [node, _node.opened]);
7576

76-
// tslint:disable-next-line:variable-name
7777
const _onNodePressed = () => {
7878
if (keepOpenedState) {
7979
globalActions.setOpenedNode({
@@ -92,9 +92,8 @@ const NodeView = React.memo(
9292
}
9393
};
9494

95-
// tslint:disable-next-line:variable-name
96-
const renderChildren = (item: INode, _level: number): any => {
97-
return (
95+
const renderChildren = useCallback(
96+
(item: INode, _level: number): ReactElement => (
9897
<NodeView
9998
getChildrenName={getChildrenName}
10099
node={item}
@@ -104,14 +103,17 @@ const NodeView = React.memo(
104103
renderNode={renderNode}
105104
keepOpenedState={keepOpenedState}
106105
/>
107-
);
108-
};
106+
),
107+
[extraData, getChildrenName, onNodePressed, renderNode, keepOpenedState],
108+
);
109109

110-
const renderItem = ({ item }: { item: INode }) =>
111-
renderChildren(item, level);
110+
const renderItem = useCallback(
111+
({ item }: { item: INode }) => renderChildren(item, level),
112+
[renderChildren, level],
113+
);
112114

113-
const rootChildrenName = getChildrenName(_node);
114-
const rootChildren = _node[rootChildrenName];
115+
const nodeChildrenName = getChildrenName(_node);
116+
const nodeChildren: [] = _node[nodeChildrenName];
115117

116118
const isNodeOpened =
117119
(keepOpenedState && globalState.nodesState[node._internalId]) ||
@@ -120,13 +122,13 @@ const NodeView = React.memo(
120122
return (
121123
<>
122124
{!_node.hidden ? (
123-
<TouchableWithoutFeedback onPress={_onNodePressed}>
124-
<View>{renderNode(_node, level)}</View>
125-
</TouchableWithoutFeedback>
125+
<Pressable onPress={_onNodePressed}>
126+
{renderNode(_node, level, !nodeChildren || !nodeChildren.length)}
127+
</Pressable>
126128
) : null}
127-
{isNodeOpened && rootChildren ? (
129+
{isNodeOpened && nodeChildren ? (
128130
<FlatList
129-
data={rootChildren}
131+
data={nodeChildren}
130132
renderItem={renderItem}
131133
extraData={extraData}
132134
keyExtractor={(item: INode) => item._internalId}

0 commit comments

Comments
 (0)