Skip to content

Commit

Permalink
feat(react-intl): merge chunks in FormatXMLElementFn
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This turns rich text formatting callback function to
non-variadic. So `(...chunks) => React.ReactNode` becomes `(chunks) =>
React.ReactNode`. This solves a couple of issues:
1. We receive feedback that variadic callback fn is not ergonomic
2. This solves the missing key issue when we render rich text
3. This allows us to utilize extra param to distinguish when 2 React
element are exactly the same except for their indices, e.g `<b>one</b>
and <b>one</b>` and you want to render them differently
fixes #1623
  • Loading branch information
Vaclav Novotny authored and Long Ho committed Jul 2, 2020
1 parent 2be4cfb commit 1b5892f
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 5 deletions.
29 changes: 26 additions & 3 deletions packages/react-intl/src/formatters/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import * as React from 'react';
import {invariant} from '@formatjs/intl-utils';
import {unapplyFormatXMLElementFn} from '../utils';

import {
Formatters,
Expand Down Expand Up @@ -72,6 +73,27 @@ function deepMergeFormatsAndSetTimeZone(
};
}

function isFormatXMLElementFn(
input: any
): input is FormatXMLElementFn<React.ReactNode, React.ReactNode> {
return typeof input === 'function';
}

export function unapplyFormatXMLElementFnInValues(
values: Record<
string,
| PrimitiveType
| React.ReactNode
| FormatXMLElementFn<React.ReactNode, React.ReactNode>
>
): typeof values {
return Object.keys(values).reduce((acc: typeof values, k) => {
const v = values[k];
acc[k] = isFormatXMLElementFn(v) ? unapplyFormatXMLElementFn(v) : v;
return acc;
}, {});
}

function prepareIntlMessageFormatHtmlOutput(
chunks: React.ReactNode,
shouldWrap?: boolean
Expand Down Expand Up @@ -141,6 +163,7 @@ export function formatMessage(
if (!values && message && typeof message === 'string') {
return message.replace(/'\{(.*?)\}'/gi, `{$1}`);
}
const patchedValues = values && unapplyFormatXMLElementFnInValues(values);
formats = deepMergeFormatsAndSetTimeZone(formats, timeZone);
defaultFormats = deepMergeFormatsAndSetTimeZone(defaultFormats, timeZone);

Expand All @@ -163,7 +186,7 @@ export function formatMessage(
);

return prepareIntlMessageFormatHtmlOutput(
formatter.format(values),
formatter.format(patchedValues),
wrapRichTextChunksInFragment
);
} catch (e) {
Expand All @@ -188,7 +211,7 @@ export function formatMessage(
});

return prepareIntlMessageFormatHtmlOutput(
formatter.format<React.ReactNode>(values),
formatter.format<React.ReactNode>(patchedValues),
wrapRichTextChunksInFragment
);
} catch (e) {
Expand All @@ -213,7 +236,7 @@ export function formatMessage(
);

return prepareIntlMessageFormatHtmlOutput(
formatter.format(values),
formatter.format(patchedValues),
wrapRichTextChunksInFragment
);
} catch (e) {
Expand Down
21 changes: 20 additions & 1 deletion packages/react-intl/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
IntlShape,
} from './types';
import * as React from 'react';
import IntlMessageFormat from 'intl-messageformat';
import IntlMessageFormat, {FormatXMLElementFn} from 'intl-messageformat';
import memoizeIntlConstructor from 'intl-format-cache';
import {invariant} from '@formatjs/intl-utils';
import {IntlRelativeTimeFormatOptions} from '@formatjs/intl-relativetimeformat';
Expand Down Expand Up @@ -134,3 +134,22 @@ export function getNamedFormat<T extends keyof CustomFormats>(

onError(new UnsupportedFormatterError(`No ${type} format named: ${name}`));
}

/**
* Takes a `formatXMLElementFn`, which takes a single React.Node argument, and
* returns a FormatXMLElementFn which takes any number of positional arguments.
* I.e. converts non-variadic FormatXMLElementFn to variadic. Variadic
* FormatXMLElementFn is needed for 'intl-messageformat' package, non-variadic
* simplifies API of 'react-intl' package.
* @param formatXMLElementFn
*/
export function unapplyFormatXMLElementFn(
formatXMLElementFn: FormatXMLElementFn<React.ReactNode, React.ReactNode>
): (node: React.ReactNode) => React.ReactNode {
return function () {
return formatXMLElementFn(
// eslint-disable-next-line prefer-rest-params
arguments.length === 1 ? arguments[0] : React.Children.toArray(arguments)
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,33 @@ exports[`<FormattedMessage> rich text supports rich-text message formatting w/ n
Hello,
<b>
Jest
<i>
<i
key=".1"
>
!
</i>
</b>
</FormattedMessage>
`;
exports[`<FormattedMessage> rich text supports rich-text message formatting w/ nested tag, chunks merged 1`] = `
<FormattedMessage
defaultMessage="Hello, <b>{name}<i>!</i></b>"
id="hello"
values={
Object {
"b": [Function],
"i": [Function],
"name": "Jest",
}
}
>
Hello,
<b>
Jest
<i
key=".1"
>
!
</i>
</b>
Expand Down
16 changes: 16 additions & 0 deletions packages/react-intl/test/unit/components/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,22 @@ describe('<FormattedMessage>', () => {
expect(rendered).toMatchSnapshot();
});

it('supports rich-text message formatting w/ nested tag, chunks merged', () => {
const rendered = mountWithProvider(
{
id: 'hello',
defaultMessage: 'Hello, <b>{name}<i>!</i></b>',
values: {
name: 'Jest',
b: (chunks: any) => <b>{chunks}</b>,
i: (msg: string) => <i>{msg}</i>,
},
},
providerProps
);
expect(rendered).toMatchSnapshot();
});

it('supports rich-text message formatting in function-as-child pattern', () => {
const rendered = mountWithProvider(
{
Expand Down

0 comments on commit 1b5892f

Please sign in to comment.