Skip to content

Commit e8167f3

Browse files
committed
feat: add support for formatToParts
1 parent 0b8f989 commit e8167f3

File tree

12 files changed

+384
-292
lines changed

12 files changed

+384
-292
lines changed

docs/Components.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,17 @@ props: Intl.DateTimeFormatOptions &
161161
{
162162
value: any,
163163
format: string,
164-
children: (formattedDate: string) => ReactElement,
164+
children: (formattedDate: string | Intl.DateTimeFormatPart[]) =>
165+
ReactElement,
166+
shouldFormatToParts: boolean,
165167
};
166168
```
167169

168170
By default `<FormattedDate>` will render the formatted date into a `<React.Fragment>`. If you need to customize rendering, you can either wrap it with another React element (recommended), or pass a function as the child.
169171

170172
**Example:**
171173

172-
```js
174+
```tsx
173175
<FormattedDate value={new Date(1459832991883)} />
174176
```
175177

@@ -179,12 +181,28 @@ By default `<FormattedDate>` will render the formatted date into a `<React.Fragm
179181

180182
**Example with Options:**
181183

182-
```js
184+
```tsx
185+
<FormattedDate
186+
value={new Date(1459832991883)}
187+
year="numeric"
188+
month="long"
189+
day="2-digit"
190+
/>
191+
```
192+
193+
```html
194+
<span>April 05, 2016</span>
195+
```
196+
197+
**shouldFormatToParts**
198+
199+
```tsx
183200
<FormattedDate
184201
value={new Date(1459832991883)}
185202
year="numeric"
186203
month="long"
187204
day="2-digit"
205+
shouldFormatToParts={true}
188206
/>
189207
```
190208

examples/TimeZone.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ const App: React.FC<Props> = ({currentTime = Date.now()}) => {
1212
The date in Tokyo is: <FormattedDate value={currentTime} />
1313
<br />
1414
The time in Tokyo is: <FormattedTime value={currentTime} />
15+
<br />
16+
<FormattedDate value={currentTime} shouldFormatToParts>
17+
{parts => <>{JSON.stringify(parts)}</>}
18+
</FormattedDate>
1519
</p>
1620
</IntlProvider>
1721
);

src/components/createFormattedComponent.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ import * as React from 'react';
22
import {invariantIntlContext} 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_;
513

614
enum DisplayName {
715
formatDate = 'FormattedDate',
@@ -21,6 +29,7 @@ export default function createFormattedComponent<Name extends keyof Formatter>(
2129
type Options = Formatter[Name];
2230
type FormatFn = IntlShape[Name];
2331
type Props = Options & {
32+
shouldFormatToParts?: boolean;
2433
value: Parameters<FormatFn>[0];
2534
children?: (val: string) => React.ReactElement | null;
2635
};
@@ -29,7 +38,30 @@ export default function createFormattedComponent<Name extends keyof Formatter>(
2938
<Context.Consumer>
3039
{intl => {
3140
invariantIntlContext(intl);
32-
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+
}
3365
const formattedValue = intl[name](props.value as any, props);
3466

3567
if (typeof props.children === 'function') {

src/components/message.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as React from 'react';
88
import {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat';
99
import {Context} from './injectIntl';
1010
import {MessageDescriptor} from '../types';
11-
import {formatMessage as baseFormatMessage} from '../format';
11+
import {formatMessage} from '../formatters/message';
1212
import {
1313
invariantIntlContext,
1414
DEFAULT_INTL_CONFIG,
@@ -31,7 +31,7 @@ const defaultFormatMessage = (
3131
);
3232
}
3333

34-
return baseFormatMessage(
34+
return formatMessage(
3535
{
3636
...DEFAULT_INTL_CONFIG,
3737
locale: 'en',

src/components/provider.tsx

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,12 @@ import {
1414
createIntlCache,
1515
} from '../utils';
1616
import {IntlConfig, IntlShape, Omit, IntlCache} from '../types';
17-
import {
18-
formatNumber,
19-
formatRelativeTime,
20-
formatDate,
21-
formatTime,
22-
formatPlural,
23-
formatHTMLMessage,
24-
formatMessage,
25-
} from '../format';
2617
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';
22+
import {formatMessage, formatHTMLMessage} from '../formatters/message';
2723
import * as shallowEquals_ from 'shallow-equal/objects';
2824
const shallowEquals: typeof shallowEquals_ =
2925
(shallowEquals_ as any).default || shallowEquals_;
@@ -134,20 +130,21 @@ export function createIntl(
134130
return {
135131
...resolvedConfig,
136132
formatters,
137-
formatNumber: formatNumber.bind(undefined, resolvedConfig, formatters),
138-
formatRelativeTime: formatRelativeTime.bind(
139-
undefined,
133+
formatNumber: formatNumberFactory(
134+
resolvedConfig,
135+
formatters.getNumberFormat
136+
),
137+
formatRelativeTime: formatRelativeTimeFactory(
140138
resolvedConfig,
141-
formatters
139+
formatters.getRelativeTimeFormat
142140
),
143-
formatDate: formatDate.bind(undefined, resolvedConfig, formatters),
144-
formatTime: formatTime.bind(undefined, resolvedConfig, formatters),
145-
formatPlural: formatPlural.bind(undefined, resolvedConfig, formatters),
146-
formatMessage: formatMessage.bind(undefined, resolvedConfig, formatters),
147-
formatHTMLMessage: formatHTMLMessage.bind(
148-
undefined,
141+
formatDate: formatDateFactory(resolvedConfig, formatters.getDateTimeFormat),
142+
formatTime: formatTimeFactory(resolvedConfig, formatters.getDateTimeFormat),
143+
formatPlural: formatPluralFactory(
149144
resolvedConfig,
150-
formatters
145+
formatters.getPluralRules
151146
),
147+
formatMessage: formatMessage.bind(null, resolvedConfig, formatters),
148+
formatHTMLMessage: formatHTMLMessage.bind(null, resolvedConfig, formatters),
152149
};
153150
}

src/formatters/dateTime.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* Copyright 2015, Yahoo Inc.
3+
* Copyrights licensed under the New BSD License.
4+
* See the accompanying LICENSE file for terms.
5+
*/
6+
7+
import {Formatters, IntlConfig, IntlFormatters} from '../types';
8+
9+
import {createError, filterProps, getNamedFormat} from '../utils';
10+
11+
const DATE_TIME_FORMAT_OPTIONS: Array<keyof Intl.DateTimeFormatOptions> = [
12+
'localeMatcher',
13+
'formatMatcher',
14+
15+
'timeZone',
16+
'hour12',
17+
18+
'weekday',
19+
'era',
20+
'year',
21+
'month',
22+
'day',
23+
'hour',
24+
'minute',
25+
'second',
26+
'timeZoneName',
27+
];
28+
29+
function getFormatter(
30+
{
31+
locale,
32+
formats,
33+
onError,
34+
timeZone,
35+
}: Pick<IntlConfig, 'locale' | 'formats' | 'onError' | 'timeZone'>,
36+
type: 'date' | 'time',
37+
getDateTimeFormat: Formatters['getDateTimeFormat'],
38+
options: Parameters<IntlFormatters['formatDate']>[1] = {}
39+
) {
40+
const {format} = options;
41+
let defaults = {
42+
...(timeZone && {timeZone}),
43+
...(format && getNamedFormat(formats!, type, format, onError)),
44+
};
45+
46+
let filteredOptions = filterProps(
47+
options,
48+
DATE_TIME_FORMAT_OPTIONS,
49+
defaults
50+
);
51+
52+
if (
53+
type === 'time' &&
54+
!filteredOptions.hour &&
55+
!filteredOptions.minute &&
56+
!filteredOptions.second
57+
) {
58+
// Add default formatting options if hour, minute, or second isn't defined.
59+
filteredOptions = {...filteredOptions, hour: 'numeric', minute: 'numeric'};
60+
}
61+
62+
return getDateTimeFormat(locale, filteredOptions);
63+
}
64+
65+
export function formatDateFactory(
66+
config: Pick<IntlConfig, 'locale' | 'formats' | 'onError' | 'timeZone'>,
67+
getDateTimeFormat: Formatters['getDateTimeFormat']
68+
) {
69+
return (
70+
value?: Parameters<IntlFormatters['formatDate']>[0],
71+
options: Parameters<IntlFormatters['formatDate']>[1] = {}
72+
) => {
73+
const date = typeof value === 'string' ? new Date(value || 0) : value;
74+
try {
75+
return getFormatter(config, 'date', getDateTimeFormat, options).format(
76+
date
77+
);
78+
} catch (e) {
79+
config.onError(createError('Error formatting date.', e));
80+
}
81+
82+
return String(date);
83+
};
84+
}
85+
86+
export function formatTimeFactory(
87+
config: Pick<IntlConfig, 'locale' | 'formats' | 'onError' | 'timeZone'>,
88+
getDateTimeFormat: Formatters['getDateTimeFormat']
89+
) {
90+
return (
91+
value?: Parameters<IntlFormatters['formatTime']>[0],
92+
options: Parameters<IntlFormatters['formatTime']>[1] = {}
93+
) => {
94+
const date = typeof value === 'string' ? new Date(value || 0) : value;
95+
96+
try {
97+
return getFormatter(config, 'time', getDateTimeFormat, options).format(
98+
date
99+
);
100+
} catch (e) {
101+
config.onError(createError('Error formatting time.', e));
102+
}
103+
104+
return String(date);
105+
};
106+
}

0 commit comments

Comments
 (0)