react-native-render-html-pyou
Version:
React Native component that renders HTML as native views
204 lines (184 loc) • 7.2 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import { View } from 'react-native';
import shallowCompare from 'react-addons-shallow-compare';
import htmlparser2 from 'htmlparser2';
import HTMLElement from './HTMLElement';
import HTMLTextNode from './HTMLTextNode';
import HTMLRenderers from './HTMLRenderers';
import HTMLStyles from './HTMLStyles';
import { TEXT_TAG_NAMES } from './HTMLUtils';
export default class HTML extends React.Component {
/* ****************************************************************************/
// Class
/* ****************************************************************************/
static propTypes = {
html: PropTypes.string.isRequired,
htmlStyles: PropTypes.object,
containerStyle: View.propTypes.style,
onLinkPress: PropTypes.func,
imagesMaxWidth: PropTypes.number,
renderers: PropTypes.object.isRequired
}
static defaultProps = {
renderers: HTMLRenderers
}
constructor (props) {
super(props);
this.renderers = {
...HTMLRenderers,
...(this.props.renderers || {})
};
this.imgsToRender = [];
}
/* ****************************************************************************/
// Data Lifecycle
/* ****************************************************************************/
shouldComponentUpdate (nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
/* ****************************************************************************/
// Rendering
/* ****************************************************************************/
/**
* Returns an RN element from the HTML node being parsed
* @param node: object
* @param index: number
* @param groupInfo: object
* @param parentTagName: string
* @parentIsText: bool
*/
createElement (node, index, groupInfo, parentTagName, parentIsText) {
return (
<HTMLElement
key={index}
htmlStyles={this.props.htmlStyles}
imagesMaxWidth={this.props.imagesMaxWidth}
htmlAttribs={node.attribs}
tagName={node.name}
groupInfo={groupInfo}
parentTagName={parentTagName}
parentIsText={parentIsText}
onLinkPress={this.props.onLinkPress}
renderers={this.renderers}>
{this.renderHtmlAsRN(node.children, node.name, !HTMLStyles.blockElements.has(node.name))}
</HTMLElement>
);
}
/**
* Returns if a text node is worth being rendered.
* Loop on it and its children and look for actual text to display,
* if none is found, don't render it (a single img or an empty p for instance)
*/
shouldRenderNode (node) {
if (!node.children || !node.children.length) {
return false;
}
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].type === 'text') {
return true;
} else if (TEXT_TAG_NAMES.has(node.children[i].name)) {
if (this.shouldRenderNode(node.children[i])) {
return true;
} else {
continue;
}
}
}
return false;
}
/**
* Loop on a HTML node and look for imgs that need
* to be rendered outside this node (ie : img outside
* of text elements)
*/
addImgsToRenderList (node, index, groupInfo, parentTagName, parentIsText) {
if (!node.children || !node.children.length) {
return;
}
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].name === 'img') {
this.imgsToRender.push(
this.createElement(
node.children[i],
index,
groupInfo,
parentTagName,
parentIsText
)
);
}
}
}
/**
* Converts the html elements to RN elements
* @param htmlElements: the array of html elements
* @param parentTagName='body': the parent html element if any
* @param parentIsText: true if the parent element was a text-y element
* @return the equivalent RN elements
*/
renderHtmlAsRN (htmlElements, parentTagName, parentIsText) {
return htmlElements.map((node, index, list) => {
if (node.type === 'text') {
const str = HTMLTextNode.removeWhitespaceListHTML(node.data, index, parentTagName);
if (str.length) {
return (<HTMLTextNode key={index}>{str}</HTMLTextNode>);
} else {
return undefined;
}
} else if (node.type === 'tag') {
// Generate grouping info if we are a group-type element
let groupInfo;
if (node.name === 'li') {
groupInfo = {
index: htmlElements.reduce((acc, e) => {
if (e === node) {
acc.found = true;
} else if (!acc.found && e.type === 'tag' && e.name === 'li') {
acc.index++;
}
return acc;
}, {index: 0, found: false}).index,
count: htmlElements.filter((e) => e.type === 'tag' && e.name === 'li').length
};
}
let ElementsToRender;
const Element = this.createElement(node, index, groupInfo, parentTagName, parentIsText);
if (this.imgsToRender.length && !parentIsText) {
ElementsToRender = (
<View key={index}>
{ this.imgsToRender.map((img, imgIndex) => <View key={`view-${index}-image-${imgIndex}`}>{ img }</View>) }
{ Element }
</View>
);
this.imgsToRender = [];
} else {
ElementsToRender = Element;
}
if (node.name === 'img') {
this.imgsToRender.push(Element);
return false;
}
if (TEXT_TAG_NAMES.has(node.name)) {
this.addImgsToRenderList(node, index, groupInfo, parentTagName, parentIsText);
if (!this.shouldRenderNode(node)) {
return false;
}
}
return ElementsToRender;
}
})
.filter((e) => e !== undefined);
}
render () {
let rnNodes;
const parser = new htmlparser2.Parser(
new htmlparser2.DomHandler((_err, dom) => {
rnNodes = this.renderHtmlAsRN(dom, 'body', false);
})
);
parser.write(this.props.html);
parser.done();
return (<View style={this.props.containerStyle || {}}>{rnNodes}</View>);
}
}