Skip to main content

DOM Tampering

This library offers rich integration with the โ€‹htmlparser2 ecosystem. Thanks to the new โ€‹TRE, you can process DOM nodes at parse time. It implies that the operation doesn't require a tree traversal contrary to legacy versions, and thus adds up very little overhead.

caution

One important downside is that a DOM node next sibling won't have been parsed yet. However, the children and parent of this node are guaranteed to be attached to this node.

Altering the DOM Tree#

The API offers one prop, โ€‹domVisitors, which is a record of 3 optional fields:

โ€‹onDocument

Triggered when the root DOM โ€‹Document has been parsed, at the very end of the parsing phase.

โ€‹onElement

Triggered when a DOM โ€‹Element has been parsed along with its children.

โ€‹onText

Triggered when a DOM โ€‹Text node has been parsed along with its content.

Those callback should not return anything. Instead, you should change the node or its children in place, or just read its content for inspection purposes.

tip

You are invited to use โ€‹domutils library to handle DOM querying and manipulation (see example below). This library is already a direct dependency of โ€‹htmlparser2.

Example: Removing Elements#

DOM Visitor to Remove First Two Children of an Ol tag
import React from 'react';
import { useWindowDimensions } from 'react-native';
import RenderHtml from 'react-native-render-html';
import { removeElement, isTag } from 'domutils';
function onElement(element) {
// Remove the first two children of an ol tag.
if (element.tagName === 'ol') {
let i = 0;
for (const child of element.children) {
// Children might be text node or comments.
// We don't want to count these.
if (isTag(child) && i < 2) {
removeElement(child);
i++;
}
}
}
}
const source = {
html: `
<ol>
<li>One</li>
<li>Two</li>
<li>Three</li>
<li>Four</li>
</ol>
`
};
const domVisitors = {
onElement: onElement
};
export default function App() {
const { width } = useWindowDimensions();
return (
<RenderHtml
contentWidth={width}
source={source}
domVisitors={domVisitors}
/>
);
}
Usage of domVisitors.onElement to remove the first two children of ol tags thanks to "removeElement" from domutils. Note that "isTag" from domutils is used to check if the children is a DOM element.
Press on the button below to show how this code renders on your device.

Example: Altering Data#

Altering Text Data
import React from 'react';
import { useWindowDimensions } from 'react-native';
import RenderHtml from 'react-native-render-html';
function onText(text) {
text.data = text.data.replace(/\{\{userName\}\}/g, 'John Doe');
}
const source = {
html: `
<p style="text-align: center;">
Hi <strong>{{userName}}</strong>
you have news today!
</p>
`
};
const domVisitors = {
onText: onText
};
export default function App() {
const { width } = useWindowDimensions();
return (
<RenderHtml
contentWidth={width}
source={source}
domVisitors={domVisitors}
/>
);
}
Usage of domVisitors.onText to resolve handlebar-style variable in text nodes data. Note that this example might cost render time on very big documents since it will apply to every text node. You might want to add conditional logic to target text nodes children of specific tags.
Press on the button below to show how this code renders on your device.

Example: Inserting Elements#

DOM Visitor to Insert Content
import React from 'react';
import { useWindowDimensions } from 'react-native';
import RenderHtml from 'react-native-render-html';
import { prependChild } from 'domutils';
import { Element } from 'domhandler';
function onElement(element) {
// Add an image extracted from data-cover-src
// attr as first child of every article.
if (element.tagName === 'article') {
const cover = element.attribs['data-cover-src'];
if (cover) {
const img = new Element('img', {
src: cover
});
prependChild(element, img);
}
}
}
const source = {
html: `
<article data-cover-src="https://picsum.photos/640/360">
<p style="padding: 10px;">
Lorem ipsum dolor sit amet, consectetur adipiscing
elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
</p>
</article>
`
};
const domVisitors = {
onElement: onElement
};
export default function App() {
const { width } = useWindowDimensions();
return (
<RenderHtml
contentWidth={width}
source={source}
domVisitors={domVisitors}
/>
);
}
Usage of domVisitors.onElement to insert an <img> element at the top of every article thanks to "prependChild" function from domutils. The source is extracted from the "data-cover-src" attribute of the article element.
Press on the button below to show how this code renders on your device.

Ignoring nodes#

Two props can be used to ignore nodes. โ€‹ignoredDomTags is an array of lowercase tags to exclude, and โ€‹ignoreDomNode is a function taking every parsed DOM node and offering you to reject it by returning true. Both props are processed at parse time.

Example: Ignoring Nodes Conditionally#

Removing DOM nodes conditionally
import React from 'react';
import { useWindowDimensions } from 'react-native';
import RenderHtml from 'react-native-render-html';
import { isTag } from 'domutils';
function ignoreDomNode(node, parent) {
// remove anchors children of <p> elements
return (
isTag(node) && node.name === 'a' && parent.name === 'p'
);
}
const source = {
html: `
<p style="text-align: center">
<a href="">you're a noisy one, aren't you?</a>
Can you see the anchor? It has been ignored!
</p>
`
};
export default function App() {
const { width } = useWindowDimensions();
return (
<RenderHtml
contentWidth={width}
source={source}
ignoreDomNode={ignoreDomNode}
/>
);
}
Usage of ignoreDomNode to remove anchors children of <p> elements. Notice that the second argument to ignoreDomNode function is used to perform assertions on the parent. Don't use node.parent since the node hasn't been attached yet.
Press on the button below to show how this code renders on your device.
caution

When โ€‹ignoreDomNode is invoked, the passed node has not been attached to his parent yet. But the parent is given as a second argument.

Root Selection#

This library provides โ€‹selectDomRoot prop to select a subtree to render. See example below:

Selecting the Root to Render
import React from 'react';
import { useWindowDimensions } from 'react-native';
import RenderHtml from 'react-native-render-html';
import { findOne } from 'domutils';
function selectDomRoot(node) {
// Third argument set to 'true' for recursive search
return findOne((e) => e.name === 'article', node.children, true);
}
const source = {
html: `
<body>
<nav>
<a href="/">home</a>
<a href="/contact">contact</a>
</nav>
<article style="padding: 10px;">
<header>
<h1>Lorem Impsum Dolor Sit!</h1>
</header>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing
elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore
eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
</p>
</article>
<footer>
Lorem ipsum inc, 2021
</footer>
</body>
`
};
export default function App() {
const { width } = useWindowDimensions();
return (
<RenderHtml
contentWidth={width}
source={source}
selectDomRoot={selectDomRoot}
/>
);
}
Usage of selectDomRoot with findOne from domutils to select a subtree to render.
Press on the button below to show how this code renders on your device.
note

When โ€‹selectDomRoot returns a falsy value, the initial root will be selected.

Prerendering#

In some scenarios, you might want to inspect the DOM before rendering, and even perform asynchronous operations based on your findings. Use cases might involve, for example:

  1. Fetching data from a Web API;

  2. Pre-caching media assets.

To do so, we will take advantage of the โ€‹composite rendering architecture and the dom source feature:

import React, { useState, useEffect, useMemo } from 'react';
import {
TRenderEngineProvider,
RenderHTMLConfigProvider,
RenderHTMLSource,
useAmbientTRenderEngine
} from 'react-native-render-html';
import { findAll } from 'domutils';
function isImgElement(node) {
return node.name === 'img';
}
function RenderSource({ html }) {
const [isDomReady, setIsDomReady] = useState(false);
// Let's use the TRE provided from the root of our app
// via TRenderEngineProvider to build the DOM
const engine = useAmbientTRenderEngine();
const dom = useMemo(() => engine.parseDocument(html), [html, engine]);
// Use effect to inspect the DOM
useEffect(function inspectDom(){
// Do any pre-rendering logic here
const images = findAll(isImgElement, dom, true);
// Do stuff with images such as preloading
// ...
// When preloading is done, set isDomReady to true!
setIsDomReady(true);
},[dom]);
return isDomReady ?
<RenderHTMLSource source={{ dom }} />
: null;
}
const html = `
hello world!
<img src='https://img.io/001.jpg' />
`
export default function App() {
return (
<TRenderEngineProvider>
<RenderHTMLConfigProvider>
<RenderSource html={html} />
</RenderHTMLConfigProvider>
</TRenderEngineProvider>
);
}

Let's note a few important details in this example:

  1. โ€‹TRenderEngineProvider accepts all โ€‹RenderHTML component props pertaining to the โ€‹Transient Render Engine layer such as โ€‹customHTMLElementModels, โ€‹classesStyles (all styling props) and DOM related such as โ€‹domVisitors, โ€‹selectDomRoot...

  2. โ€‹RenderHTMLConfigProvider accepts all โ€‹RenderHTML component props pertaining to the โ€‹Rendering layer such as โ€‹renderers, โ€‹renderersProps, โ€‹computeEmbeddedMaxWidth, ...

  3. โ€‹RenderHTMLSource accepts all RenderHTML component props pertaining to the document such as โ€‹source, โ€‹onTTreeChange, โ€‹contentWidth...

  4. The general recommendation for this three-layers rendering architecture is that the engine and configuration should be put near the top of your App to factor the cost of instantiating the engine. This is especially usefull for apps which will render hundreds to thousands of small snippets such as chat apps.