Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implemented custom resolvers to define custom logic #169

Merged
merged 1 commit into from
Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ npm install --save react-to-text

## Usage

### Out of the box
react-to-text takes in any React component and will return the text content (without any HTML). It
will accept any value React can render (strings, arrays, Fragments, etc.), and can handle deeply
nested components.
Expand All @@ -31,6 +32,29 @@ console.log(text);
> "We sell apples and oranges."
```

### Custom logic
The default logic won't always work, for example when you have a custom component which simply renders one of it's props.
In this case you can define custom stringification behavior using a second argument.

```jsx
import reactToText, { ResolverMap } from 'react-to-text';

const CustomComponent: React.FC<{ title: string }> = (props) => <p>{props.title}</p>;

// since this component does not have any direct children
// it will not output anything by default
console.log(reactToText(<CustomComponent title="foo" />));
> ""

// using a custom resolver map using the custom component as it's key
// and the stringification logic as it's value, we can adjust this behavior
const resolvers: ResolverMap = new Map([
[CustomComponent, (props: { title: string }) => props.title],
]);
console.log(reactToText(<CustomComponent title="foo" />, resolvers));
> "foo"
```

## License

Licensed under MIT.
3 changes: 2 additions & 1 deletion dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react';
declare function reactToText(node: React.ReactNode): string;
export declare type ResolverMap = Map<string | React.JSXElementConstructor<any>, (props: any) => string>;
declare function reactToText(node: React.ReactNode, resolvers?: ResolverMap): string;
export default reactToText;
17 changes: 14 additions & 3 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use strict';

function reactToText(node) {
var react = require('react');

/* eslint-disable @typescript-eslint/no-explicit-any */

function reactToText(node, resolvers) {
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') {
return node.toString();
}
Expand All @@ -10,7 +14,14 @@ function reactToText(node) {
}

if (Array.isArray(node)) {
return node.map(reactToText).join('');
return node.map(entry => reactToText(entry, resolvers)).join('');
}

const [nodeType, nodeProps] = /*#__PURE__*/react.isValidElement(node) ? [node.type, node.props] : [null, null]; // check if custom resolver is available

if (nodeType && (resolvers === null || resolvers === void 0 ? void 0 : resolvers.has(nodeType))) {
const resolver = resolvers.get(nodeType);
return resolver(nodeProps);
} // Because ReactNode includes {} in its union we need to jump through a few hoops.


Expand All @@ -20,7 +31,7 @@ function reactToText(node) {
return '';
}

return reactToText(props.children);
return reactToText(props.children, resolvers);
}

module.exports = reactToText;
21 changes: 20 additions & 1 deletion src/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';

import reactToText from './index';
import reactToText, { ResolverMap } from './index';

const CustomComponent: React.FC<{ title: string }> = (props) => <p>{props.title}</p>;

describe('reactToText', () => {
describe('When given text', () => {
Expand Down Expand Up @@ -83,4 +85,21 @@ describe('reactToText', () => {
expect(reactToText(<div>one &mdash; two</div>)).toBe('one — two');
});
});

describe('When given a custom component', () => {
it('Returns uses the custom resolver behavior', () => {
const resolvers: ResolverMap = new Map([
[CustomComponent, (props: { title: string }) => props.title],
]);

expect(
reactToText(
<div>
foo <CustomComponent title="bar" />
</div>,
resolvers,
),
).toBe('foo bar');
});
});
});
17 changes: 13 additions & 4 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react';
import React, { isValidElement } from 'react';

function reactToText(node: React.ReactNode): string {
export type ResolverMap = Map<string | React.JSXElementConstructor<any>, (props: any) => string>;

function reactToText(node: React.ReactNode, resolvers?: ResolverMap): string {
if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') {
return node.toString();
}
if (!node) {
return '';
}
if (Array.isArray(node)) {
return node.map(reactToText).join('');
return node.map((entry) => reactToText(entry, resolvers)).join('');
}

const [nodeType, nodeProps] = isValidElement(node) ? [node.type, node.props] : [null, null];
// check if custom resolver is available
if (nodeType && resolvers?.has(nodeType)) {
const resolver = resolvers.get(nodeType)!;
return resolver(nodeProps);
}

// Because ReactNode includes {} in its union we need to jump through a few hoops.
Expand All @@ -19,7 +28,7 @@ function reactToText(node: React.ReactNode): string {
return '';
}

return reactToText(props.children);
return reactToText(props.children, resolvers);
}

export default reactToText;