diff --git a/README.md b/README.md index 217fa91..703e558 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,7 @@ const data = [{title: 'Node 1', items: [{title: 'Node 1.1'}, {title: 'Node 1.2'} ### Reaching the list -`listProps` is forwarded to the underlying list, which is how you reach anything -on its surface: +`listProps` is forwarded to the underlying list: ```javascript ``` -Three groups of props reach the list, applied in this order: +Props reach the list in this order: -| Order | What | Notes | +| Order | What | | | --- | --- | --- | -| 1 | `listProps` | everything you pass, applied first | -| 2 | `extraData`, `initialNumToRender`, `style` | their own props, which win over `listProps` — but **only when you actually pass them**, so a value set through `listProps` is never erased by an absent prop | -| 3 | `data`, `renderItem`, `keyExtractor` | set by the component and not overridable. They are excluded from `IListProps`, so passing one is a compile error rather than a silent no-op | +| 1 | `listProps` | | +| 2 | `extraData`, `initialNumToRender`, `style` | win over `listProps`, and only when you pass them | +| 3 | `data`, `renderItem`, `keyExtractor` | set by the component. Excluded from `IListProps`, so passing one is a compile error | -`initialNumToRender` therefore has two spellings. Its own prop predates -`listProps` and stays supported; `listProps.initialNumToRender` is equivalent, -and the dedicated prop wins if you set both. +`initialNumToRender` has two spellings: its own prop and +`listProps.initialNumToRender`. They are equivalent, and the prop wins if you set +both. -`IListProps` is typed against `FlatList`, the default. Another `ListComponent` -with props of its own may need a cast. +`IListProps` is typed against `FlatList`. Another `ListComponent` with props of +its own may need a cast. ### NestedRow @@ -127,30 +126,19 @@ with props of its own may need a cast. | **`height`** | Fixed height for the row | number | Not required — the row sizes to its content | | **`style`** | Row container style | `StyleProp` | Not required | -#### Why levels start at 1 +#### Levels start at 1 -The nodes of `data` are at level **1**, not 0, so with the default increment a -top-level row is already indented by 10px. - -That is deliberate. `NestedRow` indents by `level * paddingLeftIncrement`, so a 0 -base would put top-level rows flush against the screen edge — changing the -appearance of every app built on the documented pattern, for no functional gain. -It is inherited from the synthetic root node the old recursive renderer wrapped -`data` in, and it is kept on purpose rather than by accident. - -If you want a flush left edge, pass `level={level - 1}` or set your own -`paddingLeftIncrement`. +The nodes of `data` are at level 1, so with the default increment a top-level row +is indented by 10px. For a flush left edge pass `level={level - 1}`, or set your +own `paddingLeftIncrement`. ### Types -Two node types are exported, because a node on the way in and a node on the way -out are not the same shape. - | Type | What it describes | | --- | --- | -| **`INode`** | a node as you write it in `data`. Every field is optional — add whatever your app needs | -| **`IRenderedNode`** | a node as `renderNode` and `onNodePressed` receive it: the `_internalId` the list assigned, and `opened` resolved to the node's current expanded state | -| **`IRow`** | one row of the flattened tree, as a custom `ListComponent` receives it in `renderItem` | +| **`INode`** | a node as you write it in `data`. Every field is optional | +| **`IRenderedNode`** | what `renderNode` and `onNodePressed` receive, where `_internalId` is a `string` and `opened` a `boolean` | +| **`IRow`** | one row of the flattened tree, as a custom `ListComponent` receives it | | **`IListProps`** | the shape of `listProps` | ```typescript @@ -165,31 +153,24 @@ const renderNode = (node: IRenderedNode, level: number, isLastLevel: boolean) => ) ``` -`getChildrenName` and `keyExtractor` are handed an `INode`, not an -`IRenderedNode`: both are called while the tree is being walked, before the list -has assigned a node anything. +`getChildrenName` and `keyExtractor` receive an `INode`. ## Performance -The tree is flattened into a single array of visible rows, and one list renders -it. Depth is a number carried on a row rather than a level of nesting in the -component tree, so: - -- there is one list, not one per node, whatever the shape of the data. Nesting - lists of the same orientation is the arrangement React Native warns against, - where windowing cannot work correctly -- the number of mounted rows is bounded by the window rather than by the size of - the tree. A 20,000-level-deep tree mounts as few rows as a flat one -- expanding and collapsing recomputes the row array instead of mounting and - unmounting lists. Rows that did not move keep their identity and are not - re-rendered -- nothing walks the tree on an ordinary render. Only a change to `data` or - `extraData` rebuilds the rows, so an inline `renderNode`, `onNodePressed` or - `getChildrenName` costs nothing - -`getChildrenName` and `keyExtractor` are read when the rows are built. If either -starts answering differently without `data` changing, change `extraData` to pick -it up. +The tree is flattened into a single array of visible rows, rendered by one list. +Depth is a number on a row rather than a level of nesting in the component tree. + +- one list, whatever the shape of the data +- the number of mounted rows is bounded by the window, not by the size of the + tree. A 20,000-level-deep tree mounts as few rows as a flat one +- expanding and collapsing recomputes the row array. Rows that did not move keep + their identity and are not re-rendered +- only a change to `data` or `extraData` rebuilds the rows, so inline + `renderNode` and `onNodePressed` callbacks cost nothing + +`getChildrenName` and `keyExtractor` are read when the rows are built. Change +`extraData` if either starts returning something different while `data` stays +the same. ### Using another list @@ -206,21 +187,17 @@ import {LegendList} from '@legendapp/list' /> ``` -Each `item` is an `IRow`, so a custom list must pass its `item` through to -`renderItem` unchanged. What else the component sets, and what `listProps` can -and cannot override, is in [Reaching the list](#reaching-the-list). +Each `item` is an `IRow`; pass it through to `renderItem` unchanged. See +[Reaching the list](#reaching-the-list) for what `listProps` can override. ### Node identity -Each node is given an `_internalId`: a path built from the node's own `id`, or -failing that its `key`, or failing that its position among its siblings. -`keyExtractor` overrides the choice. Identity is what expanded state is keyed by, -so giving nodes stable keys is what lets a node stay expanded while `data` -changes around it. +Each node gets an `_internalId`: a path built from its own `id`, or its `key`, or +its position among its siblings. `keyExtractor` overrides that. -By default a node's expanded state is forgotten once the node leaves the tree, -including while it sits inside a collapsed parent. `keepOpenedState` keeps that -state instead, so a subtree comes back expanded as it was. +Expanded state is keyed by this id, so stable keys keep a node expanded while +`data` changes around it. The state is dropped once a node leaves the tree, +including while it sits inside a collapsed parent. `keepOpenedState` keeps it. ## Examples @@ -239,13 +216,6 @@ cd -react-native-nested-listview-examples-expo npm install && npm start ``` -There is no separate bare React Native example, and there is nothing to lose by -that: this library is pure JavaScript with no native module, so it installs and -behaves identically in a bare app and in an Expo one. The -[bare examples repository](https://github.com/fjmorant/react-native-nested-listview-examples) -is archived — it targeted React Native 0.70 and no longer builds on current -toolchains, and keeping two example apps current is what let it rot. - ## Roadmap The roadmap is tracked on the [GitHub project board](https://github.com/users/fjmorant/projects/7), @@ -268,38 +238,25 @@ yarn check-package # entrypoints and types agree, across all resolution modes ### Releasing -Releasing is one button: **Actions → Publish → Run workflow**. +**Actions → Publish → Run workflow.** -It reads the version from `package.json`, refuses to go on if that version is -already tagged or already on npm, takes the release notes from that version's -CHANGELOG entry, runs the whole gate, publishes to npm with +It takes the version from `package.json` and the release notes from that +version's CHANGELOG entry, runs the checks, publishes to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements), and creates the GitHub release. -The CHANGELOG entry is required. A version with no `## ` section stops -the run before anything is published, so the release and the CHANGELOG cannot -drift apart — and a rehearsal shows the notes it would publish. +**Rehearse only** is ticked by default: it runs every check and publishes +nothing. Untick it to release. -**Rehearse only** is ticked by default, so the default action of that button -publishes nothing — it runs every check and stops. Untick it to release for -real. Either way the run's summary says plainly what did or did not happen. - -Bumping the version stays a pull request, because the CHANGELOG has to be -written by a person anyway. Everything after that point is what this automates: +The run stops before publishing if the version is already tagged, already on +npm, or has no `## ` section in the CHANGELOG. ``` -# 1. a PR bumping package.json and adding the CHANGELOG entry for it +# 1. a PR bumping package.json and adding the CHANGELOG entry # 2. merge it # 3. Actions -> Publish -> Run workflow, with Rehearse only unticked ``` -Publishing needs an `NPM_TOKEN` secret on the **`production`** environment — an -npm **automation** token, since a classic token fails against an account that -requires 2FA for publishing. The job declares that environment, so its -protection rules apply: adding required reviewers there makes a real publish -something that has to be approved, and restricting *Deployment branches and -tags* limits where it can run from. - ### Trying a local build in an app Pack the library and install the tarball into the consuming app: