react-native-json-tree
Version:
React Native JSON viewing component, based on react-json-tree
396 lines (361 loc) • 9.99 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { Text, View } from 'react-native'
import JSONArrow from './JSONArrow'
import getCollectionEntries from './getCollectionEntries'
import ItemRange from './ItemRange'
import objType from './objType'
// Returns the "n Items" string for this node,
// generating and caching it if it hasn't been created yet.
function createObjectItemString(data) {
const len = Object.getOwnPropertyNames(data).length
return `${len} ${len !== 1 ? 'keys' : 'key'}`
}
// Configures <JSONNestedNode> to render an Object
export const JSONObjectNode = ({ data, ...props }) => (
<JSONNestedNode
{...props}
createItemString={createObjectItemString}
data={data}
expandable={Object.getOwnPropertyNames(data).length > 0}
nodeType="Object"
nodeTypeIndicator="{}"
/>
)
JSONObjectNode.propTypes = {
data: PropTypes.object,
}
/**
* Renders simple values (eg. strings, numbers, booleans, etc)
*/
const JSONValueNode = ({
nodeType,
styling,
labelRenderer,
keyPath,
valueRenderer,
value,
valueGetter = (value) => value,
}) => (
<View {...styling('value', nodeType, keyPath)}>
<Text {...styling(['label', 'valueLabel'], nodeType, keyPath)}>
{labelRenderer(keyPath, nodeType, false, false)}
</Text>
<Text {...styling('valueText', nodeType, keyPath)}>
{valueRenderer(valueGetter(value), value, ...keyPath)}
</Text>
</View>
)
JSONValueNode.propTypes = {
keyPath: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
).isRequired,
labelRenderer: PropTypes.func.isRequired,
nodeType: PropTypes.string.isRequired,
styling: PropTypes.func.isRequired,
value: PropTypes.any,
valueGetter: PropTypes.func,
valueRenderer: PropTypes.func.isRequired,
}
export const JSONNode = ({
getItemString,
isCustomNode,
keyPath,
labelRenderer,
styling,
value,
valueRenderer,
...rest
}) => {
const nodeType = isCustomNode(value) ? 'Custom' : objType(value)
const key = keyPath[0]
const simpleNodeProps = {
getItemString,
keyPath,
labelRenderer,
nodeType,
styling,
value,
valueRenderer,
}
const nestedNodeProps = {
...rest,
...simpleNodeProps,
data: value,
isCustomNode,
}
switch (nodeType) {
case 'Object':
case 'Error':
return <JSONObjectNode key={key} {...nestedNodeProps} />
case 'Array':
return <JSONArrayNode key={key} {...nestedNodeProps} />
case 'Iterable':
return <JSONIterableNode key={key} {...nestedNodeProps} />
case 'String':
return (
<JSONValueNode key={key} {...simpleNodeProps} valueGetter={(raw) => `"${raw}"`} />
)
case 'Number':
return <JSONValueNode key={key} {...simpleNodeProps} />
case 'Boolean':
return (
<JSONValueNode
key={key}
{...simpleNodeProps}
valueGetter={(raw) => (raw ? 'true' : 'false')}
/>
)
case 'Date':
return (
<JSONValueNode
key={key}
{...simpleNodeProps}
valueGetter={(raw) => raw.toISOString()}
/>
)
case 'Null':
return <JSONValueNode key={key} {...simpleNodeProps} valueGetter={() => 'null'} />
case 'Undefined':
return (
<JSONValueNode key={key} {...simpleNodeProps} valueGetter={() => 'undefined'} />
)
case 'Function':
case 'Symbol':
return (
<JSONValueNode
key={key}
{...simpleNodeProps}
valueGetter={(raw) => raw.toString()}
/>
)
case 'Custom':
return <JSONValueNode key={key} {...simpleNodeProps} />
default:
return null
}
}
JSONNode.propTypes = {
getItemString: PropTypes.func.isRequired,
isCustomNode: PropTypes.func.isRequired,
keyPath: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
).isRequired,
labelRenderer: PropTypes.func.isRequired,
styling: PropTypes.func.isRequired,
value: PropTypes.any,
valueRenderer: PropTypes.func.isRequired,
}
/**
* Renders nested values (eg. objects, arrays, lists, etc.)
*/
function renderChildNodes(props, from, to) {
const {
nodeType,
data,
collectionLimit,
circularCache,
keyPath,
postprocessValue,
sortObjectKeys,
} = props
const childNodes = []
const collectionEntries = getCollectionEntries(
nodeType,
data,
sortObjectKeys,
collectionLimit,
from,
to,
)
collectionEntries.forEach((entry) => {
if (entry.to) {
childNodes.push(
<ItemRange
{...props}
key={`ItemRange--${entry.from}-${entry.to}`}
from={entry.from}
to={entry.to}
renderChildNodes={renderChildNodes}
/>,
)
} else {
const { key, value } = entry
const isCircular = circularCache.indexOf(value) !== -1
const node = (
<JSONNode
{...props}
{...{ postprocessValue, collectionLimit }}
key={`Node--${key}`}
keyPath={[key, ...keyPath]}
value={postprocessValue(value)}
circularCache={[...circularCache, value]}
isCircular={isCircular}
hideRoot={false}
/>
)
if (node !== false) {
childNodes.push(node)
}
}
})
return childNodes
}
export class JSONNestedNode extends React.PureComponent {
static propTypes = {
createItemString: PropTypes.func.isRequired,
collectionLimit: PropTypes.number,
data: PropTypes.any,
expandable: PropTypes.bool,
getItemString: PropTypes.func.isRequired,
hideRoot: PropTypes.bool.isRequired,
isCircular: PropTypes.bool,
keyPath: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
).isRequired,
labelRenderer: PropTypes.func.isRequired,
level: PropTypes.number.isRequired,
nodeType: PropTypes.string.isRequired,
nodeTypeIndicator: PropTypes.any,
shouldExpandNode: PropTypes.func,
sortObjectKeys: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
styling: PropTypes.func.isRequired,
}
static defaultProps = {
circularCache: [],
data: [],
expandable: true,
level: 0,
}
constructor(props) {
super(props)
// calculate individual node expansion if necessary
const expanded =
props.shouldExpandNode &&
!props.isCircular &&
props.shouldExpandNode(props.keyPath, props.data, props.level)
this.state = {
createdChildNodes: false,
expanded,
}
}
handlePress = () => this.setState({ expanded: !this.state.expanded })
render() {
const {
getItemString,
nodeTypeIndicator,
nodeType,
data,
hideRoot,
createItemString,
styling,
collectionLimit,
keyPath,
labelRenderer,
expandable,
} = this.props
const { expanded } = this.state
const renderedChildren =
expanded || (hideRoot && this.props.level === 0)
? renderChildNodes({ ...this.props, level: this.props.level + 1 })
: null
const itemType = (
<Text {...styling('nestedNodeItemType', expanded)}>
{nodeTypeIndicator}
</Text>
)
const renderedItemString = getItemString(
nodeType,
data,
itemType,
createItemString(data, collectionLimit),
)
const stylingArgs = [keyPath, nodeType, expanded, expandable]
const onPressItemString = expandable ? this.handlePress : () => {}
return hideRoot ? (
<View {...styling('rootNode', ...stylingArgs)}>
<View {...styling('rootNodeChildren', ...stylingArgs)}>
{renderedChildren}
</View>
</View>
) : (
<View {...styling('nestedNode', ...stylingArgs)}>
<View style={{ flexDirection: 'row' }}>
{expandable && (
<JSONArrow
styling={styling}
nodeType={nodeType}
expanded={expanded}
onPress={this.handlePress}
/>
)}
<Text
{...styling(['label', 'nestedNodeLabel'], ...stylingArgs)}
onPress={onPressItemString}
>
{labelRenderer(...stylingArgs)}
</Text>
<Text
{...styling('nestedNodeItemString', ...stylingArgs)}
onPress={onPressItemString}
>
{renderedItemString}
</Text>
</View>
{expanded ? (
<View {...styling('nestedNodeChildren', ...stylingArgs)}>
{renderedChildren}
</View>
) : null}
</View>
)
}
}
// Returns the "n Items" string for this node,
// generating and caching it if it hasn't been created yet.
function createIterableItemString(data, limit) {
let count = 0
let hasMore = false
if (Number.isSafeInteger(data.size)) {
count = data.size
} else {
for (const entry of data) {
// eslint-disable-line no-unused-vars
if (limit && count + 1 > limit) {
hasMore = true
break
}
count += 1
}
}
return `${hasMore ? '>' : ''}${count} ${count !== 1 ? 'entries' : 'entry'}`
}
// Configures <JSONNestedNode> to render an iterable
export function JSONIterableNode({ ...props }) {
return (
<JSONNestedNode
{...props}
nodeType="Iterable"
nodeTypeIndicator="()"
createItemString={createIterableItemString}
/>
)
}
// Returns the "n Items" string for this node,
// generating and caching it if it hasn't been created yet.
function createItemString(data) {
return `${data.length} ${data.length !== 1 ? 'items' : 'item'}`;
}
// Configures <JSONNestedNode> to render an Array
export const JSONArrayNode = ({ data, ...props }) =>
<JSONNestedNode
{...props}
createItemString={createItemString}
data={data}
expandable={data.length > 0}
nodeType="Array"
nodeTypeIndicator="[]"
/>;
JSONArrayNode.propTypes = {
data: PropTypes.array,
};