Extract raw HTML in react-native-render-html custom renderer - react-native-render-html

I'm using react-native-render-html to render html.
The renderers method allows me to provide custom function to render a particular tag. However I want to replace the children with my custom component, using the raw inner HTML from the source.
Consider. I have the below html snippet provided to <HTML /> component:
<a> <b> <c meta="xyz"> Some text </c> <b> </a>
and I have a custom renderer which returns a component that takes a html string and does some magic with it:
const renderers = {
c: () => (
<Custom html={/** how do I get "<c meta="xyz"> Some text </c>"? */} />
)
}

The API was not initially designed to handle these kind of use cases, but as for version 5.0.0, it is very easy!
Version 6.x
import * as React from 'react';
import HTML, {domNodeToHTMLString} from 'react-native-render-html';
function CustomRenderer({ tnode, style, key }) {
const html = React.useMemo(() => domNodeToHTMLString(tnode.domNode), [tnode]);
return <Custom key={key} html={html} style={style} />;
}
Version 5.x
Since version 5, it is extremely easy with the help of the new domNodeToHTMLString util, see snippet below:
import * as React from 'react';
import HTML, {domNodeToHTMLString} from 'react-native-render-html';
function customRenderer(htmlAttribs, children, inlineStyle, passProps) {
const html = domNodeToHTMLString(passProps.domNode);
return <Custom key={passProp.key} html={html} />;
}
Version 4.x and below
To use this hack, you'll need to add “stringify-entities” to your list of dependencies. What this hack does basically, is to use the alterNode hook to add a very unconventional __rawHtml attribute to the DOM node. The attribute will be thereafter passed to the renderer function.
import * as React from 'react';
import HTML from 'react-native-render-html';
import strigifyEntities from 'stringify-entities';
import Custom from './Custom';
function renderOpeningTag(tag, attributes) {
const strAttributes = [];
Object.keys(attributes).forEach((key) => {
strAttributes.push(`${key}="${strigifyEntities(`${attributes[key]}`)}"`);
});
return `<${tag}${strAttributes.length ? ' ' : ''}${strAttributes.join(' ')}>`;
}
function nodeToRawHTML(root) {
let html = '';
if (root.type === 'tag') {
const strChildren = root.children.reduce((prev, curr) => {
return `${prev}${nodeToRawHTML(curr)}`;
}, '');
html = `${renderOpeningTag(root.name, root.attribs)}${strChildren}</${
root.name
}>`;
} else if (root.type === 'text') {
const text = strigifyEntities(root.data);
html = text;
}
return html;
}
function alterNode(node) {
if (node.type === 'tag' && node.name === 'c') {
node.attribs.__rawHtml = nodeToRawHTML(node);
}
}
const renderers = {
c: ({__rawHtml}, children, convertedCSSStyles, passProp) => {
return <Custom key={passProp.key} html={__rawHtml} />
},
};
export default function App() {
return (
<HTML
renderers={renderers}
alterNode={alterNode}
html={'<a> <b> <c meta="xyz"> Some text </c> <b> </a>'}
/>
);
}

Related

Inject Props to React Component

For security reasons, I have to update ant design in my codebase from version 3 to 4.
Previously, this is how I use the icon:
import { Icon } from 'antd';
const Demo = () => (
<div>
<Icon type="smile" />
</div>
);
Since my codebase is relatively big and every single page use Icon, I made a global function getIcon(type) that returns <Icon type={type}>, and I just have to call it whenever I need an Icon.
But starting from antd 4, we have to import Icon we want to use like this:
import { SmileOutlined } from '#ant-design/icons';
const Demo = () => (
<div>
<SmileOutlined />
</div>
);
And yes! Now my getIcon() is not working, I can't pass the type parameter directly.
I tried to import every icon I need and put them inside an object, and call them when I need them. Here's the code:
import {
QuestionCircleTwoTone,
DeleteOutlined,
EditTwoTone
} from '#ant-design/icons';
let icons = {
'notFound': <QuestionCircleTwoTone/>,
'edit': <EditTwoTone/>,
'delete': <DeleteOutlined/>,
}
export const getIcon = (
someParam: any
) => {
let icon = icons[type] !== undefined ? icons[type] : icons['notFound'];
return (
icon
);
};
My problem is: I want to put someParam to the Icon Component, how can I do that?
Or, is there any proper solution to solve my problem?
Thanks~
You can pass props as follows in the icons Object:
let icons = {
'notFound':(props:any)=> <QuestionCircleTwoTone {...props}/>,
'edit': (props:any)=><EditTwoTone {...props}/>,
'delete':(props:any)=> <DeleteOutlined {...props}/>,
}
And then if you will pass any prop to the Icon component then it will pass the prop to the specific icon component
let Icon = icons[type] !== undefined ? icons[type] : icons['notFound'];
return (<Icon someParam={'c'}/>)

Dynamically paste value in dot notation in react component

I'm trying to use the React-NBA-Logos package(npm link) in a small project and faced a question. According to the documentation here is an example of this package use
import React from 'react';
import * as NBAIcons from 'react-nba-logos';
const Example = () => {
return <NBAIcons.TOR />; // Loads the Toronto Raptors logo
};
export default Example;
The question is it possible to paste a variable with a dynamic string value(with some correct tripcode) here? Something like this
const Example = () => {
const NEW_VAR = 'NYK';
return <NBAIcons.NEW_VAR />;
};
You can get around this by creating an alias for the component.
const Example = () => {
const NEW_VAR = 'NYK';
const Component = NBAIcons[NEW_VAR];
return <Component />;
};
A functional approach would work well here:
function NBAIcon (name, props = {}) {
const Icon = NBAIcons[name];
return <Icon {...props} />;
}
function Example () {
return (
<ul>
<li>{NBAIcon('TOR')}</li>
<li>{NBAIcon('ATL', {size: '64px'})}</li>
</ul>
);
}

Lazy Hydrate + Code Splitting on NextJS app

I know how to do Lazy Hydration and I know how to do Code Splitting, but how can I make the splitted chunck download only when the component is hydrating?
My code looks like this
import React from 'react';
import dynamic from 'next/dynamic';
import ReactLazyHydrate from 'react-lazy-hydration';
const MyComponent = dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
export const PageComponent = () => {
return (
...
<ReactLazyHydrate whenVisible>
<MyComponent/>
</ReactLazyHydrate>
...
);
};
MyComponent is rendered below the fold, which means that it is only gonna hydrate when the user scrolls. The problem is that the JS chunck for MyComponent will be downloaded right away when the page loads.
I was able to hack it by using the dynamic import only on client but this makes the component disappear for a second when it hydrates, because the html rendered on server will not be used by react. It will recreate the DOM element and it will be empty until the JS chunck loads.
When the element disappear for a sec it increases the page CLS and that's the main reason why I can not use this hack.
Here is the code for this hack
const MyComponent = typeof window === 'undefined'
? require('components/my-component').MyComponent
: dynamic(() => import('components/my-component').then((mod) => mod.MyComponent));
Note that I want to render the component's HTML on the server render. That't why I don't want to Lazy Load it. I want to Lazy Hydrate so I can have the component's HTML rendered on server but only download
and execute it's JS when it is visible.
Update:
In document:
// stops preloading of code-split chunks
class LazyHead extends Head {
getDynamicChunks(files) {
const dynamicScripts = super.getDynamicChunks(files);
try {
// get chunk manifest from loadable
const loadableManifest = __non_webpack_require__(
'../../react-loadable-manifest.json',
);
// search and filter modules based on marker ID
const chunksToExclude = Object.values(loadableManifest).filter(
manifestModule => manifestModule?.id?.startsWith?.('lazy') || false,
);
const excludeMap = chunksToExclude?.reduce?.((acc, chunks) => {
if (chunks.files) {
acc.push(...chunks.files);
}
return acc;
}, []);
const filteredChunks = dynamicScripts?.filter?.(
script => !excludeMap?.includes(script?.key),
);
return filteredChunks;
} catch (e) {
// if it fails, return the dynamic scripts that were originally sent in
return dynamicScripts;
}
}
}
const backupScript = NextScript.getInlineScriptSource;
NextScript.getInlineScriptSource = (props) => {
// dont let next load all dynamic IDS, let webpack manage it
if (props?.__NEXT_DATA__?.dynamicIds) {
const filteredDynamicModuleIds = props?.__NEXT_DATA__?.dynamicIds?.filter?.(
moduleID => !moduleID?.startsWith?.('lazy'),
);
if (filteredDynamicModuleIds) {
// mutate dynamicIds from next data
props.__NEXT_DATA__.dynamicIds = filteredDynamicModuleIds;
}
}
return backupScript(props);
};
in next config
const mapModuleIds = fn => (compiler) => {
const { context } = compiler.options;
compiler.hooks.compilation.tap('ChangeModuleIdsPlugin', (compilation) => {
compilation.hooks.beforeModuleIds.tap('ChangeModuleIdsPlugin', (modules) => {
const { chunkGraph } = compilation;
for (const module of modules) {
if (module.libIdent) {
const origId = module.libIdent({ context });
// eslint-disable-next-line
if (!origId) continue;
const namedModuleId = fn(origId, module);
if (namedModuleId) {
chunkGraph.setModuleId(module, namedModuleId);
}
}
}
});
});
};
const withNamedLazyChunks = (nextConfig = {}) => Object.assign({}, nextConfig, {
webpack: (config, options) => {
config.plugins.push(
mapModuleIds((id, module) => {
if (
id.includes('/global-brand-statement.js')
|| id.includes('signposting/signposting.js')
|| id.includes('reviews-container/index.js')
|| id.includes('why-we-made-this/why-we-made-this.js')
) {
return `lazy-${module.debugId}`;
}
return false;
}),
);
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options);
}
return config;
},
});
In file, using next/dynamic
<LazyHydrate whenVisible style={null} className="col-xs-12">
<GlobalBrandStatement data={globalBrandData} />
</LazyHydrate>
Not sure if this is what you’re after, but I use lazy hydration mixed with webpack plugin and custom next head to preserve the html but strip out below the fold dynamic imported scripts. So I only download the JS and hydrate the component just before the user scrolls into view. Regardless of it the component was used during render - I don’t need the runtime unless a user is going to see it.
Currently in production and has reduced initial page load by 50%. No impact to SEO
Get me on twitter #scriptedAlchemy if you need the implementation, I’ve not yet written a post about it - but I can tell you it’s totally possible to achieve this “download as you scroll” design with very little effort.
import { useState } from "react";
import dynamic from "next/dynamic";
const MyComponent = dynamic(() => import("components/my-component"));
export const PageComponent = () => {
const [downloadComp, setDownloadComp] = useState(false);
return (
<>
<div className="some-class-name">
<button onClick={() => setDownloadComp(true)}>
Download the component
</button>
{downloadComp && <MyComponent />}
</div>
</>
);
};
The above code will download the once you hit the button. If you want it to download if your scroll to position in that case you can use something like react-intersection-observer to set the setDownloadComp. I don't have experience using react-lazy-hydration but I have been using react-intersection-observer and nextjs dynamic import to lazy load components that depends on user scroll.
I have made a library to make this thing simple. And you can benefit with:
Fully HTML page render (Better for SEO)
Only load JS and Hydrate when needed (Better for PageSpeed)
How to use it
import lazyHydrate from 'next-lazy-hydrate';
// Lazy hydrate when scroll into view
const WhyUs = lazyHydrate(() => import('../components/whyus'));
// Lazy hydrate when users hover into the view
const Footer = lazyHydrate(
() => import('../components/footer', { on: ['hover'] })
);
const HomePage = () => {
return (
<div>
<AboveTheFoldComponent />
{/* ----The Fold---- */}
<WhyUs />
<Footer />
</div>
);
};
Read more: https://github.com/thanhlmm/next-lazy-hydrate

Display links from message as a link

I'm trying to replay any occurrence of a link in a text as a link, but I only get it to show the example.com as text in the message. (Just to be clear, it shows other message content if there is other content in the message than the link, but the link gets "linkified(as seen below)")
Here is the message component.
import React from "react";
import moment from "moment";
import { Comment, Image } from "semantic-ui-react";
const isOwnMessage = (message, user) => {
return message.user.id === user.uid ? "message__self" : "";
};
const isImage = message => {
return message.hasOwnProperty("image");
};
const timeFromNow = timestamp => moment(timestamp).fromNow();
function linkify(text) {
var urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&##\/%?=~_|!:,.;]*[-A-Z0-9+&##\/%=~_|])/ig;
return text.replace(urlRegex, function(url) {
return '' + url + '';
});
}
const Message = ({ message, user }) => (
<Comment>
<Comment.Avatar src={message.user.avatar} />
<Comment.Content className={isOwnMessage(message, user)}>
<Comment.Author as="a">{message.user.name}</Comment.Author>
<Comment.Metadata>{timeFromNow(message.timestamp)}</Comment.Metadata>
{isImage(message) ? (
<Image src={message.image} className="message__image" />
) : (
<div>
<Comment.Text>{linkify(message.content)}</Comment.Text>
<React.Fragment dangerouslySetInnerHTML={linkify(message.content)}>
</React.Fragment>
</div>
)}
</Comment.Content>
</Comment>
);
export default Message;
dangerouslySetInnerHTML prop needs an object with __html property
don't use React.Fragment when using dangerouslySetInnerHTML. Just use a div
Working demo
Code Snippet
function linkify(text) {
var urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&##\/%?=~_|!:,.;]*[-A-Z0-9+&##\/%=~_|])/gi;
return text.replace(urlRegex, function(url) {
return '' + url + "";
});
}
const text = `I'm trying to replay any occurrence of a link in a text as a link, but I only get it to show the example.com as text in the message. (Just to be clear, it shows other message content if there is other content in the message than the link, but the link gets "linkified(as seen below)")`;
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<br />
<div dangerouslySetInnerHTML={{ __html: linkify(text) }} />; }
</div>
);
}
you better treat your url variable first by your regex then return like a valid react element:
function linkify(text) {
const urlRegex =/(\b(https?|ftp|file):\/\/[-A-Z0-9+&##\/%?=~_|!:,.;]*[-A-Z0-9+&##\/%=~_|])/ig;
// extract your url by urlRegex something like
const url = text.match(urlRegex)[0]
return <a href={url} >{url}</a>
}

How to assert a React component with multiple <a> tags has an <a> tag with a given href

I'm trying to write a mocha test for a React component. Basically the component needs to render an <a> tag with its href set to a value in a property that is passed in. The issue is that the component can render multiple <a> tags in an unpredictable order and only one of them has to have the correct href.
I'm using enzyme, chai and chai-enzyme
The following is a cut down version of my real code, but neither of the tests are passing:
const TestComponent = function TestComponent(props) {
const { myHref } = props;
return (
<div>
Link 1<br />
<a href={myHref}>Link 2</a><br />
Link 3
</div>
);
};
TestComponent.propTypes = {
myHref: React.PropTypes.string.isRequired
};
describe('<TestComonent />', () => {
it('renders link with correct href', () => {
const myHref = 'http://example.com/test.htm';
const wrapper = Enzyme.render(
<TestComponent myHref={myHref} />
);
expect(wrapper).to.have.attr('href', myHref);
});
it('renders link with correct href 2', () => {
const myHref = 'http://example.com/test.htm';
const wrapper = Enzyme.render(
<TestComponent myHref={myHref} />
);
expect(wrapper.find('a')).to.have.attr('href', myHref);
});
});
It turns out that I was approaching this wrong. Rather than try to get the assertion part of the expression to work with multiple results from the query, it is better to change the find query to be more specific. It is possible to use attribute filters in a similar way to jQuery. As such my test becomes like this:
const TestComponent = function TestComponent(props) {
const { myHref } = props;
return (
<div>
Link 1<br />
<a href={myHref}>Link 2</a><br />
Link 3
</div>
);
};
TestComponent.propTypes = {
myHref: React.PropTypes.string.isRequired
};
describe('<TestComonent />', () => {
it('renders link with correct href', () => {
const myHref = 'http://example.com/test.htm';
const wrapper = Enzyme.render(
<TestComponent myHref={myHref} />
);
expect(wrapper.find(`a[href="${myHref}"]`)).be.present();
});
});

Resources