├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── __tests__ │ ├── __data__ │ │ └── people.js │ ├── __snapshots__ │ │ ├── controlled-list-view.test.js.snap │ │ ├── group-data-by.test.js.snap │ │ └── sort-data-by.test.js.snap │ ├── controlled-list-view.test.js │ ├── group-data-by.test.js │ └── sort-data-by.test.js ├── controlled-list-view.js ├── group-data-by.js └── sort-data-by.js ├── package.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "formidable/configurations/es6-react", 4 | "plugin:flowtype/recommended" 5 | ], 6 | "plugins": [ 7 | "flowtype" 8 | ], 9 | "rules": { 10 | "quotes": ["error", "single", { "avoidEscape": true }], 11 | "func-style": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*[.]android.js 5 | 6 | # Ignore templates with `@flow` in header 7 | .*/local-cli/generator.* 8 | 9 | # Ignore malformed json 10 | .*/node_modules/y18n/test/.*\.json 11 | 12 | # Ignore the website subdir 13 | /website/.* 14 | 15 | # Ignore BUCK generated dirs 16 | /\.buckd/ 17 | 18 | # Ignore unexpected extra @providesModule 19 | .*/node_modules/commoner/test/source/widget/share.js 20 | 21 | # Ignore duplicate module providers 22 | # For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root 23 | .*/Libraries/react-native/React.js 24 | .*/Libraries/react-native/ReactNative.js 25 | .*/node_modules/jest-runtime/build/__tests__/.* 26 | 27 | [include] 28 | index.js 29 | lib/ 30 | 31 | [libs] 32 | node_modules/react-native/Libraries/react-native/react-native-interface.js 33 | node_modules/react-native/flow 34 | 35 | [options] 36 | module.system=haste 37 | 38 | esproposal.class_static_fields=enable 39 | esproposal.class_instance_fields=enable 40 | 41 | experimental.strict_type_args=true 42 | 43 | munge_underscores=true 44 | 45 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 46 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 47 | 48 | suppress_type=$FlowIssue 49 | suppress_type=$FlowFixMe 50 | suppress_type=$FixMe 51 | 52 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-2]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 53 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-2]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 54 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 55 | 56 | unsafe.enable_getters_and_setters=true 57 | 58 | [version] 59 | ^0.32.0 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store 40 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Formidable 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-controlled-listview

2 | 3 |

4 | 5 | 6 | 7 | 8 | npm version 9 | 10 | 11 | 12 |

13 | 14 |

15 | The standard React Native ListView you know and love, with a declarative Flux-friendly API 16 |

17 | 18 | *** 19 | 20 | ### Why? 21 | 22 | For performance reasons, React Native [`ListView`](https://facebook.github.io/react-native/docs/listview.html) needs a [`ListView.DataSource`](https://facebook.github.io/react-native/docs/listviewdatasource.html), so it can efficiently update itself. To benefit from these optimisations, any component wishing to render a `ListView` needs to be stateful to hold the DataSource, and faff about with lifecycle methods to update it. 23 | 24 | This library hides that statefulness and provides a simple, props-based API to render ListViews. 25 | 26 | ### How-to 27 | 28 | Installation: 29 | ``` 30 | npm i --save react-native-controlled-listview 31 | ``` 32 | 33 | Instead of `dataSource`, controlled `ListView` expects an array prop `items`. Optionally, you can sort the list with `sortBy` or group it into sections with `sectionBy`: 34 | 35 | ```diff 36 | - import { ListView } from 'react-native'; 37 | + import ListView from 'react-native-controlled-listview'; 38 | 39 | // stateless function component 40 | export default (props) => ( 41 | + person.lastName} 45 | + sectionBy={(person) => person.lastName[0]} 46 | renderRow={(person) => ( 47 | {person.lastName}, {person.firstName} 48 | )} 49 | renderSectionHeader={(sectionData, initial) => ( 50 | {initial} 51 | )} 52 | /> 53 | ); 54 | ``` 55 | 56 | ## Immutability 57 | 58 | There is one gotcha. This component **expects you to clone the `items` prop** when you want to ListView to update. If you are using Redux, this should already be the case. 59 | 60 | The `items` prop can be an instance of `Immutable.List`, or an array. If using plain arrays, never mutate it in-place, or the ListView won't update. 61 | 62 | See [`dataSourceShouldUpdate`](#datasourceshouldupdate--prevprops-nextprops--boolean) on how to customise the update logic. 63 | 64 | ## Props 65 | 66 | ##### `items : any[] | Immutable.List` **(required)** 67 | 68 | List data source. 69 | 70 | ##### `sortBy : (a, b) => number | boolean` 71 | 72 | Sorts the list based on a comparator. Value can be one of type: 73 | * `(a, b) => number` a standard [`Array#sort compareFunction.`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) 74 | * `(a, b) => boolean` a shorthand comparator: if returns true, `a` comes first; if false, `b` comes first. 75 | 76 | ##### `sectionBy : (a, b) => string` 77 | 78 | Groups the list based on returned value and renders section headers for each group. 79 | 80 | If using `sectionBy`, you must also provide [`renderSectionHeader`](https://facebook.github.io/react-native/docs/listview.html#rendersectionheader) 81 | 82 | ##### `rowHasChanged : (prevItem, nextItem) => boolean` 83 | 84 | Passed directly to [`ListView.DataSource`](https://facebook.github.io/react-native/docs/listviewdatasource.html). constructor. Defaults to `!Immutable.is(prevItem, nextItem)`, which performs a `===` comparison for plain objects. 85 | 86 | ##### `sectionHeaderHasChanged : (prevSectionData, nextSectionData) => boolean` 87 | 88 | Passed directly to [`ListView.DataSource`](https://facebook.github.io/react-native/docs/listviewdatasource.html). constructor. Defaults to `prev !== next`. 89 | 90 | ##### `dataSourceShouldUpdate : (prevProps, nextProps) => boolean` 91 | 92 | Controls when the data source should be updated. The default implementation is `!Immutable.is(prevProps.items, nextProps.items)`, which performs a `===` comparison for plain arrays. 93 | 94 | ##### [`...ListView.props`](https://facebook.github.io/react-native/docs/listview.html#props) 95 | 96 | All other properties, except `dataSource` are passed directly to the underlying ListView. 97 | 98 | ## Please note 99 | 100 | This project is in a pre-release state. The API may be considered relatively stable, but changes may still occur. 101 | 102 | [MIT licensed](LICENSE) 103 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import ControlledListView from './lib/controlled-list-view'; 4 | export default ControlledListView; 5 | -------------------------------------------------------------------------------- /lib/__tests__/__data__/people.js: -------------------------------------------------------------------------------- 1 | export default Object.freeze([ 2 | { firstName: 'Orin', lastName: 'Incandenza' }, 3 | { firstName: 'Don', lastName: 'Gately' }, 4 | { firstName: 'Avril', lastName: 'Incandenza' }, 5 | { firstName: 'Michael', lastName: 'Pemulis' }, 6 | { firstName: 'Hal', lastName: 'Incandenza' }, 7 | { firstName: 'Mildred', lastName: 'Bonk' } 8 | ]); 9 | -------------------------------------------------------------------------------- /lib/__tests__/__snapshots__/controlled-list-view.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test handles items of type Immutable.List 1`] = ` 2 | 34 | 38 | B 39 | 40 | 44 | Bonk, Mildred 45 | 46 | 50 | G 51 | 52 | 56 | Gately, Don 57 | 58 | 62 | I 63 | 64 | 68 | Incandenza, Hal 69 | 70 | 74 | Incandenza, Avril 75 | 76 | 80 | Incandenza, Orin 81 | 82 | 86 | P 87 | 88 | 92 | Pemulis, Michael 93 | 94 | 95 | `; 96 | 97 | exports[`test handles items of type Immutable.List 2`] = ` 98 | 127 | 131 | I 132 | 133 | 137 | Incandenza, Orin 138 | 139 | 140 | `; 141 | 142 | exports[`test renders items in correct sort order 1`] = ` 143 | 167 | 171 | Bonk, Mildred 172 | 173 | 177 | Gately, Don 178 | 179 | 183 | Incandenza, Hal 184 | 185 | 189 | Incandenza, Avril 190 | 191 | 195 | Incandenza, Orin 196 | 197 | 201 | Pemulis, Michael 202 | 203 | 204 | `; 205 | 206 | exports[`test splits items into sections 1`] = ` 207 | 239 | 243 | B 244 | 245 | 249 | Bonk, Mildred 250 | 251 | 255 | G 256 | 257 | 261 | Gately, Don 262 | 263 | 267 | I 268 | 269 | 273 | Incandenza, Hal 274 | 275 | 279 | Incandenza, Avril 280 | 281 | 285 | Incandenza, Orin 286 | 287 | 291 | P 292 | 293 | 297 | Pemulis, Michael 298 | 299 | 300 | `; 301 | 302 | exports[`test updates list when items prop is updated 1`] = ` 303 | 327 | 331 | Incandenza, Orin 332 | 333 | 337 | Gately, Don 338 | 339 | 343 | Incandenza, Avril 344 | 345 | 349 | Pemulis, Michael 350 | 351 | 355 | Incandenza, Hal 356 | 357 | 361 | Bonk, Mildred 362 | 363 | 364 | `; 365 | 366 | exports[`test updates list when items prop is updated 2`] = ` 367 | 391 | 395 | Incandenza, Orin 396 | 397 | 398 | `; 399 | -------------------------------------------------------------------------------- /lib/__tests__/__snapshots__/group-data-by.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test groups items based on the provided function 1`] = ` 2 | Object { 3 | "B": Array [ 4 | Object { 5 | "firstName": "Mildred", 6 | "lastName": "Bonk", 7 | }, 8 | ], 9 | "G": Array [ 10 | Object { 11 | "firstName": "Don", 12 | "lastName": "Gately", 13 | }, 14 | ], 15 | "I": Array [ 16 | Object { 17 | "firstName": "Orin", 18 | "lastName": "Incandenza", 19 | }, 20 | Object { 21 | "firstName": "Avril", 22 | "lastName": "Incandenza", 23 | }, 24 | Object { 25 | "firstName": "Hal", 26 | "lastName": "Incandenza", 27 | }, 28 | ], 29 | "P": Array [ 30 | Object { 31 | "firstName": "Michael", 32 | "lastName": "Pemulis", 33 | }, 34 | ], 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /lib/__tests__/__snapshots__/sort-data-by.test.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test sorts items when given a boolean array comparator 1`] = ` 2 | Array [ 3 | Object { 4 | "firstName": "Mildred", 5 | "lastName": "Bonk", 6 | }, 7 | Object { 8 | "firstName": "Don", 9 | "lastName": "Gately", 10 | }, 11 | Object { 12 | "firstName": "Hal", 13 | "lastName": "Incandenza", 14 | }, 15 | Object { 16 | "firstName": "Avril", 17 | "lastName": "Incandenza", 18 | }, 19 | Object { 20 | "firstName": "Orin", 21 | "lastName": "Incandenza", 22 | }, 23 | Object { 24 | "firstName": "Michael", 25 | "lastName": "Pemulis", 26 | }, 27 | ] 28 | `; 29 | 30 | exports[`test sorts items when given a standard array comparator 1`] = ` 31 | Array [ 32 | Object { 33 | "firstName": "Mildred", 34 | "lastName": "Bonk", 35 | }, 36 | Object { 37 | "firstName": "Don", 38 | "lastName": "Gately", 39 | }, 40 | Object { 41 | "firstName": "Orin", 42 | "lastName": "Incandenza", 43 | }, 44 | Object { 45 | "firstName": "Avril", 46 | "lastName": "Incandenza", 47 | }, 48 | Object { 49 | "firstName": "Hal", 50 | "lastName": "Incandenza", 51 | }, 52 | Object { 53 | "firstName": "Michael", 54 | "lastName": "Pemulis", 55 | }, 56 | ] 57 | `; 58 | -------------------------------------------------------------------------------- /lib/__tests__/controlled-list-view.test.js: -------------------------------------------------------------------------------- 1 | /*global it, expect*/ 2 | /*eslint-disable react/no-multi-comp*/ 3 | import { Text } from 'react-native'; 4 | import { fromJS } from 'immutable'; 5 | import { cloneDeep } from 'lodash'; 6 | import React from 'react'; 7 | import ControlledListView from '../controlled-list-view'; 8 | import people from './__data__/people'; 9 | import renderer from 'react-test-renderer'; 10 | 11 | const renderRow = (person) => ( 12 | {`${person.lastName}, ${person.firstName}`} 13 | ); 14 | 15 | const renderSectionHeader = (sectionData, initial) => ( 16 | {initial} 17 | ); 18 | 19 | // helper to update component props 20 | const setProps = (component, changedProps) => { 21 | const instance = component.getInstance(); 22 | const nextProps = { 23 | ...instance.props, 24 | ...changedProps 25 | }; 26 | instance.componentWillReceiveProps(nextProps); 27 | instance.props = nextProps; 28 | }; 29 | 30 | it('updates list when items prop is updated', () => { 31 | const component = renderer.create( 32 | 33 | ); 34 | expect(component.toJSON()).toMatchSnapshot(); 35 | 36 | setProps(component, { items: [people[0]] }); 37 | expect(component.toJSON()).toMatchSnapshot(); 38 | }); 39 | 40 | it('doesn\'t update list when items are mutated in-place', () => { 41 | const items = cloneDeep(people); 42 | 43 | const component = renderer.create( 44 | 45 | ); 46 | 47 | const initialSnapshot = component.toJSON(); 48 | 49 | // mutate array in-place 50 | items[0].firstName = 'Poor Tony'; 51 | items[0].lastName = 'Krause'; 52 | items.pop(); 53 | 54 | // expected: no change 55 | setProps(component, { items }); 56 | expect(component.toJSON()).toEqual(initialSnapshot); 57 | }); 58 | 59 | it('handles items of type Immutable.List', () => { 60 | const list = fromJS(people); 61 | const component = renderer.create( 62 | a.get('lastName') < b.get('lastName')} 65 | sectionBy={(person) => person.get('lastName')[0]} 66 | renderRow={(person) => renderRow(person.toJS())} 67 | renderSectionHeader={renderSectionHeader} 68 | /> 69 | ); 70 | expect(component.toJSON()).toMatchSnapshot(); 71 | 72 | setProps(component, { items: list.take(1) }); 73 | expect(component.toJSON()).toMatchSnapshot(); 74 | }); 75 | 76 | it('renders items in correct sort order', () => { 77 | const tree = renderer.create( 78 | a.lastName < b.lastName} 81 | renderRow={renderRow} 82 | /> 83 | ).toJSON(); 84 | expect(tree).toMatchSnapshot(); 85 | }); 86 | 87 | it('splits items into sections', () => { 88 | const tree = renderer.create( 89 | a.lastName < b.lastName} 92 | sectionBy={(person) => person.lastName[0]} 93 | renderRow={renderRow} 94 | renderSectionHeader={renderSectionHeader} 95 | /> 96 | ).toJSON(); 97 | expect(tree).toMatchSnapshot(); 98 | }); 99 | -------------------------------------------------------------------------------- /lib/__tests__/group-data-by.test.js: -------------------------------------------------------------------------------- 1 | /*global it, expect*/ 2 | import 'react-native'; 3 | import groupDataBy from '../group-data-by'; 4 | import people from './__data__/people'; 5 | 6 | it('groups items based on the provided function', () => { 7 | expect(groupDataBy((person) => person.lastName[0], people)).toMatchSnapshot(); 8 | }); 9 | -------------------------------------------------------------------------------- /lib/__tests__/sort-data-by.test.js: -------------------------------------------------------------------------------- 1 | /*global it, expect*/ 2 | import 'react-native'; 3 | import sortDataBy from '../sort-data-by'; 4 | import people from './__data__/people'; 5 | 6 | it('sorts items when given a standard array comparator', () => { 7 | const comparator = (a, b) => { 8 | if (a.lastName > b.lastName) { 9 | return +1; 10 | } 11 | if (b.lastName < b.lastName) { 12 | return -1; 13 | } 14 | return 0; 15 | }; 16 | 17 | expect(sortDataBy(comparator, people)).toMatchSnapshot(); 18 | }); 19 | 20 | it('sorts items when given a boolean array comparator', () => { 21 | const comparator = (a, b) => { 22 | return a.lastName < b.lastName; 23 | }; 24 | 25 | expect(sortDataBy(comparator, people)).toMatchSnapshot(); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/controlled-list-view.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ListView } from 'react-native'; 4 | import is from 'immutable-is'; 5 | 6 | import groupDataBy from './group-data-by'; 7 | import sortDataBy from './sort-data-by'; 8 | 9 | const isImmutableList = (items) => items && typeof items.toArray === 'function'; 10 | const arrayOrImmutableListPropTypeValidator = (props, propName, componentName) => { 11 | const items = props[propName]; 12 | return (!Array.isArray(items) && !isImmutableList(items)) 13 | ? new Error(`Invalid prop ${propName} passed to ${componentName}.` + 14 | 'Expected Array or Immutable.List') 15 | : undefined; 16 | }; 17 | 18 | class ControlledListView extends Component { 19 | 20 | constructor(props) { 21 | super(props); 22 | const dataSource = new ListView.DataSource({ 23 | rowHasChanged: props.rowHasChanged, 24 | sectionHeaderHasChanged: props.sectionHeaderHasChanged 25 | }); 26 | 27 | this.state = { 28 | dataSource: this.updateDataSource(dataSource, props) 29 | }; 30 | } 31 | 32 | componentWillReceiveProps(nextProps) { 33 | if (nextProps.dataSourceShouldUpdate(this.props, nextProps)) { 34 | this.setState({ 35 | dataSource: this.updateDataSource(this.state.dataSource, nextProps) 36 | }); 37 | } 38 | } 39 | 40 | updateDataSource(dataSource, { items, sortBy, sectionBy }) { 41 | let data = isImmutableList(items) ? items.toArray() : items; 42 | if (sortBy) { 43 | data = sortDataBy(sortBy, data); 44 | } 45 | if (sectionBy) { 46 | const grouped = groupDataBy(sectionBy, data); 47 | return dataSource.cloneWithRowsAndSections(grouped); 48 | } else { 49 | return dataSource.cloneWithRows(data); 50 | } 51 | } 52 | 53 | render() { 54 | //eslint-disable-next-line no-unused-vars 55 | const { items, sortBy, sectionBy, ...listViewProps } = this.props; 56 | const { dataSource } = this.state; 57 | return ( 58 | 59 | ); 60 | } 61 | } 62 | 63 | ControlledListView.propTypes = { 64 | dataSourceShouldUpdate: PropTypes.func, 65 | items: arrayOrImmutableListPropTypeValidator, 66 | rowHasChanged: PropTypes.func, 67 | sectionBy: PropTypes.func, 68 | sectionHeaderHasChanged: PropTypes.func, 69 | sortBy: PropTypes.func 70 | }; 71 | 72 | ControlledListView.defaultProps = { 73 | rowHasChanged: (a, b) => !is(a, b), 74 | sectionHeaderHasChanged: (a, b) => a !== b, 75 | dataSourceShouldUpdate: (prevProps, nextProps) => !is(prevProps.items, nextProps.items) 76 | }; 77 | 78 | export default ControlledListView; 79 | -------------------------------------------------------------------------------- /lib/group-data-by.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type GroupComparator = (a: any) => string; 4 | 5 | export default function groupDataBy(groupBy: GroupComparator, data: any[]): {} { 6 | return data.reduce((grouped, item) => { 7 | const key = groupBy(item); 8 | return { 9 | ...grouped, 10 | [key]: (grouped[key] || []).concat(item) 11 | }; 12 | }, {}); 13 | } 14 | -------------------------------------------------------------------------------- /lib/sort-data-by.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | type SortComparator = (a: any, b: any) => number | boolean; 4 | 5 | export default function sortDataBy(sortBy: SortComparator, data: any[]): any[] { 6 | return data.slice(0).sort((a, b) => { 7 | const result = sortBy(a, b); 8 | if (typeof result === 'boolean') { 9 | return result ? -1 : 1; 10 | } else if (typeof result === 'number') { 11 | return result; 12 | } else { 13 | throw new Error(`Comparator returned an unexpected value ${result}`); 14 | } 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-controlled-listview", 3 | "version": "0.2.0", 4 | "description": "The standard React Native ListView with a declarative API", 5 | "keywords": [ 6 | "react", 7 | "react-component", 8 | "react-native", 9 | "ios", 10 | "android", 11 | "listview" 12 | ], 13 | "main": "index.js", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/FormidableLabs/react-native-controlled-listview.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/FormidableLabs/react-native-controlled-listview/issues" 20 | }, 21 | "homepage": "https://github.com/FormidableLabs/react-native-controlled-listview#readme", 22 | "author": "Jani Eväkallio", 23 | "license": "MIT", 24 | "scripts": { 25 | "lint": "eslint index.js lib", 26 | "flow": "flow check", 27 | "test": "jest", 28 | "prerelease": "npm run lint && npm run flow && npm test" 29 | }, 30 | "jest": { 31 | "preset": "jest-react-native", 32 | "testPathIgnorePatterns": [ 33 | "node_modules", 34 | "__data__" 35 | ] 36 | }, 37 | "dependencies": { 38 | "immutable-is": "^3.7.6" 39 | }, 40 | "devDependencies": { 41 | "babel-eslint": "^6.0.2", 42 | "babel-jest": "^16.0.0", 43 | "babel-preset-react-native": "^1.9.0", 44 | "eslint": "^2.10.2", 45 | "eslint-config-formidable": "^2.0.1", 46 | "eslint-plugin-babel": "^3.2.0", 47 | "eslint-plugin-filenames": "^1.1.0", 48 | "eslint-plugin-flowtype": "^2.19.0", 49 | "eslint-plugin-import": "^1.16.0", 50 | "eslint-plugin-react": "^6.0.0", 51 | "flow-bin": "^0.32.0", 52 | "immutable": "^3.8.1", 53 | "jest": "^16.0.1", 54 | "jest-react-native": "^16.0.0", 55 | "lodash": "^4.16.4", 56 | "prop-types": "^15.6.0", 57 | "react": "15.3.2", 58 | "react-native": "0.34.0", 59 | "react-test-renderer": "^15.3.2" 60 | } 61 | } 62 | --------------------------------------------------------------------------------