Skip to content

Commit a1b5ff1

Browse files
committed
feat: introduce Parts component
namely, FormattedNumberParts, FormattedTimeParts, FormattedDateParts add docs as well fixes #1048
1 parent e8167f3 commit a1b5ff1

File tree

17 files changed

+681
-167
lines changed

17 files changed

+681
-167
lines changed

docs/Components.md

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ React Intl has a set of React components that provide a declarative way to setup
1111
- [Dynamic Language Selection](#dynamic-language-selection)
1212
- [Date Formatting Components](#date-formatting-components)
1313
- [`FormattedDate`](#formatteddate)
14+
- [`FormattedDateParts`](#formatteddateparts)
1415
- [`FormattedTime`](#formattedtime)
16+
- [`FormattedTimeParts`](#formattedtimeparts)
1517
- [`FormattedRelativeTime`](#formattedrelativetime)
1618
- [Number Formatting Components](#number-formatting-components)
1719
- [`FormattedNumber`](#formattednumber)
20+
- [`FormattedNumberParts`](#formattednumberparts)
1821
- [`FormattedPlural`](#formattedplural)
1922
- [String Formatting Components](#string-formatting-components)
2023
- [Message Syntax](#message-syntax)
@@ -161,9 +164,7 @@ props: Intl.DateTimeFormatOptions &
161164
{
162165
value: any,
163166
format: string,
164-
children: (formattedDate: string | Intl.DateTimeFormatPart[]) =>
165-
ReactElement,
166-
shouldFormatToParts: boolean,
167+
children: (formattedDate: string) => ReactElement,
167168
};
168169
```
169170

@@ -194,20 +195,40 @@ By default `<FormattedDate>` will render the formatted date into a `<React.Fragm
194195
<span>April 05, 2016</span>
195196
```
196197

197-
**shouldFormatToParts**
198+
### `FormattedDateParts`
199+
200+
This component provides more customization to `FormattedDate` by allowing children function to have access to underlying parts of the formatted date. The available parts are listed [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/formatToParts)
201+
202+
**Props:**
203+
204+
```ts
205+
props: Intl.DateTimeFormatOptions &
206+
{
207+
value: any,
208+
format: string,
209+
children: (parts: Intl.DateTimeFormatPart[]) => ReactElement,
210+
};
211+
```
198212

199213
```tsx
200-
<FormattedDate
214+
<FormattedDateParts
201215
value={new Date(1459832991883)}
202216
year="numeric"
203217
month="long"
204218
day="2-digit"
205-
shouldFormatToParts={true}
206-
/>
219+
>
220+
{parts => (
221+
<>
222+
<b>{parts[0].value}</b>
223+
{parts[1].value}
224+
<small>{parts[2].value}</small>
225+
</>
226+
)}
227+
</FormattedDate>
207228
```
208229

209230
```html
210-
<span>April 05, 2016</span>
231+
<span> <b>April</b> <small>05</small> </span>
211232
```
212233

213234
### `FormattedTime`
@@ -243,6 +264,37 @@ By default `<FormattedTime>` will render the formatted time into a `<span>`. If
243264
<span>1:09 AM</span>
244265
```
245266

267+
### `FormattedTimeParts`
268+
269+
This component provides more customization to `FormattedTime` by allowing children function to have access to underlying parts of the formatted date. The available parts are listed [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat/formatToParts)
270+
271+
**Props:**
272+
273+
```ts
274+
props: Intl.DateTimeFormatOptions &
275+
{
276+
value: any,
277+
format: string,
278+
children: (parts: Intl.DateTimeFormatPart[]) => ReactElement,
279+
};
280+
```
281+
282+
```tsx
283+
<FormattedTimeParts value={new Date(1459832991883)}>
284+
{parts => (
285+
<>
286+
<b>{parts[0].value}</b>
287+
{parts[1].value}
288+
<small>{parts[2].value}</small>
289+
</>
290+
)}
291+
</FormattedDate>
292+
```
293+
294+
```html
295+
<span> <b>01</b>:<small>09</small> </span>
296+
```
297+
246298
### `FormattedRelativeTime`
247299

248300
This component uses the [`formatRelativeTime`](API.md#formatrelativetime) API and has `props` that correspond to the following relative formatting options:
@@ -337,6 +389,39 @@ By default `<FormattedNumber>` will render the formatted number into a `<span>`.
337389
<span>1,000</span>
338390
```
339391

392+
### `FormattedNumberParts`
393+
394+
This component provides more customization to `FormattedNumber` by allowing children function to have access to underlying parts of the formatted date. The available parts are listed [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat/formatToParts)
395+
396+
**Props:**
397+
398+
```ts
399+
props: NumberFormatOptions &
400+
{
401+
value: number,
402+
format: string,
403+
children: (parts: Intl.NumberFormatPart[]) => ReactElement,
404+
};
405+
```
406+
407+
**Example:**
408+
409+
```tsx
410+
<FormattedNumberParts value={1000}>
411+
{parts => (
412+
<>
413+
<b>{parts[0].value}</b>
414+
{parts[1].value}
415+
<small>{parts[2].value}</small>
416+
</>
417+
)}
418+
</FormattedNumberParts>
419+
```
420+
421+
```html
422+
<span> <b>1</b>,<small>000</small> </span>
423+
```
424+
340425
### `FormattedPlural`
341426

342427
This component uses the [`formatPlural`](API.md#formatplural) API and [`Intl.PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/PluralRules) has `props` that correspond to `Intl.PluralRulesOptions`.

examples/TimeZone.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import * as React from 'react';
2-
import {FormattedDate, FormattedTime, IntlProvider} from '../';
2+
import {
3+
FormattedDate,
4+
FormattedTime,
5+
IntlProvider,
6+
FormattedDateParts,
7+
} from '../';
38

49
interface Props {
510
currentTime?: Date | number;
@@ -13,9 +18,20 @@ const App: React.FC<Props> = ({currentTime = Date.now()}) => {
1318
<br />
1419
The time in Tokyo is: <FormattedTime value={currentTime} />
1520
<br />
16-
<FormattedDate value={currentTime} shouldFormatToParts>
17-
{parts => <>{JSON.stringify(parts)}</>}
18-
</FormattedDate>
21+
<FormattedDateParts
22+
value={new Date(1459832991883)}
23+
year="numeric"
24+
month="long"
25+
day="2-digit"
26+
>
27+
{(parts: Intl.DateTimeFormatPart[]) => (
28+
<>
29+
<b>{parts[0].value}</b>
30+
{parts[1].value}
31+
<small>{parts[2].value}</small>
32+
</>
33+
)}
34+
</FormattedDateParts>
1935
</p>
2036
</IntlProvider>
2137
);

src/components/createFormattedComponent.tsx

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,113 @@
11
import * as React from 'react';
2-
import {invariantIntlContext} from '../utils';
2+
import {invariantIntlContext, createError} from '../utils';
33
import {IntlShape, FormatDateOptions, FormatNumberOptions} from '../types';
44
import {Context} from './injectIntl';
5-
import {formatDateFactory, formatTimeFactory} from '../formatters/dateTime';
6-
import {formatNumberFactory} from '../formatters/number';
7-
// Since rollup cannot deal with namespace being a function,
8-
// this is to interop with TypeScript since `invariant`
9-
// does not export a default
10-
// https://github.com/rollup/rollup/issues/1267
11-
import * as invariant_ from 'invariant';
12-
const invariant: typeof invariant_ = (invariant_ as any).default || invariant_;
5+
import {getFormatter as getDateTimeFormatter} from '../formatters/dateTime';
6+
import {getFormatter as getNumberFormatter} from '../formatters/number';
137

148
enum DisplayName {
159
formatDate = 'FormattedDate',
1610
formatTime = 'FormattedTime',
1711
formatNumber = 'FormattedNumber',
1812
}
1913

14+
enum DisplayNameParts {
15+
formatDate = 'FormattedDateParts',
16+
formatTime = 'FormattedTimeParts',
17+
formatNumber = 'FormattedNumberParts',
18+
}
19+
2020
type Formatter = {
2121
formatDate: FormatDateOptions;
2222
formatTime: FormatDateOptions;
2323
formatNumber: FormatNumberOptions;
2424
};
2525

26-
export default function createFormattedComponent<Name extends keyof Formatter>(
26+
export const FormattedNumberParts: React.FC<
27+
Formatter['formatNumber'] & {
28+
value: Parameters<IntlShape['formatNumber']>[0];
29+
30+
children(val: Intl.NumberFormatPart[]): React.ReactElement | null;
31+
}
32+
> = props => (
33+
<Context.Consumer>
34+
{intl => {
35+
invariantIntlContext(intl);
36+
const {value, children} = props;
37+
let formattedParts: Intl.NumberFormatPart[] = [];
38+
try {
39+
formattedParts = getNumberFormatter(
40+
intl,
41+
intl.formatters.getNumberFormat,
42+
props
43+
).formatToParts(value);
44+
} catch (e) {
45+
intl.onError(createError(`Error formatting number.`, e));
46+
}
47+
return children(formattedParts);
48+
}}
49+
</Context.Consumer>
50+
);
51+
FormattedNumberParts.displayName = 'FormattedNumberParts';
52+
53+
export function createFormattedDateTimePartsComponent<
54+
Name extends keyof Formatter
55+
>(name: Name) {
56+
type FormatFn = IntlShape[Name];
57+
type Props = Formatter[Name] & {
58+
value: Parameters<FormatFn>[0];
59+
children(val: Intl.DateTimeFormatPart[]): React.ReactElement | null;
60+
};
61+
62+
const ComponentParts: React.FC<Props> = props => (
63+
<Context.Consumer>
64+
{intl => {
65+
invariantIntlContext(intl);
66+
const {value, children} = props;
67+
const date = typeof value === 'string' ? new Date(value || 0) : value;
68+
let formattedParts: Intl.DateTimeFormatPart[] = [];
69+
try {
70+
formattedParts = getDateTimeFormatter(
71+
intl,
72+
name === 'formatDate' ? 'date' : 'time',
73+
intl.formatters.getDateTimeFormat,
74+
props
75+
).formatToParts(date as Date);
76+
} catch (e) {
77+
intl.onError(
78+
createError(
79+
`Error formatting ${name === 'formatDate' ? 'date' : 'time'}.`,
80+
e
81+
)
82+
);
83+
}
84+
85+
return children(formattedParts);
86+
}}
87+
</Context.Consumer>
88+
);
89+
ComponentParts.displayName = DisplayNameParts[name];
90+
return ComponentParts;
91+
}
92+
93+
export function createFormattedComponent<Name extends keyof Formatter>(
2794
name: Name
2895
) {
29-
type Options = Formatter[Name];
3096
type FormatFn = IntlShape[Name];
31-
type Props = Options & {
32-
shouldFormatToParts?: boolean;
97+
type Props = Formatter[Name] & {
3398
value: Parameters<FormatFn>[0];
34-
children?: (val: string) => React.ReactElement | null;
99+
children?(val: string): React.ReactElement | null;
35100
};
36101

37102
const Component: React.FC<Props> = props => (
38103
<Context.Consumer>
39104
{intl => {
40105
invariantIntlContext(intl);
41-
let formattedParts;
42-
if (props.shouldFormatToParts) {
43-
if (name === 'formatDate') {
44-
formattedParts = formatDateFactory(
45-
intl,
46-
intl.formatters.getDateTimeFormat
47-
)(props.value, props);
48-
} else if (name === 'formatTime') {
49-
formattedParts = formatTimeFactory(
50-
intl,
51-
intl.formatters.getDateTimeFormat
52-
)(props.value, props);
53-
} else {
54-
formattedParts = formatNumberFactory(
55-
intl,
56-
intl.formatters.getNumberFormat
57-
)(props.value as number, props);
58-
}
59-
invariant(
60-
typeof props.children === 'function',
61-
'render props must be a function when `shouldFormatToParts` is `true`'
62-
);
63-
return props.children!(formattedParts);
64-
}
65-
const formattedValue = intl[name](props.value as any, props);
106+
const {value, children} = props;
107+
const formattedValue = intl[name](value as any, props);
66108

67-
if (typeof props.children === 'function') {
68-
return props.children(formattedValue);
109+
if (typeof children === 'function') {
110+
return children(formattedValue as any);
69111
}
70112
const Text = intl.textComponent || React.Fragment;
71113
return <Text>{formattedValue}</Text>;

src/components/provider.tsx

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ import {
1515
} from '../utils';
1616
import {IntlConfig, IntlShape, Omit, IntlCache} from '../types';
1717
import areIntlLocalesSupported from 'intl-locales-supported';
18-
import {formatNumberFactory} from '../formatters/number';
19-
import {formatRelativeTimeFactory} from '../formatters/relativeTime';
20-
import {formatDateFactory, formatTimeFactory} from '../formatters/dateTime';
21-
import {formatPluralFactory} from '../formatters/plural';
18+
import {formatNumber} from '../formatters/number';
19+
import {formatRelativeTime} from '../formatters/relativeTime';
20+
import {formatDate, formatTime} from '../formatters/dateTime';
21+
import {formatPlural} from '../formatters/plural';
2222
import {formatMessage, formatHTMLMessage} from '../formatters/message';
2323
import * as shallowEquals_ from 'shallow-equal/objects';
2424
const shallowEquals: typeof shallowEquals_ =
@@ -130,17 +130,28 @@ export function createIntl(
130130
return {
131131
...resolvedConfig,
132132
formatters,
133-
formatNumber: formatNumberFactory(
133+
formatNumber: formatNumber.bind(
134+
null,
134135
resolvedConfig,
135136
formatters.getNumberFormat
136137
),
137-
formatRelativeTime: formatRelativeTimeFactory(
138+
formatRelativeTime: formatRelativeTime.bind(
139+
null,
138140
resolvedConfig,
139141
formatters.getRelativeTimeFormat
140142
),
141-
formatDate: formatDateFactory(resolvedConfig, formatters.getDateTimeFormat),
142-
formatTime: formatTimeFactory(resolvedConfig, formatters.getDateTimeFormat),
143-
formatPlural: formatPluralFactory(
143+
formatDate: formatDate.bind(
144+
null,
145+
resolvedConfig,
146+
formatters.getDateTimeFormat
147+
),
148+
formatTime: formatTime.bind(
149+
null,
150+
resolvedConfig,
151+
formatters.getDateTimeFormat
152+
),
153+
formatPlural: formatPlural.bind(
154+
null,
144155
resolvedConfig,
145156
formatters.getPluralRules
146157
),

0 commit comments

Comments
 (0)