@nstudio/ui-collectionview
Version:
Customized NativeScript CollectionView for high performance lists. Supports vertical and horizontal modes, templating, and more.
151 lines • 7.01 kB
JavaScript
import * as React from 'react';
import { NSVRoot, render as RNSRender, registerElement, unmountComponentAtNode } from 'react-nativescript';
export function registerCollectionView() {
registerElement('collectionView', () => require('..').CollectionView);
}
/**
* A React wrapper around the NativeScript CollectionView component.
* @see https://docs.nativescript.org/ui/ns-ui-widgets/list-view
* @module ui/list-view/list-view
*/
export class _CollectionView extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
this.argsViewToRootKeyAndRef = new Map();
this.roots = new Set();
/**
* CollectionView code-behind:
* @see https://github.com/NativeScript/nativescript-sdk-examples-js/blob/master/app/ns-ui-widgets-category/list-view/code-behind/code-behind-ts-page.ts
* CollectionView item templates:
* @see https://medium.com/@alexander.vakrilov/faster-nativescript-CollectionView-with-multiple-item-templates-8f903a32e48f
* Cell state in CollectionView:
* @see https://medium.com/@alexander.vakrilov/managing-component-state-in-nativescript-CollectionView-b139e45d899b
* @see https://github.com/NativeScript/nativescript-angular/issues/1245#issuecomment-393465035
* loadMoreItems:
* @see https://github.com/NativeScript/nativescript-sdk-examples-js/blob/master/app/ns-ui-widgets-category/list-view/events/events-ts-page.ts
*/
this.defaultOnItemLoading = (args) => {
const { logLevel, onCellRecycle, onCellFirstLoad } = this.props._debug;
const { items, itemTemplateSelector } = this.props;
const item = _CollectionView.isItemsSource(items) ? items.getItem(args.index) : items[args.index];
const template = itemTemplateSelector
? typeof itemTemplateSelector === 'string'
? itemTemplateSelector
: itemTemplateSelector(item, args.index, items)
: null;
const cellFactory = template === null ? this.props.cellFactory : this.props.cellFactories ? this.props.cellFactories.get(template).cellFactory : this.props.cellFactory;
if (typeof cellFactory === 'undefined') {
console.warn(`CollectionView: No cell factory found, given template ${template}!`);
return;
}
const view = args.view;
if (!view) {
const rootKeyAndRef = this.renderNewRoot(item, cellFactory);
args.view = rootKeyAndRef.nativeView;
/* Here we're re-using the ref - I assume this is best practice. If not, we can make a new one on each update instead. */
this.argsViewToRootKeyAndRef.set(args.view, rootKeyAndRef);
if (onCellFirstLoad)
onCellFirstLoad(rootKeyAndRef.nativeView);
}
else {
if (onCellRecycle)
onCellRecycle(view);
const { rootKey, nativeView } = this.argsViewToRootKeyAndRef.get(view);
if (typeof rootKey === 'undefined') {
console.error('Unable to find root key that args.view corresponds to!', view);
return;
}
if (!nativeView) {
console.error('Unable to find ref that args.view corresponds to!', view);
return;
}
// args.view = null;
RNSRender(cellFactory(item), null, () => {
// console.log(`Rendered into cell! detachedRootRef:`);
}, rootKey);
}
};
this.renderNewRoot = (item, cellFactory) => {
const node = this.getNativeView();
if (!node) {
throw new Error('Unable to get ref to CollectionView');
}
const rootKey = `CollectionView-${node._domId}-${this.roots.size.toString()}`;
const root = new NSVRoot();
RNSRender(cellFactory(item), root, () => {
// console.log(`Rendered into cell! ref:`);
}, rootKey);
this.roots.add(rootKey);
return {
rootKey,
nativeView: root.baseRef.nativeView
};
};
this.state = {
nativeCells: {},
nativeCellToItemIndex: new Map(),
itemIndexToNativeCell: props._debug.logLevel === 'debug' ? new Map() : undefined
};
}
getNativeView() {
const ref = this.props.forwardedRef || this.myRef;
return ref.current ? ref.current.nativeView : null;
}
componentDidMount() {
const node = this.getNativeView();
if (!node) {
console.warn('React ref to NativeScript View lost, so unable to set item templates.');
return;
}
/* NOTE: does not support updating of this.props.cellFactories upon Props update. */
if (this.props.cellFactories) {
const itemTemplates = [];
this.props.cellFactories.forEach((info, key) => {
const { placeholderItem, cellFactory } = info;
itemTemplates.push({
key,
createView: () => {
const rootKeyAndRef = this.renderNewRoot(placeholderItem, cellFactory);
this.argsViewToRootKeyAndRef.set(rootKeyAndRef.nativeView, rootKeyAndRef);
return rootKeyAndRef.nativeView;
}
});
});
node.itemTemplates = itemTemplates;
}
}
componentWillUnmount() {
this.roots.forEach((root) => unmountComponentAtNode(root));
}
static isItemsSource(arr) {
/**
* Same implementation as used in official ListPicker component:
* @see https://github.com/NativeScript/NativeScript/blob/b436ecde3605b695a0ffa1757e38cc094e2fe311/tns-core-modules/ui/list-picker/list-picker-common.ts#L74
*/
return typeof arr.getItem === 'function';
}
render() {
const {
// Only used by the class component; not the JSX element.
forwardedRef,
//@ts-ignore
children, _debug, cellFactories, cellFactory, ...rest } = this.props;
if (children) {
console.warn("Ignoring 'children' prop on CollectionView; not supported.");
}
return React.createElement("collectionView", { ...rest, onItemLoading: this.defaultOnItemLoading, ref: forwardedRef || this.myRef });
}
}
_CollectionView.defaultProps = {
_debug: {
logLevel: 'info',
onCellFirstLoad: undefined,
onCellRecycle: undefined
}
};
//@ts-ignore
export const CollectionView = React.forwardRef((props, ref) => (
//@ts-ignore
React.createElement(_CollectionView, { ...props, forwardedRef: ref })));
//# sourceMappingURL=index.js.map