├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── example
├── .expo-shared
│ └── assets.json
├── .gitignore
├── .watchmanconfig
├── App.js
├── README.md
├── app.json
├── assets
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── package.json
├── rn-cli.config.js
├── screenshots
│ ├── listview-cropped.png
│ └── listview.png
├── src
│ ├── GenericListExample.js
│ ├── ImmutableListViewExample.js
│ ├── ImmutableVirtualizedListExample.js
│ ├── mockData.js
│ ├── styles.js
│ └── utils.js
└── yarn.lock
├── images
└── cover.png
├── index.d.ts
├── package.json
├── scripts
└── setup-jest.js
├── src
├── ImmutableListView
│ ├── EmptyListView.js
│ ├── ImmutableListView.js
│ ├── __tests__
│ │ ├── EmptyListView.test.js
│ │ ├── ImmutableListView.test.js
│ │ └── __snapshots__
│ │ │ ├── EmptyListView.test.js.snap
│ │ │ └── ImmutableListView.test.js.snap
│ └── index.js
├── ImmutableVirtualizedList
│ ├── EmptyVirtualizedList.js
│ ├── ImmutableVirtualizedList.js
│ ├── __tests__
│ │ ├── EmptyVirtualizedList.test.js
│ │ ├── ImmutableVirtualizedList.test.js
│ │ └── __snapshots__
│ │ │ ├── EmptyVirtualizedList.test.js.snap
│ │ │ └── ImmutableVirtualizedList.test.js.snap
│ └── index.js
├── __tests__
│ ├── comparison.test.js
│ └── utils.test.js
├── index.js
├── styles.js
├── test-utils.js
└── utils.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "module:metro-react-native-babel-preset"
4 | ],
5 | "plugins": [
6 | "@babel/plugin-proposal-class-properties"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | android
4 | ios
5 |
6 | lib
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "cooperka/react-native",
3 |
4 | "parser": "babel-eslint",
5 |
6 | "env": {
7 | "jest": true
8 | },
9 |
10 | "rules": {
11 | "react-native/no-color-literals": "off"
12 | },
13 |
14 | "globals": {
15 | "__DEV__": true
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib
2 |
3 | node_modules
4 | .idea
5 |
6 | npm-debug.log
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | **/__tests__/
2 | /scripts/
3 | src/test-utils.js
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "10"
5 | - "12"
6 |
7 | cache:
8 | yarn: true
9 | directories:
10 | - node_modules
11 | - jest-cache
12 |
13 | script:
14 | - yarn run lint
15 | - yarn test -- --cacheDirectory="jest-cache"
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Kevin Cooper
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Immutable ListView
2 |
3 | [](https://travis-ci.org/cooperka/react-native-immutable-list-view)
4 | [](https://www.npmjs.com/package/react-native-immutable-list-view)
5 | [](https://www.npmjs.com/package/react-native-immutable-list-view)
6 | [](https://github.com/cooperka/react-native-immutable-list-view)
7 |
8 |
9 |
10 | [](#README)
11 |
12 |
13 |
14 | Drop-in replacement for React Native's [`ListView`](https://facebook.github.io/react-native/docs/listview.html),
15 | [`FlatList`](https://facebook.github.io/react-native/docs/flatlist.html),
16 | and [`VirtualizedList`](https://facebook.github.io/react-native/docs/virtualizedlist.html).
17 |
18 | 
19 |
20 | It supports [Immutable.js](https://facebook.github.io/immutable-js/) to give you faster performance and less headaches.
21 |
22 | ## Motivation
23 |
24 | - Do you use Immutable data, only to write the same boilerplate over and over in order to display it?
25 | - Do you want to show 'Loading...', 'No results', and 'Error!' states in your lists?
26 | - Do you have nested objects in your state so a shallow diff won't cut it for pure rendering?
27 | - Do you want better performance while animating screen transitions?
28 |
29 | If you answered yes to ANY of these questions, this project can help. Check out the examples below.
30 |
31 | ## How it works
32 |
33 | For FlatList and VirtualizedList:
34 |
35 | ```jsx
36 |
40 | ```
41 |
42 | For ListView (deprecated as of React Native v0.59):
43 |
44 | ```jsx
45 |
49 | ```
50 |
51 | The screenshot above shows two different lists. The first uses this data:
52 |
53 | ```js
54 | Immutable.fromJS({
55 | 'Section A': [
56 | 'foo',
57 | 'bar',
58 | ],
59 | 'Section B': [
60 | 'fizz',
61 | 'buzz',
62 | ],
63 | })
64 | ```
65 |
66 | The second list is even simpler:
67 |
68 | ```js
69 | Immutable.Range(1, 100)
70 | ```
71 |
72 | There's an example app [here](https://github.com/cooperka/react-native-immutable-list-view/tree/master/example)
73 | if you'd like to see it in action.
74 |
75 | ## Installation
76 |
77 | 1. Install:
78 | - Using [npm](https://www.npmjs.com/#getting-started): `npm install react-native-immutable-list-view --save`
79 | - Using [Yarn](https://yarnpkg.com/): `yarn add react-native-immutable-list-view`
80 |
81 | 2. Import it in your JS:
82 |
83 | For FlatList and VirtualizedList:
84 |
85 | ```js
86 | import { ImmutableVirtualizedList } from 'react-native-immutable-list-view';
87 | ```
88 |
89 | For ListView:
90 |
91 | ```js
92 | import { ImmutableListView } from 'react-native-immutable-list-view/lib/ImmutableListView';
93 | ```
94 |
95 | ## Example usage -- replacing FlatList
96 |
97 | Goodbye, `keyExtractor` boilerplate!
98 |
99 | > Note: This example diff looks much better on [GitHub](https://github.com/cooperka/react-native-immutable-list-view#example-usage----replacing-flatlist) than on npm's site.
100 | > Red means delete, green means add.
101 |
102 | ```diff
103 | -import { Text, View, FlatList } from 'react-native';
104 | +import { Text, View } from 'react-native';
105 | +import { ImmutableVirtualizedList } from 'react-native-immutable-list-view';
106 |
107 | import style from './styles';
108 | import listData from './listData';
109 |
110 | class App extends Component {
111 |
112 | renderItem({ item, index }) {
113 | return {item};
114 | }
115 |
116 | render() {
117 | return (
118 |
119 |
120 | Welcome to React Native!
121 |
122 | - items.get(index)}
125 | - getItemCount={(items) => items.size}
126 | - keyExtractor={(item, index) => String(index)}
127 | +
131 |
132 | );
133 | }
134 |
135 | }
136 | ```
137 |
138 | ## Example usage -- replacing ListView
139 |
140 | You can remove all that boilerplate in your constructor, as well as lifecycle methods like
141 | `componentWillReceiveProps` if all they're doing is updating your `dataSource`.
142 | `ImmutableListView` will handle all of this for you.
143 |
144 | > Note: This example diff looks much better on [GitHub](https://github.com/cooperka/react-native-immutable-list-view#example-usage----replacing-listview) than on npm's site.
145 | > Red means delete, green means add.
146 |
147 | ```diff
148 | -import { Text, View, ListView } from 'react-native';
149 | +import { Text, View } from 'react-native';
150 | +import { ImmutableListView } from 'react-native-immutable-list-view/lib/ImmutableListView';
151 |
152 | import style from './styles';
153 | import listData from './listData';
154 |
155 | class App extends Component {
156 |
157 | - constructor(props) {
158 | - super(props);
159 | -
160 | - const dataSource = new ListView.DataSource({
161 | - rowHasChanged: (r1, r2) => r1 !== r2,
162 | - sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
163 | - });
164 | -
165 | - const mutableData = listData.toJS();
166 | -
167 | - this.state = {
168 | - dataSource: dataSource.cloneWithRowsAndSections(mutableData),
169 | - };
170 | - }
171 | -
172 | - componentWillReceiveProps(newProps) {
173 | - this.setState({
174 | - dataSource: this.state.dataSource.cloneWithRows(newProps.listData),
175 | - });
176 | - }
177 | -
178 | renderRow(rowData) {
179 | return {rowData};
180 | }
181 |
182 | renderSectionHeader(sectionData, category) {
183 | return {category};
184 | }
185 |
186 | render() {
187 | return (
188 |
189 |
190 | Welcome to React Native!
191 |
192 | -
199 |
200 | );
201 | }
202 |
203 | }
204 | ```
205 |
206 | ## Customization
207 |
208 | All the props supported by React Native's underlying List are simply passed through, and should work exactly the same.
209 | You can see all the [VirtualizedList props](https://facebook.github.io/react-native/docs/virtualizedlist.html#props)
210 | or [ListView props](https://facebook.github.io/react-native/docs/listview.html#props) on React Native's website.
211 |
212 | You can customize the look of your list by implementing [`renderItem`](https://facebook.github.io/react-native/docs/flatlist.html#renderitem) for FlatList and VirtualizedList
213 | or [`renderRow`](https://facebook.github.io/react-native/docs/listview.html#renderrow) for ListView.
214 |
215 | Here are the additional props that `ImmutableVirtualizedList` and `ImmutableListView` accept:
216 |
217 | | Prop name | Data type | Default value? | Description |
218 | |-----------|-----------|----------------|-------------|
219 | | `immutableData` | Any [`Immutable.Iterable`](https://facebook.github.io/immutable-js/docs/#/Iterable/isIterable) | Required. | The data to render. See below for some examples. |
220 | | `rowsDuringInteraction` | `number` | `undefined` | How many rows of data to initially display while waiting for interactions to finish (e.g. Navigation animations). |
221 | | `sectionHeaderHasChanged` | `func` | `(prevSectionData, nextSectionData) => false` | Only needed if your section header is dependent on your row data (uncommon; see [`ListViewDataSource`'s constructor](https://facebook.github.io/react-native/docs/listviewdatasource.html#constructor) for details). |
222 | | `renderEmpty` | `string` or `func` | `undefined` | If your data is empty (e.g. `null`, `[]`, `{}`) and this prop is defined, then this will be rendered instead. Pull-refresh and scrolling functionality will be **lost**. Most of the time you should use `renderEmptyInList` instead. |
223 | | `renderEmptyInList` | `string` or `func` | `'No data.'` | If your data is empty (e.g. `null`, `[]`, `{}`) and this prop is defined, then this will be rendered instead. Pull-refresh and scrolling functionality will be **kept**! See [below](#loading--empty--error-states) for more details. |
224 |
225 | Also see [React Native's `FlatListExample`](https://github.com/facebook/react-native/blob/master/RNTester/js/FlatListExample.js)
226 | for more inspiration.
227 |
228 | ## Methods
229 |
230 | Methods such as `scrollToEnd` are passed through just like the props described above.
231 | You can read about them [here](https://facebook.github.io/react-native/docs/listview.html#methods) for ListView
232 | or [here](https://facebook.github.io/react-native/docs/virtualizedlist.html#methods) for FlatList and VirtualizedList.
233 |
234 | The references to the raw `VirtualizedList` or `ListView` component are available via `getVirtualizedList()` or `getListView()`.
235 | These references allow you to access any other methods on the underlying List that you might need.
236 |
237 | ## How to format your data
238 |
239 | `ImmutableListView` accepts several [standard formats](https://facebook.github.io/react-native/releases/0.37/docs/listviewdatasource.html#constructor)
240 | for list data. Here are some examples:
241 |
242 | #### List
243 |
244 | ```js
245 | [rowData1, rowData2, ...]
246 | ```
247 |
248 | #### Map of Lists
249 |
250 | ```js
251 | {
252 | section1: [
253 | rowData1,
254 | rowData2,
255 | ...
256 | ],
257 | ...
258 | }
259 | ```
260 |
261 | #### Map of Maps
262 |
263 | ```js
264 | {
265 | section1: {
266 | rowId1: rowData1,
267 | rowId2: rowData2,
268 | ...
269 | },
270 | ...
271 | }
272 | ```
273 |
274 | To try it out yourself, you can use the [example app](https://github.com/cooperka/react-native-immutable-list-view/tree/master/example)!
275 |
276 | Support is coming soon for section headers with `ImmutableVirtualizedList` too, similar to [`SectionList`](https://facebook.github.io/react-native/docs/sectionlist.html).
277 | See [PR #34](https://github.com/cooperka/react-native-immutable-list-view/pull/34).
278 |
279 | ## Loading / Empty / Error states
280 |
281 | The optional `renderEmptyInList` prop takes a string and renders an Immutable List displaying the text you specified.
282 | By default, this text is simply `No data.`, but you can customize this based on your state. For example:
283 |
284 | ```jsx
285 | render() {
286 | const emptyText = this.state.isLoading
287 | ? "Loading..."
288 | : this.state.errorMsg
289 | ? "Error!"
290 | : "No data.";
291 |
292 | return (
293 |
298 | );
299 | }
300 | ```
301 |
302 | The empty list will receive all the same props as your normal list, so things like pull-to-refresh will still work.
303 |
--------------------------------------------------------------------------------
/example/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true,
3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true
4 | }
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | npm-debug.*
4 | yarn-error.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 | web-report/
13 | .idea/*
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/example/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/example/App.js:
--------------------------------------------------------------------------------
1 | // Choose one:
2 | // import Example from './src/ImmutableListViewExample';
3 | import Example from './src/ImmutableVirtualizedListExample';
4 |
5 | export default Example;
6 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # ImmutableListView Examples
2 |
3 | ## VirtualizedList
4 |
5 | By default, the example app uses an `ImmutableListView`. If you want to try out the new `ImmutableVirtualizedList`,
6 | you can simply uncomment the line in `index.android.js` or `index.ios.js` and it will use that component instead.
7 |
8 | ## Usage
9 |
10 | 1. `(cd .. && yarn install)` to install in the parent directory
11 | 2. `yarn install` here
12 | 3. `react-native run-android` or `react-native run-ios`
13 |
14 | You can also use `npm` instead of `yarn` if you prefer.
15 |
16 | ## Screenshot
17 |
18 | 
19 |
--------------------------------------------------------------------------------
/example/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "Immutable List View Example",
4 | "slug": "example",
5 | "privacy": "public",
6 | "sdkVersion": "35.0.0",
7 | "platforms": [
8 | "ios",
9 | "android",
10 | "web"
11 | ],
12 | "version": "1.0.0",
13 | "orientation": "portrait",
14 | "icon": "./assets/icon.png",
15 | "splash": {
16 | "image": "./assets/splash.png",
17 | "resizeMode": "contain",
18 | "backgroundColor": "#ffffff"
19 | },
20 | "updates": {
21 | "fallbackToCacheTimeout": 0
22 | },
23 | "assetBundlePatterns": [
24 | "**/*"
25 | ],
26 | "ios": {
27 | "supportsTablet": true
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/example/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/assets/icon.png
--------------------------------------------------------------------------------
/example/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/assets/splash.png
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject"
9 | },
10 | "dependencies": {
11 | "expo": "^35.0.0",
12 | "immutable": "4.0.0-rc.12",
13 | "prop-types": "15.7.2",
14 | "react": "16.8.3",
15 | "react-dom": "16.8.3",
16 | "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
17 | "react-native-immutable-list-view": "file:..",
18 | "react-native-web": "^0.11.7"
19 | },
20 | "devDependencies": {
21 | "babel-preset-expo": "^7.0.0"
22 | },
23 | "private": true
24 | }
25 |
--------------------------------------------------------------------------------
/example/rn-cli.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | const blacklist = require('metro-config/src/defaults/blacklist');
3 |
4 | module.exports = {
5 | resolver: {
6 | blacklistRE: blacklist([
7 | /node_modules\/.*\/node_modules\/react-native\/.*/,
8 | ]),
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/example/screenshots/listview-cropped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/screenshots/listview-cropped.png
--------------------------------------------------------------------------------
/example/screenshots/listview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/example/screenshots/listview.png
--------------------------------------------------------------------------------
/example/src/GenericListExample.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import PropTypes from 'prop-types';
3 | import React, { Component } from 'react';
4 | import { Text, View, Button, RefreshControl } from 'react-native';
5 |
6 | import style from './styles';
7 |
8 | const EMPTY_LIST = Immutable.List();
9 | const MOCK_DELAY = 800;
10 |
11 | class GenericListExample extends Component {
12 | defaultStateA = {
13 | data: undefined, // Will be manually set on mount.
14 | isLoading: false,
15 | errorMsg: undefined,
16 | };
17 |
18 | defaultStateB = {
19 | data: undefined, // Will be manually set on mount.
20 | isLoading: false,
21 | errorMsg: undefined,
22 | };
23 |
24 | static propTypes = {
25 | ListComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.element]).isRequired,
26 | listComponentProps: PropTypes.object.isRequired,
27 |
28 | initialDataA: PropTypes.object.isRequired,
29 | dataMutatorA: PropTypes.func.isRequired,
30 | extraPropsA: PropTypes.object,
31 |
32 | initialDataB: PropTypes.object.isRequired,
33 | dataMutatorB: PropTypes.func.isRequired,
34 | extraPropsB: PropTypes.object,
35 | };
36 |
37 | componentWillMount() {
38 | const { initialDataA, initialDataB } = this.props;
39 |
40 | this.defaultStateA.data = initialDataA;
41 | this.defaultStateB.data = initialDataB;
42 |
43 | this.setState({
44 | listA: {
45 | ...this.defaultStateA,
46 | },
47 | listB: {
48 | ...this.defaultStateB,
49 | },
50 | });
51 | }
52 |
53 | changeDataA(delay = 0) {
54 | const { listA } = this.state;
55 | const { dataMutatorA } = this.props;
56 |
57 | if (delay) {
58 | this.setState({
59 | listA: {
60 | ...listA,
61 | isLoading: true,
62 | },
63 | });
64 | }
65 |
66 | setTimeout(() => {
67 | this.setState({
68 | listA: {
69 | ...listA,
70 | data: dataMutatorA(listA.data),
71 | isLoading: false,
72 | errorMsg: undefined,
73 | },
74 | });
75 | }, delay);
76 | }
77 |
78 | changeDataB(delay = 0) {
79 | const { listB } = this.state;
80 | const { dataMutatorB } = this.props;
81 |
82 | if (delay) {
83 | this.setState({
84 | listB: {
85 | ...listB,
86 | isLoading: true,
87 | },
88 | });
89 | }
90 |
91 | setTimeout(() => {
92 | this.setState({
93 | listB: {
94 | ...listB,
95 | data: dataMutatorB(listB.data),
96 | isLoading: false,
97 | errorMsg: undefined,
98 | },
99 | });
100 | }, delay);
101 | }
102 |
103 | toggleDefaultState() {
104 | this.setState({
105 | listA: {
106 | ...this.defaultStateA,
107 | },
108 | listB: {
109 | ...this.defaultStateB,
110 | },
111 | });
112 | }
113 |
114 | toggleLoadingState() {
115 | this.setState({
116 | listA: {
117 | ...this.defaultStateA,
118 | data: EMPTY_LIST,
119 | isLoading: true,
120 | },
121 | listB: {
122 | ...this.defaultStateB,
123 | data: EMPTY_LIST,
124 | isLoading: true,
125 | },
126 | });
127 | }
128 |
129 | toggleErrorState() {
130 | this.setState({
131 | listA: {
132 | ...this.defaultStateA,
133 | data: EMPTY_LIST,
134 | errorMsg: 'Error! Fake data A has gone rogue!',
135 | },
136 | listB: {
137 | ...this.defaultStateB,
138 | data: EMPTY_LIST,
139 | errorMsg: 'Error! Fake data B has gone rogue!',
140 | },
141 | });
142 | }
143 |
144 | render() {
145 | const { listA, listB } = this.state;
146 | const {
147 | ListComponent, extraPropsA, extraPropsB, listComponentProps,
148 | } = this.props;
149 |
150 | const emptyTextA = listA.isLoading ? 'Loading...' : listA.errorMsg;
151 | const emptyTextB = listB.isLoading ? 'Loading...' : listB.errorMsg;
152 |
153 | return (
154 |
155 |
156 | {ListComponent.displayName || ListComponent.name}
157 |
158 |
159 |
160 | State:
161 |
162 |
178 |
179 |
180 |
181 | this.changeDataA()}
183 | title="Update data (or pull-refresh)"
184 | />
185 |
186 | this.changeDataA(MOCK_DELAY)}
191 | />
192 | }
193 | {...listComponentProps}
194 | {...extraPropsA}
195 | immutableData={listA.data}
196 | renderEmptyInList={emptyTextA}
197 | />
198 |
199 |
200 |
201 | this.changeDataB()}
203 | title="Update data (or pull-refresh)"
204 | />
205 |
206 | this.changeDataB(MOCK_DELAY)}
211 | />
212 | }
213 | {...listComponentProps}
214 | {...extraPropsB}
215 | immutableData={listB.data}
216 | renderEmptyInList={emptyTextB}
217 | />
218 |
219 |
220 |
221 | );
222 | }
223 | }
224 |
225 | export default GenericListExample;
226 |
--------------------------------------------------------------------------------
/example/src/ImmutableListViewExample.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import React from 'react';
3 |
4 | // ESLint can't resolve the module location when running on Travis, so ignore these lints.
5 | // eslint-disable-next-line import/no-unresolved, import/extensions
6 | import { ImmutableListView } from 'react-native-immutable-list-view/lib/ImmutableListView';
7 |
8 | import GenericListExample from './GenericListExample';
9 |
10 | import utils from './utils';
11 | import mockData from './mockData';
12 |
13 | /**
14 | *
15 | * Note: This code is NOT a good example for use in your own app.
16 | * It's only written this way because the example apps are complex
17 | * and need to be repeated for every type of list.
18 | *
19 | * For working example code to use in your own app, please see the
20 | * extensive documentation in the README.
21 | *
22 | */
23 | function ImmutableListViewExample() {
24 | return (
25 | data.setIn(['Section A', 1], 'This value was changed!')}
33 | extraPropsA={{
34 | renderSectionHeader: utils.renderSectionHeader,
35 | }}
36 |
37 | initialDataB={Immutable.Range(1, 100)}
38 | dataMutatorB={(data) => data.toSeq().map((n) => n * 2)}
39 | />
40 | );
41 | }
42 |
43 | export default ImmutableListViewExample;
44 |
--------------------------------------------------------------------------------
/example/src/ImmutableVirtualizedListExample.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import React from 'react';
3 |
4 | // ESLint can't resolve the module location when running on Travis, so ignore these lints.
5 | // eslint-disable-next-line import/no-unresolved, import/extensions
6 | import { ImmutableVirtualizedList } from 'react-native-immutable-list-view';
7 |
8 | import GenericListExample from './GenericListExample';
9 |
10 | import utils from './utils';
11 |
12 | /**
13 | *
14 | * Note: This code is NOT a good example for use in your own app.
15 | * It's only written this way because the example apps are complex
16 | * and need to be repeated for every type of list.
17 | *
18 | * For working example code to use in your own app, please see the
19 | * extensive documentation in the README.
20 | *
21 | */
22 | function ImmutableVirtualizedListExample() {
23 | return (
24 | data.set(3, 'This value was changed!')}
33 |
34 | initialDataB={Immutable.Range(1, 100)}
35 | dataMutatorB={(data) => data.toSeq().map((n) => n * 2)}
36 | />
37 | );
38 | }
39 |
40 | export default ImmutableVirtualizedListExample;
41 |
--------------------------------------------------------------------------------
/example/src/mockData.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | const data = Immutable.fromJS({
4 | 'Section A': [
5 | 'foo',
6 | 'bar',
7 | ],
8 | 'Section B': [
9 | 'fizz',
10 | 'buzz',
11 | ],
12 | });
13 |
14 | export default data;
15 |
--------------------------------------------------------------------------------
/example/src/styles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet, Platform } from 'react-native';
2 |
3 | const style = StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | justifyContent: 'center',
7 | alignItems: 'center',
8 | backgroundColor: '#F5FCFF',
9 | },
10 | title: {
11 | fontSize: 20,
12 | textAlign: 'center',
13 | padding: 24,
14 | },
15 | controlPanelContainer: {
16 | flexDirection: 'row',
17 | justifyContent: 'center',
18 | alignItems: 'center',
19 | marginBottom: 12,
20 | borderWidth: 2,
21 | borderRadius: 8,
22 | borderColor: 'black',
23 | backgroundColor: 'transparent',
24 | },
25 | controlPanelLabel: {
26 | fontSize: 18,
27 | padding: 8,
28 | },
29 | controlPanelSpacer: {
30 | width: Platform.OS === 'android' ? 4 : 0,
31 | },
32 | listContainer: {
33 | flex: 1,
34 | flexDirection: 'row',
35 | },
36 | list: {
37 | flex: 1,
38 | },
39 | listButton: {
40 | margin: 4,
41 | },
42 | listRowItem: {
43 | fontSize: 14,
44 | textAlign: 'center',
45 | color: '#333333',
46 | marginBottom: 5,
47 | },
48 | listHeaderItem: {
49 | fontSize: 16,
50 | textAlign: 'center',
51 | color: '#333333',
52 | marginBottom: 5,
53 | },
54 | });
55 |
56 | export default style;
57 |
--------------------------------------------------------------------------------
/example/src/utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | import style from './styles';
5 |
6 | const utils = {
7 |
8 | renderRow(rowData) {
9 | return {rowData};
10 | },
11 |
12 | // eslint-disable-next-line react/prop-types
13 | renderItem({ item }) {
14 | return {item};
15 | },
16 |
17 | renderSectionHeader(sectionData, category) {
18 | return (
19 |
20 | {category}
21 |
22 | );
23 | },
24 |
25 | trivialKeyExtractor(item, index) {
26 | return String(index);
27 | },
28 |
29 | };
30 |
31 | export default utils;
32 |
--------------------------------------------------------------------------------
/images/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cooperka/react-native-immutable-list-view/ac1569031ba200d16e6318a5dcca6a911fc93426/images/cover.png
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as Immutable from 'immutable'
3 | import { ListViewProps, VirtualizedListProps } from 'react-native';
4 |
5 | type Omit = Pick>
6 |
7 | // Heuristic check if data is Immutable
8 | type ImmutableData = {
9 | slice: (begin?: number, end?: number) => any;
10 | keySeq: () => any
11 | }
12 |
13 | export type ImmutableListViewProps = Omit & {
14 | immutableData: ImmutableData,
15 | dataSource?: never,
16 | sectionHeaderHasChanged?: (prevSectionData:any, nextSectionData:any) => boolean,
17 | rowsDuringInteraction?: number,
18 | renderEmpty?: string | React.FC,
19 | renderEmptyInList?: string | React.FC,
20 | }
21 |
22 | export declare class ImmutableListView extends React.Component {}
23 |
24 | export declare class EmptyListView extends React.Component & {
25 | dataSource?: never,
26 | renderRow?: React.FC,
27 | emptyText?: string,
28 | }> {}
29 |
30 | export type ImmutableVirtualizedListProps = VirtualizedListProps & {
31 | immutableData: ImmutableData,
32 | renderEmpty?: string | React.FC>,
33 | renderEmptyInList?: string | React.FC>,
34 | }
35 |
36 | export declare class ImmutableVirtualizedList extends React.Component> {}
37 |
38 | export declare class EmptyVirtualizedList extends React.Component & {
39 | renderItem: React.FC,
40 | emptyText: string
41 | }> {}
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-immutable-list-view",
3 | "version": "2.0.0",
4 | "description": "Drop-in replacement for React Native's ListView, FlatList, and VirtualizedList.",
5 | "main": "lib/index.js",
6 | "types": "index.d.ts",
7 | "files": [
8 | "lib",
9 | "src",
10 | "index.d.ts"
11 | ],
12 | "scripts": {
13 | "test": "jest",
14 | "lint": "eslint . --ext .js,.jsx",
15 | "clean": "rm -rf lib",
16 | "build": "yarn run clean && babel src --out-dir lib --ignore src/__tests__",
17 | "prepublish": "yarn run build"
18 | },
19 | "jest": {
20 | "preset": "react-native",
21 | "transform": {
22 | "node_modules/react-native/.+\\.js$": "/node_modules/react-native/jest/preprocessor.js"
23 | },
24 | "setupFiles": [
25 | "./scripts/setup-jest.js"
26 | ],
27 | "testRegex": "/src/.*__tests__/.+\\.test\\.js$",
28 | "modulePathIgnorePatterns": [
29 | "/example/"
30 | ],
31 | "verbose": true
32 | },
33 | "repository": {
34 | "type": "git",
35 | "url": "git+https://github.com/cooperka/react-native-immutable-list-view.git"
36 | },
37 | "keywords": [
38 | "react",
39 | "listview",
40 | "datasource",
41 | "pure",
42 | "immutable",
43 | "list",
44 | "map",
45 | "set"
46 | ],
47 | "author": "Kevin Cooper",
48 | "license": "MIT",
49 | "bugs": {
50 | "url": "https://github.com/cooperka/react-native-immutable-list-view/issues"
51 | },
52 | "homepage": "https://github.com/cooperka/react-native-immutable-list-view#readme",
53 | "peerDependencies": {
54 | "react": ">=15.1 || >=16.0.0-alpha.6",
55 | "react-native": ">=0.28"
56 | },
57 | "dependencies": {
58 | "immutable": ">=3.8",
59 | "prop-types": "^15.5.10"
60 | },
61 | "devDependencies": {
62 | "@babel/cli": "7.6.4",
63 | "@babel/core": "7.6.4",
64 | "@babel/plugin-proposal-class-properties": "7.5.5",
65 | "babel-eslint": "10.0.3",
66 | "babel-jest": "24.9.0",
67 | "eslint": "6.5.1",
68 | "eslint-config-airbnb": "18.0.1",
69 | "eslint-config-cooperka": "1.0.4",
70 | "eslint-plugin-import": "2.18.2",
71 | "eslint-plugin-jsx-a11y": "6.2.3",
72 | "eslint-plugin-react": "7.16.0",
73 | "eslint-plugin-react-native": "3.7.0",
74 | "jest": "24.9.0",
75 | "lodash": "4.17.15",
76 | "metro-react-native-babel-preset": "0.56.0",
77 | "react": "16.8.3",
78 | "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
79 | "react-test-renderer": "16.8.3"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/scripts/setup-jest.js:
--------------------------------------------------------------------------------
1 | const mockComponent = require.requireActual('react-native/jest/mockComponent');
2 |
3 | // Jest's default mock doesn't work well with setion headers. Use the old mock until it's improved.
4 | // https://github.com/facebook/react-native/commit/5537055bf87c8b19e9fa8413486eef6a7ac5017f#diff-606adbd6a8c97d177b17baee5a69cdd9
5 | // https://github.com/facebook/react-native/blob/master/Libraries/CustomComponents/ListView/__mocks__/ListViewMock.js
6 | jest.mock('ListView', () => {
7 | const RealListView = require.requireActual('ListView');
8 | const ListView = mockComponent('ListView');
9 | ListView.prototype.render = RealListView.prototype.render;
10 | return ListView;
11 | });
12 |
--------------------------------------------------------------------------------
/src/ImmutableListView/EmptyListView.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import PropTypes from 'prop-types';
3 | import React, { PureComponent } from 'react';
4 | import { Text, ListView } from 'react-native';
5 |
6 | // ListView renders EmptyListView which renders an empty ListView. Cycle is okay here.
7 | // eslint-disable-next-line import/no-cycle
8 | import ImmutableListView from './ImmutableListView';
9 |
10 | import styles from '../styles';
11 | import utils from '../utils';
12 |
13 | /**
14 | * A ListView that displays a single item showing that there is nothing to display.
15 | * Useful e.g. for preserving the ability to pull-refresh an empty list.
16 | */
17 | class EmptyListView extends PureComponent {
18 | static propTypes = {
19 | // Pass through any props that ListView would normally take.
20 | ...ListView.propTypes,
21 |
22 | // ImmutableListView handles creating the dataSource, so don't allow it to be passed in.
23 | dataSource: PropTypes.oneOf([undefined]),
24 |
25 | // Make this prop optional instead of required.
26 | renderRow: PropTypes.func,
27 |
28 | emptyText: PropTypes.string,
29 | };
30 |
31 | static defaultProps = {
32 | ...ListView.defaultProps,
33 |
34 | emptyText: 'No data.',
35 | };
36 |
37 | state = {
38 | listData: utils.UNITARY_LIST,
39 | };
40 |
41 | componentWillMount() {
42 | this.setListDataFromProps(this.props);
43 | }
44 |
45 | componentWillReceiveProps(nextProps) {
46 | this.setListDataFromProps(nextProps);
47 | }
48 |
49 | setListDataFromProps(props) {
50 | const { listData } = this.state;
51 | const { renderEmpty, renderEmptyInList, emptyText } = props;
52 |
53 | // Update the data to make sure the list re-renders if any of the relevant props have changed.
54 | this.setState({
55 | listData: listData.set(0, Immutable.fromJS([renderEmpty, renderEmptyInList, emptyText])),
56 | });
57 | }
58 |
59 | /**
60 | * Returns a simple text element showing the `emptyText` string.
61 | * This method can be overridden by passing in your own `renderRow` prop instead.
62 | */
63 | renderRow() {
64 | const { emptyText } = this.props;
65 |
66 | return (
67 |
68 | {emptyText}
69 |
70 | );
71 | }
72 |
73 | render() {
74 | const { listData } = this.state;
75 | const {
76 | renderEmpty, renderEmptyInList, renderSectionHeader, emptyText, ...passThroughProps
77 | } = this.props;
78 |
79 | return (
80 | this.renderRow()}
82 | {...passThroughProps}
83 | immutableData={listData}
84 | />
85 | );
86 | }
87 | }
88 |
89 | export { EmptyListView };
90 |
--------------------------------------------------------------------------------
/src/ImmutableListView/ImmutableListView.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import PropTypes from 'prop-types';
3 | import React, { PureComponent } from 'react';
4 | import { Text, ListView, InteractionManager } from 'react-native';
5 |
6 | import styles from '../styles';
7 | import utils from '../utils';
8 |
9 | // ListView renders EmptyListView which renders an empty ListView. Cycle is okay here.
10 | // eslint-disable-next-line import/no-cycle
11 | import { EmptyListView } from './EmptyListView';
12 |
13 | /**
14 | * A ListView capable of displaying {@link https://facebook.github.io/immutable-js/ Immutable} data
15 | * out of the box.
16 | */
17 | class ImmutableListView extends PureComponent {
18 | static propTypes = {
19 | // Pass through any props that ListView would normally take.
20 | ...ListView.propTypes,
21 |
22 | // ImmutableListView handles creating the dataSource, so don't allow it to be passed in.
23 | dataSource: PropTypes.oneOf([undefined]),
24 |
25 | /**
26 | * The immutable data to be rendered in a ListView.
27 | */
28 | // eslint-disable-next-line consistent-return
29 | immutableData: (props, propName, componentName) => {
30 | // Note: It's not enough to simply validate PropTypes.instanceOf(Immutable.Iterable),
31 | // because different imports of Immutable.js across files have different class prototypes.
32 | if (!utils.isImmutableIterable(props[propName])) {
33 | return new Error(`Invalid prop ${propName} supplied to ${componentName}: Must be instance of Immutable.Iterable.`);
34 | }
35 | },
36 |
37 | /**
38 | * A function taking (prevSectionData, nextSectionData)
39 | * and returning true if the section header will change.
40 | */
41 | sectionHeaderHasChanged: PropTypes.func,
42 |
43 | /**
44 | * How many rows of data to display while waiting for interactions to finish (e.g. Navigation animations).
45 | * You can use this to improve the animation performance of longer lists when pushing new routes.
46 | *
47 | * @see https://facebook.github.io/react-native/docs/performance.html#slow-navigator-transitions
48 | */
49 | rowsDuringInteraction: PropTypes.number,
50 |
51 | /**
52 | * A plain string, or a function that returns some {@link PropTypes.element}
53 | * to be rendered in place of a `ListView` when there are no items in the list.
54 | *
55 | * Things like pull-refresh functionality will be lost unless explicitly supported by your custom component.
56 | * Consider `renderEmptyInList` instead if you want this.
57 | *
58 | * It will be passed all the original props of the ImmutableListView.
59 | */
60 | renderEmpty: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
61 |
62 | /**
63 | * A plain string, or a function that returns some {@link PropTypes.element}
64 | * to be rendered inside of an `EmptyListView` when there are no items in the list.
65 | *
66 | * This allows pull-refresh functionality to be preserved.
67 | *
68 | * It will be passed all the original props of the ImmutableListView.
69 | */
70 | renderEmptyInList: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
71 | };
72 |
73 | static defaultProps = {
74 | ...ListView.defaultProps,
75 |
76 | // The data contained in the section generally doesn't affect the header text, so return false.
77 | // eslint-disable-next-line no-unused-vars
78 | sectionHeaderHasChanged: (prevSectionData, nextSectionData) => false,
79 |
80 | // Note: enableEmptySections is being used to mimic the default behavior of the upcoming version.
81 | enableEmptySections: true,
82 |
83 | // Note: removeClippedSubviews is disabled to work around a long-standing bug:
84 | // https://github.com/facebook/react-native/issues/1831
85 | removeClippedSubviews: false,
86 |
87 | renderEmptyInList: 'No data.',
88 | };
89 |
90 | state = {
91 | dataSource: new ListView.DataSource({
92 | rowHasChanged: (prevRowData, nextRowData) => !Immutable.is(prevRowData, nextRowData),
93 |
94 | getRowData: (dataBlob, sectionID, rowID) => {
95 | const rowData = utils.getValueFromKey(sectionID, dataBlob);
96 | return utils.getValueFromKey(rowID, rowData);
97 | },
98 |
99 | // eslint-disable-next-line react/destructuring-assignment
100 | sectionHeaderHasChanged: this.props.sectionHeaderHasChanged,
101 |
102 | getSectionHeaderData: (dataBlob, sectionID) => utils.getValueFromKey(sectionID, dataBlob),
103 | }),
104 |
105 | interactionOngoing: true,
106 | };
107 |
108 | componentWillMount() {
109 | this.canSetState = true;
110 | this.setStateFromPropsAfterInteraction(this.props);
111 | }
112 |
113 | componentWillReceiveProps(newProps) {
114 | this.setStateFromPropsAfterInteraction(newProps);
115 | }
116 |
117 | componentWillUnmount() {
118 | this.canSetState = false;
119 | }
120 |
121 | setStateFromPropsAfterInteraction(props) {
122 | // Always set state right away before the interaction.
123 | this.setStateFromProps(props, false);
124 |
125 | // If set, wait for animations etc. to complete before rendering the full list of data.
126 | if (props.rowsDuringInteraction >= 0) {
127 | InteractionManager.runAfterInteractions(() => {
128 | this.setStateFromProps(props, true);
129 | });
130 | }
131 | }
132 |
133 | setStateFromProps(props, interactionHasJustFinished) {
134 | // In some cases the component will have been unmounted before executing
135 | // InteractionManager.runAfterInteractions, causing a warning if we try to set state.
136 | if (!this.canSetState) return;
137 |
138 | const { dataSource, interactionOngoing } = this.state;
139 | const { immutableData, rowsDuringInteraction, renderSectionHeader } = props;
140 |
141 | const shouldDisplayPartialData = rowsDuringInteraction >= 0 && interactionOngoing && !interactionHasJustFinished;
142 |
143 | const displayData = (shouldDisplayPartialData
144 | ? immutableData.slice(0, rowsDuringInteraction)
145 | : immutableData);
146 |
147 | const updatedDataSource = (renderSectionHeader
148 | ? dataSource.cloneWithRowsAndSections(
149 | displayData, utils.getKeys(displayData), utils.getRowIdentities(displayData),
150 | )
151 | : dataSource.cloneWithRows(
152 | displayData, utils.getKeys(displayData),
153 | ));
154 |
155 | this.setState({
156 | dataSource: updatedDataSource,
157 | interactionOngoing: interactionHasJustFinished ? false : interactionOngoing,
158 | });
159 | }
160 |
161 | getListView() {
162 | return this.listViewRef;
163 | }
164 |
165 | getMetrics = (...args) =>
166 | this.listViewRef && this.listViewRef.getMetrics(...args);
167 |
168 | scrollTo = (...args) =>
169 | this.listViewRef && this.listViewRef.scrollTo(...args);
170 |
171 | scrollToEnd = (...args) =>
172 | this.listViewRef && this.listViewRef.scrollToEnd(...args);
173 |
174 | renderEmpty() {
175 | const {
176 | immutableData, enableEmptySections, renderEmpty, renderEmptyInList, contentContainerStyle,
177 | } = this.props;
178 |
179 | const shouldTryToRenderEmpty = renderEmpty || renderEmptyInList;
180 | if (shouldTryToRenderEmpty && utils.isEmptyListView(immutableData, enableEmptySections)) {
181 | if (renderEmpty) {
182 | if (typeof renderEmpty === 'string') {
183 | return {renderEmpty};
184 | }
185 | return renderEmpty(this.props);
186 | }
187 | if (renderEmptyInList) {
188 | if (typeof renderEmptyInList === 'string') {
189 | const { renderRow, ...passThroughProps } = this.props;
190 | return ;
191 | }
192 | return renderEmptyInList(this.props)} />;
193 | }
194 | }
195 |
196 | return null;
197 | }
198 |
199 | render() {
200 | const { dataSource } = this.state;
201 | const {
202 | immutableData, renderEmpty, renderEmptyInList, rowsDuringInteraction, sectionHeaderHasChanged, ...passThroughProps
203 | } = this.props;
204 |
205 | return this.renderEmpty() || (
206 | { this.listViewRef = component; }}
208 | dataSource={dataSource}
209 | {...passThroughProps}
210 | />
211 | );
212 | }
213 | }
214 |
215 | export default ImmutableListView;
216 |
--------------------------------------------------------------------------------
/src/ImmutableListView/__tests__/EmptyListView.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { renderers } from '../../test-utils';
5 |
6 | import { EmptyListView } from '../EmptyListView';
7 |
8 | describe('EmptyListView', () => {
9 | it('renders with default text', () => {
10 | const tree = renderer.create(
11 | ,
12 | );
13 | expect(tree.toJSON()).toMatchSnapshot();
14 | });
15 |
16 | it('renders with custom text', () => {
17 | const tree = renderer.create(
18 | ,
21 | );
22 | expect(tree.toJSON()).toMatchSnapshot();
23 | });
24 |
25 | it('renders with custom renderRow', () => {
26 | const tree = renderer.create(
27 | renderers.renderRow('Overridden!')}
30 | />,
31 | );
32 | expect(tree.toJSON()).toMatchSnapshot();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/ImmutableListView/__tests__/ImmutableListView.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { InteractionManager } from 'react-native';
3 | import renderer from 'react-test-renderer';
4 |
5 | import ImmutableListView from '../ImmutableListView';
6 |
7 | import { data, renderers, expectors } from '../../test-utils';
8 |
9 | describe('ImmutableListView', () => {
10 | it('renders with empty data', () => {
11 | expectors.expectToMatchSnapshotWithData(data.EMPTY_DATA);
12 | });
13 |
14 | it('renders basic List', () => {
15 | expectors.expectToMatchSnapshotWithData(data.LIST_DATA);
16 | });
17 |
18 | it('renders nested List', () => {
19 | expectors.expectToMatchSnapshotWithData(data.LIST_DATA_NESTED);
20 | });
21 |
22 | it('renders Map: List rows, without section headers', () => {
23 | expectors.expectToMatchSnapshotWithData(data.MAP_DATA_LIST_ROWS);
24 | });
25 |
26 | it('renders Map: List rows, with section headers', () => {
27 | expectors.expectToMatchSnapshotWithData(data.MAP_DATA_LIST_ROWS, true);
28 | });
29 |
30 | it('renders Map: Map rows, without section headers', () => {
31 | expectors.expectToMatchSnapshotWithData(data.MAP_DATA_MAP_ROWS);
32 | });
33 |
34 | it('renders Map: Map rows, with section headers', () => {
35 | expectors.expectToMatchSnapshotWithData(data.MAP_DATA_MAP_ROWS, true);
36 | });
37 |
38 | it('renders basic Set', () => {
39 | expectors.expectToMatchSnapshotWithData(data.SET_DATA);
40 | });
41 |
42 | it('renders basic Range', () => {
43 | expectors.expectToMatchSnapshotWithData(data.RANGE_DATA);
44 | });
45 | });
46 |
47 | describe('ImmutableListView with delayed rendering', () => {
48 | it('renders basic List during interactions', () => {
49 | // Mock this method to make sure it's not run.
50 | InteractionManager.runAfterInteractions = () => {};
51 |
52 | const tree = renderer.create(
53 | ,
58 | ).toJSON();
59 | expect(tree).toMatchSnapshot();
60 | });
61 |
62 | it('renders basic List after interactions', () => {
63 | // Mock this method to make sure it runs immediately.
64 | InteractionManager.runAfterInteractions = (callback) => callback();
65 |
66 | const tree = renderer.create(
67 | ,
72 | ).toJSON();
73 | expect(tree).toMatchSnapshot();
74 | });
75 | });
76 |
77 | describe('ImmutableListView with renderEmpty', () => {
78 | it('renders normally when there are some items', () => {
79 | const tree = renderer.create(
80 | renderers.renderRow('No items')}
84 | />,
85 | );
86 | expect(tree.toJSON()).toMatchSnapshot();
87 | });
88 |
89 | it('renders empty with a function', () => {
90 | const tree = renderer.create(
91 | renderers.renderRow('No items')}
95 | />,
96 | );
97 | expect(tree.toJSON()).toMatchSnapshot();
98 | });
99 |
100 | it('renders empty with a string', () => {
101 | const color = 'red';
102 |
103 | const tree = renderer.create(
104 | ,
110 | );
111 | expect(tree.toJSON()).toMatchSnapshot();
112 | });
113 |
114 | it('doesn\'t render empty with null', () => {
115 | const tree = renderer.create(
116 | ,
122 | );
123 | expect(tree.toJSON()).toMatchSnapshot();
124 | });
125 | });
126 |
127 | describe('ImmutableListView with renderEmptyInList', () => {
128 | it('renders normally when there are some items', () => {
129 | const tree = renderer.create(
130 | renderers.renderRow('No items')}
134 | />,
135 | );
136 | expect(tree.toJSON()).toMatchSnapshot();
137 | });
138 |
139 | it('renders empty with a function', () => {
140 | const tree = renderer.create(
141 | renderers.renderRow('No items')}
145 | />,
146 | );
147 | expect(tree.toJSON()).toMatchSnapshot();
148 | });
149 |
150 | it('renders empty with a string', () => {
151 | const color = 'red';
152 |
153 | const tree = renderer.create(
154 | ,
160 | );
161 | expect(tree.toJSON()).toMatchSnapshot();
162 | });
163 |
164 | it('doesn\'t render empty with null', () => {
165 | const tree = renderer.create(
166 | ,
172 | );
173 | expect(tree.toJSON()).toMatchSnapshot();
174 | });
175 | });
176 |
--------------------------------------------------------------------------------
/src/ImmutableListView/__tests__/__snapshots__/EmptyListView.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`EmptyListView renders with custom renderRow 1`] = `
4 |
25 |
26 |
27 | "Overridden!"
28 |
29 |
30 |
31 | `;
32 |
33 | exports[`EmptyListView renders with custom text 1`] = `
34 |
55 |
56 |
64 | Nothing. Nothing at all.
65 |
66 |
67 |
68 | `;
69 |
70 | exports[`EmptyListView renders with default text 1`] = `
71 |
92 |
93 |
101 | No data.
102 |
103 |
104 |
105 | `;
106 |
--------------------------------------------------------------------------------
/src/ImmutableListView/__tests__/__snapshots__/ImmutableListView.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ImmutableListView renders Map: List rows, with section headers 1`] = `
4 |
33 |
34 |
37 | first (3 items)
38 |
39 |
40 | "m"
41 |
42 |
43 | "a"
44 |
45 |
46 | "p"
47 |
48 |
51 | second (1 items)
52 |
53 |
54 | "foo"
55 |
56 |
59 | third (0 items)
60 |
61 |
64 | fourth (1 items)
65 |
66 |
67 | "bar"
68 |
69 |
70 |
71 | `;
72 |
73 | exports[`ImmutableListView renders Map: List rows, without section headers 1`] = `
74 |
95 |
96 |
97 | ["m","a","p"]
98 |
99 |
100 | ["foo"]
101 |
102 |
103 | []
104 |
105 |
106 | ["bar"]
107 |
108 |
109 |
110 | `;
111 |
112 | exports[`ImmutableListView renders Map: Map rows, with section headers 1`] = `
113 |
140 |
141 |
144 | first (2 items)
145 |
146 |
147 | "data 1"
148 |
149 |
150 | "data 2"
151 |
152 |
155 | second (0 items)
156 |
157 |
158 |
159 | `;
160 |
161 | exports[`ImmutableListView renders Map: Map rows, without section headers 1`] = `
162 |
183 |
184 |
185 | {"row1":"data 1","row2":"data 2"}
186 |
187 |
188 | {}
189 |
190 |
191 |
192 | `;
193 |
194 | exports[`ImmutableListView renders basic List 1`] = `
195 |
216 |
217 |
218 | "lists"
219 |
220 |
221 | "are"
222 |
223 |
224 | "great"
225 |
226 |
227 |
228 | `;
229 |
230 | exports[`ImmutableListView renders basic Range 1`] = `
231 |
252 |
253 |
254 | 3
255 |
256 |
257 | 6
258 |
259 |
260 | 9
261 |
262 |
263 |
264 | `;
265 |
266 | exports[`ImmutableListView renders basic Set 1`] = `
267 |
288 |
289 |
290 | "one"
291 |
292 |
293 | "two"
294 |
295 |
296 | "three"
297 |
298 |
299 |
300 | `;
301 |
302 | exports[`ImmutableListView renders nested List 1`] = `
303 |
324 |
325 |
326 | ["so","are"]
327 |
328 |
329 | ["nested","lists"]
330 |
331 |
332 |
333 | `;
334 |
335 | exports[`ImmutableListView renders with empty data 1`] = `
336 |
357 |
358 |
366 | No data.
367 |
368 |
369 |
370 | `;
371 |
372 | exports[`ImmutableListView with delayed rendering renders basic List after interactions 1`] = `
373 |
394 |
395 |
396 | "lists"
397 |
398 |
399 | "are"
400 |
401 |
402 | "great"
403 |
404 |
405 |
406 | `;
407 |
408 | exports[`ImmutableListView with delayed rendering renders basic List during interactions 1`] = `
409 |
430 |
431 |
432 | "lists"
433 |
434 |
435 |
436 | `;
437 |
438 | exports[`ImmutableListView with renderEmpty doesn't render empty with null 1`] = `
439 |
460 |
461 |
462 | `;
463 |
464 | exports[`ImmutableListView with renderEmpty renders empty with a function 1`] = `
465 |
466 | "No items"
467 |
468 | `;
469 |
470 | exports[`ImmutableListView with renderEmpty renders empty with a string 1`] = `
471 |
484 | No items
485 |
486 | `;
487 |
488 | exports[`ImmutableListView with renderEmpty renders normally when there are some items 1`] = `
489 |
510 |
511 |
512 | "lists"
513 |
514 |
515 | "are"
516 |
517 |
518 | "great"
519 |
520 |
521 |
522 | `;
523 |
524 | exports[`ImmutableListView with renderEmptyInList doesn't render empty with null 1`] = `
525 |
546 |
547 |
548 | `;
549 |
550 | exports[`ImmutableListView with renderEmptyInList renders empty with a function 1`] = `
551 |
572 |
573 |
574 | "No items"
575 |
576 |
577 |
578 | `;
579 |
580 | exports[`ImmutableListView with renderEmptyInList renders empty with a string 1`] = `
581 |
607 |
608 |
616 | No items
617 |
618 |
619 |
620 | `;
621 |
622 | exports[`ImmutableListView with renderEmptyInList renders normally when there are some items 1`] = `
623 |
644 |
645 |
646 | "lists"
647 |
648 |
649 | "are"
650 |
651 |
652 | "great"
653 |
654 |
655 |
656 | `;
657 |
--------------------------------------------------------------------------------
/src/ImmutableListView/index.js:
--------------------------------------------------------------------------------
1 | import ImmutableListView from './ImmutableListView';
2 |
3 | export { ImmutableListView };
4 | export * from './EmptyListView';
5 |
--------------------------------------------------------------------------------
/src/ImmutableVirtualizedList/EmptyVirtualizedList.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import PropTypes from 'prop-types';
3 | import React, { PureComponent } from 'react';
4 | import { Text, VirtualizedList } from 'react-native';
5 |
6 | // ListView renders EmptyListView which renders an empty ListView. Cycle is okay here.
7 | // eslint-disable-next-line import/no-cycle
8 | import ImmutableVirtualizedList from './ImmutableVirtualizedList';
9 |
10 | import styles from '../styles';
11 | import utils from '../utils';
12 |
13 | /**
14 | * A VirtualizedList that displays a single item showing that there is nothing to display.
15 | * Useful e.g. for preserving the ability to pull-refresh an empty list.
16 | */
17 | class EmptyVirtualizedList extends PureComponent {
18 | static propTypes = {
19 | // Pass through any props that VirtualizedList would normally take.
20 | ...VirtualizedList.propTypes,
21 |
22 | // Make this prop optional instead of required.
23 | renderItem: PropTypes.func,
24 |
25 | emptyText: PropTypes.string,
26 | };
27 |
28 | static defaultProps = {
29 | ...VirtualizedList.defaultProps,
30 |
31 | keyExtractor: (_, index) => String(index),
32 |
33 | emptyText: 'No data.',
34 | };
35 |
36 | state = {
37 | listData: utils.UNITARY_LIST,
38 | };
39 |
40 | componentWillMount() {
41 | this.setListDataFromProps(this.props);
42 | }
43 |
44 | componentWillReceiveProps(nextProps) {
45 | this.setListDataFromProps(nextProps);
46 | }
47 |
48 | setListDataFromProps(props) {
49 | const { listData } = this.state;
50 | const { renderEmpty, renderEmptyInList, emptyText } = props;
51 |
52 | // Update the data to make sure the list re-renders if any of the relevant props have changed.
53 | this.setState({
54 | listData: listData.set(0, Immutable.fromJS([renderEmpty, renderEmptyInList, emptyText])),
55 | });
56 | }
57 |
58 | /**
59 | * Returns a simple text element showing the `emptyText` string.
60 | * This method can be overridden by passing in your own `renderItem` prop instead.
61 | */
62 | renderItem() {
63 | const { emptyText } = this.props;
64 |
65 | return (
66 |
67 | {emptyText}
68 |
69 | );
70 | }
71 |
72 | render() {
73 | const { listData } = this.state;
74 | const { renderEmpty, renderEmptyInList, ...passThroughProps } = this.props;
75 |
76 | return (
77 | this.renderItem()}
79 | {...passThroughProps}
80 | immutableData={listData}
81 | keyExtractor={() => 'empty'}
82 | />
83 | );
84 | }
85 | }
86 |
87 | export { EmptyVirtualizedList };
88 |
--------------------------------------------------------------------------------
/src/ImmutableVirtualizedList/ImmutableVirtualizedList.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 | import PropTypes from 'prop-types';
3 | import React, { PureComponent } from 'react';
4 | import { Text, VirtualizedList } from 'react-native';
5 |
6 | import styles from '../styles';
7 | import utils from '../utils';
8 |
9 | // ListView renders EmptyListView which renders an empty ListView. Cycle is okay here.
10 | // eslint-disable-next-line import/no-cycle
11 | import { EmptyVirtualizedList } from './EmptyVirtualizedList';
12 |
13 | /**
14 | * A VirtualizedList capable of displaying {@link https://facebook.github.io/immutable-js/ Immutable} data
15 | * out of the box.
16 | */
17 | // eslint-disable-next-line react/prefer-stateless-function
18 | class ImmutableVirtualizedList extends PureComponent {
19 | static propTypes = {
20 | // Pass through any props that VirtualizedList would normally take.
21 | ...VirtualizedList.propTypes,
22 |
23 | /**
24 | * The immutable data to be rendered in a VirtualizedList.
25 | */
26 | // eslint-disable-next-line consistent-return
27 | immutableData: (props, propName, componentName) => {
28 | // Note: It's not enough to simply validate PropTypes.instanceOf(Immutable.Iterable),
29 | // because different imports of Immutable.js across files have different class prototypes.
30 | // TODO: Add support for Immutable.Map, etc.
31 | if (Immutable.Map.isMap(props[propName])) {
32 | return new Error(`Invalid prop ${propName} supplied to ${componentName}: Support for Immutable.Map is coming soon. For now, try an Immutable List, Set, or Range.`);
33 | } else if (!utils.isImmutableIterable(props[propName])) {
34 | return new Error(`Invalid prop ${propName} supplied to ${componentName}: Must be instance of Immutable.Iterable.`);
35 | }
36 | },
37 |
38 | /**
39 | * A plain string, or a function that returns some {@link PropTypes.element}
40 | * to be rendered in place of a `VirtualizedList` when there are no items in the list.
41 | *
42 | * Things like pull-refresh functionality will be lost unless explicitly supported by your custom component.
43 | * Consider `renderEmptyInList` instead if you want this.
44 | *
45 | * It will be passed all the original props of the ImmutableVirtualizedList.
46 | */
47 | renderEmpty: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
48 |
49 | /**
50 | * A plain string, or a function that returns some {@link PropTypes.element}
51 | * to be rendered inside of an `EmptyVirtualizedList` when there are no items in the list.
52 | *
53 | * This allows pull-refresh functionality to be preserved.
54 | *
55 | * It will be passed all the original props of the ImmutableVirtualizedList.
56 | */
57 | renderEmptyInList: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
58 | };
59 |
60 | static defaultProps = {
61 | ...VirtualizedList.defaultProps,
62 |
63 | renderEmptyInList: 'No data.',
64 | };
65 |
66 | getVirtualizedList() {
67 | return this.virtualizedListRef;
68 | }
69 |
70 | scrollToEnd = (...args) =>
71 | this.virtualizedListRef && this.virtualizedListRef.scrollToEnd(...args);
72 |
73 | scrollToIndex = (...args) =>
74 | this.virtualizedListRef && this.virtualizedListRef.scrollToIndex(...args);
75 |
76 | scrollToItem = (...args) =>
77 | this.virtualizedListRef && this.virtualizedListRef.scrollToItem(...args);
78 |
79 | scrollToOffset = (...args) =>
80 | this.virtualizedListRef && this.virtualizedListRef.scrollToOffset(...args);
81 |
82 | recordInteraction = (...args) =>
83 | this.virtualizedListRef && this.virtualizedListRef.recordInteraction(...args);
84 |
85 | renderEmpty() {
86 | const {
87 | immutableData, renderEmpty, renderEmptyInList, contentContainerStyle,
88 | } = this.props;
89 |
90 | const shouldTryToRenderEmpty = renderEmpty || renderEmptyInList;
91 | if (shouldTryToRenderEmpty && utils.isEmptyListView(immutableData)) {
92 | if (renderEmpty) {
93 | if (typeof renderEmpty === 'string') {
94 | return {renderEmpty};
95 | }
96 | return renderEmpty(this.props);
97 | }
98 | if (renderEmptyInList) {
99 | if (typeof renderEmptyInList === 'string') {
100 | const { renderItem, ...passThroughProps } = this.props;
101 | return ;
102 | }
103 | return renderEmptyInList(this.props)} />;
104 | }
105 | }
106 |
107 | return null;
108 | }
109 |
110 | render() {
111 | const { immutableData, renderEmpty, renderEmptyInList, ...passThroughProps } = this.props;
112 |
113 | return this.renderEmpty() || (
114 | { this.virtualizedListRef = component; }}
116 | data={immutableData}
117 | getItem={(items, index) => utils.getValueFromKey(index, items)}
118 | getItemCount={(items) => ((items && items.size) || 0)}
119 | keyExtractor={(item, index) => String(index)}
120 | {...passThroughProps}
121 | />
122 | );
123 | }
124 | }
125 |
126 | export default ImmutableVirtualizedList;
127 |
--------------------------------------------------------------------------------
/src/ImmutableVirtualizedList/__tests__/EmptyVirtualizedList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { renderers } from '../../test-utils';
5 |
6 | import { EmptyVirtualizedList } from '../EmptyVirtualizedList';
7 |
8 | describe('EmptyVirtualizedList', () => {
9 | it('renders with default text', () => {
10 | const tree = renderer.create(
11 | ,
12 | );
13 | expect(tree.toJSON()).toMatchSnapshot();
14 | });
15 |
16 | it('renders with custom text', () => {
17 | const tree = renderer.create(
18 | ,
21 | );
22 | expect(tree.toJSON()).toMatchSnapshot();
23 | });
24 |
25 | it('renders with custom renderRow', () => {
26 | const tree = renderer.create(
27 | renderers.renderRow('Overridden!')}
30 | />,
31 | );
32 | expect(tree.toJSON()).toMatchSnapshot();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/ImmutableVirtualizedList/__tests__/ImmutableVirtualizedList.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { data, renderers, expectors } from '../../test-utils';
5 |
6 | import ImmutableVirtualizedList from '../ImmutableVirtualizedList';
7 |
8 | describe('ImmutableVirtualizedList', () => {
9 | it('renders with empty data', () => {
10 | expectors.expectVirtualizedToMatchSnapshotWithData(data.EMPTY_DATA);
11 | });
12 |
13 | it('renders basic List', () => {
14 | expectors.expectVirtualizedToMatchSnapshotWithData(data.LIST_DATA);
15 | });
16 |
17 | it('renders nested List', () => {
18 | expectors.expectVirtualizedToMatchSnapshotWithData(data.LIST_DATA_NESTED);
19 | });
20 |
21 | it('renders basic Range', () => {
22 | expectors.expectVirtualizedToMatchSnapshotWithData(data.RANGE_DATA);
23 | });
24 | });
25 |
26 | describe('ImmutableVirtualizedList with renderEmpty', () => {
27 | it('renders normally when there are some items', () => {
28 | const tree = renderer.create(
29 | renderers.renderRow('No items')}
33 | />,
34 | );
35 | expect(tree.toJSON()).toMatchSnapshot();
36 | });
37 |
38 | it('renders empty with a function', () => {
39 | const tree = renderer.create(
40 | renderers.renderRow('No items')}
44 | />,
45 | );
46 | expect(tree.toJSON()).toMatchSnapshot();
47 | });
48 |
49 | it('renders empty with a string', () => {
50 | const color = 'red';
51 |
52 | const tree = renderer.create(
53 | ,
59 | );
60 | expect(tree.toJSON()).toMatchSnapshot();
61 | });
62 |
63 | it('doesn\'t render empty with null', () => {
64 | const tree = renderer.create(
65 | ,
71 | );
72 | expect(tree.toJSON()).toMatchSnapshot();
73 | });
74 | });
75 |
76 | describe('ImmutableVirtualizedList with renderEmptyInList', () => {
77 | it('renders normally when there are some items', () => {
78 | const tree = renderer.create(
79 | renderers.renderRow('No items')}
83 | />,
84 | );
85 | expect(tree.toJSON()).toMatchSnapshot();
86 | });
87 |
88 | it('renders empty with a function', () => {
89 | const tree = renderer.create(
90 | renderers.renderRow('No items')}
94 | />,
95 | );
96 | expect(tree.toJSON()).toMatchSnapshot();
97 | });
98 |
99 | it('renders empty with a string', () => {
100 | const color = 'red';
101 |
102 | const tree = renderer.create(
103 | ,
109 | );
110 | expect(tree.toJSON()).toMatchSnapshot();
111 | });
112 |
113 | it('doesn\'t render empty with null', () => {
114 | const tree = renderer.create(
115 | ,
121 | );
122 | expect(tree.toJSON()).toMatchSnapshot();
123 | });
124 | });
125 |
--------------------------------------------------------------------------------
/src/ImmutableVirtualizedList/__tests__/__snapshots__/EmptyVirtualizedList.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`EmptyVirtualizedList renders with custom renderRow 1`] = `
4 |
35 |
36 |
40 |
41 | "Overridden!"
42 |
43 |
44 |
45 |
46 | `;
47 |
48 | exports[`EmptyVirtualizedList renders with custom text 1`] = `
49 |
80 |
81 |
85 |
93 | Nothing. Nothing at all.
94 |
95 |
96 |
97 |
98 | `;
99 |
100 | exports[`EmptyVirtualizedList renders with default text 1`] = `
101 |
132 |
133 |
137 |
145 | No data.
146 |
147 |
148 |
149 |
150 | `;
151 |
--------------------------------------------------------------------------------
/src/ImmutableVirtualizedList/__tests__/__snapshots__/ImmutableVirtualizedList.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ImmutableVirtualizedList renders basic List 1`] = `
4 |
32 |
33 |
37 |
38 | "lists"
39 |
40 |
41 |
45 |
46 | "are"
47 |
48 |
49 |
53 |
54 | "great"
55 |
56 |
57 |
58 |
59 | `;
60 |
61 | exports[`ImmutableVirtualizedList renders basic Range 1`] = `
62 |
84 |
85 |
89 |
90 | 3
91 |
92 |
93 |
97 |
98 | 6
99 |
100 |
101 |
105 |
106 | 9
107 |
108 |
109 |
110 |
111 | `;
112 |
113 | exports[`ImmutableVirtualizedList renders nested List 1`] = `
114 |
147 |
148 |
152 |
153 | ["so","are"]
154 |
155 |
156 |
160 |
161 | ["nested","lists"]
162 |
163 |
164 |
165 |
166 | `;
167 |
168 | exports[`ImmutableVirtualizedList renders with empty data 1`] = `
169 |
200 |
201 |
205 |
213 | No data.
214 |
215 |
216 |
217 |
218 | `;
219 |
220 | exports[`ImmutableVirtualizedList with renderEmpty doesn't render empty with null 1`] = `
221 |
243 |
244 |
245 | `;
246 |
247 | exports[`ImmutableVirtualizedList with renderEmpty renders empty with a function 1`] = `
248 |
249 | "No items"
250 |
251 | `;
252 |
253 | exports[`ImmutableVirtualizedList with renderEmpty renders empty with a string 1`] = `
254 |
267 | No items
268 |
269 | `;
270 |
271 | exports[`ImmutableVirtualizedList with renderEmpty renders normally when there are some items 1`] = `
272 |
300 |
301 |
305 |
306 | {"item":"lists","index":0,"separators":{}}
307 |
308 |
309 |
313 |
314 | {"item":"are","index":1,"separators":{}}
315 |
316 |
317 |
321 |
322 | {"item":"great","index":2,"separators":{}}
323 |
324 |
325 |
326 |
327 | `;
328 |
329 | exports[`ImmutableVirtualizedList with renderEmptyInList doesn't render empty with null 1`] = `
330 |
352 |
353 |
354 | `;
355 |
356 | exports[`ImmutableVirtualizedList with renderEmptyInList renders empty with a function 1`] = `
357 |
388 |
389 |
393 |
394 | "No items"
395 |
396 |
397 |
398 |
399 | `;
400 |
401 | exports[`ImmutableVirtualizedList with renderEmptyInList renders empty with a string 1`] = `
402 |
438 |
439 |
443 |
451 | No items
452 |
453 |
454 |
455 |
456 | `;
457 |
458 | exports[`ImmutableVirtualizedList with renderEmptyInList renders normally when there are some items 1`] = `
459 |
487 |
488 |
492 |
493 | {"item":"lists","index":0,"separators":{}}
494 |
495 |
496 |
500 |
501 | {"item":"are","index":1,"separators":{}}
502 |
503 |
504 |
508 |
509 | {"item":"great","index":2,"separators":{}}
510 |
511 |
512 |
513 |
514 | `;
515 |
--------------------------------------------------------------------------------
/src/ImmutableVirtualizedList/index.js:
--------------------------------------------------------------------------------
1 | import ImmutableVirtualizedList from './ImmutableVirtualizedList';
2 |
3 | export { ImmutableVirtualizedList };
4 | export * from './EmptyVirtualizedList';
5 |
--------------------------------------------------------------------------------
/src/__tests__/comparison.test.js:
--------------------------------------------------------------------------------
1 | import { data, expectors } from '../test-utils';
2 |
3 | describe('ImmutableListView vs. ListView', () => {
4 | it('renders the same as ListView with empty data', () => {
5 | expectors.expectToMatchListViewWithData(data.EMPTY_DATA);
6 | });
7 |
8 | it('renders the same as ListView with basic List', () => {
9 | expectors.expectToMatchListViewWithData(data.LIST_DATA);
10 | });
11 |
12 | it('renders the same as ListView with nested List', () => {
13 | expectors.expectToMatchListViewWithData(data.LIST_DATA_NESTED);
14 | });
15 |
16 | it('renders the same as ListView with Map: List rows, without section headers', () => {
17 | expectors.expectToMatchListViewWithData(data.MAP_DATA_LIST_ROWS);
18 | });
19 |
20 | // This is currently NOT the same. This behavior is documented in the README.
21 | // To see what actually renders, look at the snapshot file.
22 | it.skip('renders the same as ListView with Map: List rows, WITH section headers', () => {
23 | expectors.expectToMatchListViewWithData(data.MAP_DATA_LIST_ROWS, true);
24 | });
25 |
26 | it('renders the same as ListView with Map: Map rows, without section headers', () => {
27 | expectors.expectToMatchListViewWithData(data.MAP_DATA_MAP_ROWS);
28 | });
29 |
30 | // This is currently NOT the same. This behavior is documented in the README.
31 | // To see what actually renders, look at the snapshot file.
32 | it.skip('renders the same as ListView with Map: Map rows, WITH section headers', () => {
33 | expectors.expectToMatchListViewWithData(data.MAP_DATA_MAP_ROWS, true);
34 | });
35 |
36 | it('renders the same as ListView with basic Set', () => {
37 | expectors.expectToMatchListViewWithData(data.SET_DATA);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/__tests__/utils.test.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | import { data } from '../test-utils';
4 |
5 | import utils from '../utils';
6 |
7 | const EMPTY_MAP = Immutable.fromJS({ foo: [], bar: {}, baz: null });
8 |
9 | describe('Utils', () => {
10 | Object.keys(data).forEach((dataType) => {
11 | const shouldBeEmpty = data[dataType] === data.EMPTY_DATA;
12 |
13 | it(`determines that ${dataType} ${shouldBeEmpty ? 'is' : 'is NOT'} empty`, () => {
14 | const isEmpty = utils.isEmptyListView(data[dataType]);
15 | expect(isEmpty).toBe(shouldBeEmpty);
16 | });
17 | });
18 |
19 | it('determines that a Map with empty sections is empty', () => {
20 | const isEmpty = utils.isEmptyListView(EMPTY_MAP);
21 | expect(isEmpty).toBe(true);
22 | });
23 |
24 | it('determines that a Map with empty sections is NOT empty with enableEmptySections', () => {
25 | const isEmpty = utils.isEmptyListView(EMPTY_MAP, true);
26 | expect(isEmpty).toBe(false);
27 | });
28 |
29 | it('determines that empty data is still empty with enableEmptySections', () => {
30 | const isEmpty = utils.isEmptyListView(data.EMPTY_DATA, true);
31 | expect(isEmpty).toBe(true);
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export {
2 | ImmutableVirtualizedList,
3 | EmptyVirtualizedList,
4 | } from './ImmutableVirtualizedList';
5 |
--------------------------------------------------------------------------------
/src/styles.js:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export default StyleSheet.create({
4 | emptyText: {
5 | padding: 8,
6 | textAlign: 'center',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/src/test-utils.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import Immutable from 'immutable';
3 | import React from 'react';
4 | import { Text, ListView } from 'react-native';
5 | import renderer from 'react-test-renderer';
6 | /* eslint-enable */
7 |
8 | import { ImmutableListView } from './ImmutableListView';
9 | import { ImmutableVirtualizedList } from './ImmutableVirtualizedList';
10 |
11 | /**
12 | * Some common types of data you may want to render with ImmutableListView.
13 | * @see https://facebook.github.io/react-native/docs/listviewdatasource.html#constructor
14 | */
15 | const data = {
16 | EMPTY_DATA: Immutable.List(),
17 |
18 | LIST_DATA: Immutable.List([
19 | 'lists',
20 | 'are',
21 | 'great',
22 | ]),
23 |
24 | LIST_DATA_NESTED: Immutable.List([
25 | [
26 | 'so',
27 | 'are',
28 | ],
29 | [
30 | 'nested',
31 | 'lists',
32 | ],
33 | ]),
34 |
35 | MAP_DATA_LIST_ROWS: Immutable.fromJS({
36 | first: [
37 | 'm',
38 | 'a',
39 | 'p',
40 | ],
41 | second: [
42 | 'foo',
43 | ],
44 | third: [
45 | ],
46 | fourth: [
47 | 'bar',
48 | ],
49 | }),
50 |
51 | MAP_DATA_MAP_ROWS: Immutable.fromJS({
52 | first: {
53 | row1: 'data 1',
54 | row2: 'data 2',
55 | },
56 | second: {},
57 | }),
58 |
59 | SET_DATA: Immutable.Set([
60 | 'one',
61 | 'two',
62 | 'three',
63 | ]),
64 |
65 | RANGE_DATA: Immutable.Range(3, 10, 3),
66 | };
67 |
68 | const renderers = {
69 | /**
70 | * @param {*} rowData
71 | */
72 | renderRow(rowData) {
73 | return {JSON.stringify(rowData)};
74 | },
75 |
76 | // eslint-disable-next-line react/prop-types
77 | renderItem({ item }) {
78 | return {JSON.stringify(item)};
79 | },
80 |
81 | /**
82 | * @param {Immutable.Iterable} sectionData
83 | * @param {String} category
84 | */
85 | renderSectionHeader(sectionData, category) {
86 | return {`${category} (${sectionData.size} items)`};
87 | },
88 | };
89 |
90 | const mocks = {
91 | /**
92 | * Mock ScrollView so that it doesn't contain any props when rendered by ListView.
93 | * This is useful for comparison between ListView and ImmutableListView.
94 | *
95 | * @returns {ImmutableListView}
96 | */
97 | getImmutableListViewWithoutProps() {
98 | jest.resetModules();
99 |
100 | // eslint-disable-next-line react/prop-types
101 | const mockScrollView = ({ children }) => React.createElement('ScrollView', {}, children);
102 | jest.doMock('ScrollView', () => mockScrollView);
103 |
104 | // eslint-disable-next-line global-require
105 | return require('./ImmutableListView').ImmutableListView;
106 | },
107 | };
108 |
109 | const expectors = {
110 | expectToMatchSnapshotWithData(immutableData, shouldRenderSectionHeaders) {
111 | const renderSectionHeaderProps = shouldRenderSectionHeaders
112 | ? { renderSectionHeader: renderers.renderSectionHeader }
113 | : {};
114 |
115 | const tree = renderer.create(
116 | ,
121 | ).toJSON();
122 | expect(tree).toMatchSnapshot();
123 | },
124 |
125 | expectVirtualizedToMatchSnapshotWithData(immutableData) {
126 | const tree = renderer.create(
127 | ,
131 | ).toJSON();
132 | expect(tree).toMatchSnapshot();
133 | },
134 |
135 | expectToMatchListViewWithData(immutableData, shouldRenderSectionHeaders) {
136 | const MockedImmutableListView = mocks.getImmutableListViewWithoutProps();
137 |
138 | const dataSource = new ListView.DataSource({
139 | rowHasChanged: (r1, r2) => r1 !== r2,
140 | sectionHeaderHasChanged: (s1, s2) => s1 !== s2,
141 | });
142 |
143 | const renderSectionHeaderProps = shouldRenderSectionHeaders
144 | ? { renderSectionHeader: renderers.renderSectionHeader }
145 | : {};
146 |
147 | const immutableTree = renderer.create(
148 | ,
154 | ).toJSON();
155 |
156 | const updatedDataSource = dataSource.cloneWithRows(immutableData.toJS());
157 | const regularTree = renderer.create(
158 | ,
163 | ).toJSON();
164 |
165 | expect(immutableTree).toEqual(regularTree);
166 | },
167 | };
168 |
169 | export {
170 | data,
171 | renderers,
172 | mocks,
173 | expectors,
174 | };
175 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | const isImmutableIterable = Immutable.Iterable.isIterable;
4 |
5 | const utils = {
6 |
7 | /** Contains exactly one item. */
8 | UNITARY_LIST: Immutable.List(['empty_list']),
9 |
10 | isImmutableIterable,
11 |
12 | /**
13 | * Return the keys from a set of data.
14 | *
15 | * @example
16 | * - getKeys({ foo: 'bar', baz: 'qux' }) will return [foo, baz].
17 | * - getKeys([2, 3, 5]) will return [0, 1, 2].
18 | *
19 | * @param {Immutable.Iterable} immutableData
20 | * @returns {Array} An array of keys for the data.
21 | */
22 | getKeys(immutableData) {
23 | if (__DEV__ && !isImmutableIterable(immutableData)) {
24 | console.warn(`Can't get keys: Data is not Immutable: ${JSON.stringify(immutableData)}`);
25 | }
26 |
27 | return immutableData.keySeq().toArray();
28 | },
29 |
30 | /**
31 | * Return a 2D array of row keys.
32 | *
33 | * @example
34 | * - getRowIdentities({ section1: ['row1', 'row2'], section2: ['row1'] })
35 | * will return [[0, 1], [0]].
36 | *
37 | * @param {Immutable.Iterable} immutableSectionData
38 | * @returns {Array}
39 | */
40 | getRowIdentities(immutableSectionData) {
41 | if (__DEV__ && !isImmutableIterable(immutableSectionData)) {
42 | console.warn(`Can't get row identities: Data is not Immutable: ${JSON.stringify(immutableSectionData)}`);
43 | }
44 |
45 | const sectionRowKeys = immutableSectionData.map(this.getKeys);
46 | return sectionRowKeys.valueSeq().toArray();
47 | },
48 |
49 | /**
50 | * @param {String|Number} key
51 | * @param {Immutable.Iterable|Object|Array} data
52 | * @returns {*} The value at the given key, whether the data is Immutable or not.
53 | */
54 | getValueFromKey(key, data) {
55 | return data.get ? data.get(key) : data[key];
56 | },
57 |
58 | /**
59 | * Returns true if the data would render as empty in a ListView: that is,
60 | * if it either has no items, or only section headers with no section data.
61 | */
62 | isEmptyListView(immutableData, enableEmptySections) {
63 | if (!immutableData || immutableData.isEmpty()) {
64 | return true;
65 | }
66 |
67 | if (!Immutable.Map.isMap(immutableData) || enableEmptySections) {
68 | return false;
69 | }
70 |
71 | return immutableData.every((item) => !item || item.isEmpty());
72 | },
73 |
74 | };
75 |
76 | export default utils;
77 |
--------------------------------------------------------------------------------