Skip to content

Commit

Permalink
feat: introduce Parts component
Browse files Browse the repository at this point in the history
namely, FormattedNumberParts, FormattedTimeParts, FormattedDateParts
add docs as well
fixes #1048
  • Loading branch information
longlho committed Aug 30, 2019
1 parent e8167f3 commit a1b5ff1
Show file tree
Hide file tree
Showing 17 changed files with 681 additions and 167 deletions.
101 changes: 93 additions & 8 deletions docs/Components.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ React Intl has a set of React components that provide a declarative way to setup
- [Dynamic Language Selection](#dynamic-language-selection)
- [Date Formatting Components](#date-formatting-components)
- [`FormattedDate`](#formatteddate)
- [`FormattedDateParts`](#formatteddateparts)
- [`FormattedTime`](#formattedtime)
- [`FormattedTimeParts`](#formattedtimeparts)
- [`FormattedRelativeTime`](#formattedrelativetime)
- [Number Formatting Components](#number-formatting-components)
- [`FormattedNumber`](#formattednumber)
- [`FormattedNumberParts`](#formattednumberparts)
- [`FormattedPlural`](#formattedplural)
- [String Formatting Components](#string-formatting-components)
- [Message Syntax](#message-syntax)
Expand Down Expand Up @@ -161,9 +164,7 @@ props: Intl.DateTimeFormatOptions &
{
value: any,
format: string,
children: (formattedDate: string | Intl.DateTimeFormatPart[]) =>
ReactElement,
shouldFormatToParts: boolean,
children: (formattedDate: string) => ReactElement,
};
```

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

**shouldFormatToParts**
### `FormattedDateParts`

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)

**Props:**

```ts
props: Intl.DateTimeFormatOptions &
{
value: any,
format: string,
children: (parts: Intl.DateTimeFormatPart[]) => ReactElement,
};
```

```tsx
<FormattedDate
<FormattedDateParts
value={new Date(1459832991883)}
year="numeric"
month="long"
day="2-digit"
shouldFormatToParts={true}
/>
>
{parts => (
<>
<b>{parts[0].value}</b>
{parts[1].value}
<small>{parts[2].value}</small>
</>
)}
</FormattedDate>
```

```html
<span>April 05, 2016</span>
<span> <b>April</b> <small>05</small> </span>
```

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

### `FormattedTimeParts`

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)

**Props:**

```ts
props: Intl.DateTimeFormatOptions &
{
value: any,
format: string,
children: (parts: Intl.DateTimeFormatPart[]) => ReactElement,
};
```

```tsx
<FormattedTimeParts value={new Date(1459832991883)}>
{parts => (
<>
<b>{parts[0].value}</b>
{parts[1].value}
<small>{parts[2].value}</small>
</>
)}
</FormattedDate>
```

```html
<span> <b>01</b>:<small>09</small> </span>
```

### `FormattedRelativeTime`

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

### `FormattedNumberParts`

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)

**Props:**

```ts
props: NumberFormatOptions &
{
value: number,
format: string,
children: (parts: Intl.NumberFormatPart[]) => ReactElement,
};
```

**Example:**

```tsx
<FormattedNumberParts value={1000}>
{parts => (
<>
<b>{parts[0].value}</b>
{parts[1].value}
<small>{parts[2].value}</small>
</>
)}
</FormattedNumberParts>
```

```html
<span> <b>1</b>,<small>000</small> </span>
```

### `FormattedPlural`

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`.
Expand Down
24 changes: 20 additions & 4 deletions examples/TimeZone.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as React from 'react';
import {FormattedDate, FormattedTime, IntlProvider} from '../';
import {
FormattedDate,
FormattedTime,
IntlProvider,
FormattedDateParts,
} from '../';

interface Props {
currentTime?: Date | number;
Expand All @@ -13,9 +18,20 @@ const App: React.FC<Props> = ({currentTime = Date.now()}) => {
<br />
The time in Tokyo is: <FormattedTime value={currentTime} />
<br />
<FormattedDate value={currentTime} shouldFormatToParts>
{parts => <>{JSON.stringify(parts)}</>}
</FormattedDate>
<FormattedDateParts
value={new Date(1459832991883)}
year="numeric"
month="long"
day="2-digit"
>
{(parts: Intl.DateTimeFormatPart[]) => (
<>
<b>{parts[0].value}</b>
{parts[1].value}
<small>{parts[2].value}</small>
</>
)}
</FormattedDateParts>
</p>
</IntlProvider>
);
Expand Down
124 changes: 83 additions & 41 deletions src/components/createFormattedComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,113 @@
import * as React from 'react';
import {invariantIntlContext} from '../utils';
import {invariantIntlContext, createError} from '../utils';
import {IntlShape, FormatDateOptions, FormatNumberOptions} from '../types';
import {Context} from './injectIntl';
import {formatDateFactory, formatTimeFactory} from '../formatters/dateTime';
import {formatNumberFactory} from '../formatters/number';
// Since rollup cannot deal with namespace being a function,
// this is to interop with TypeScript since `invariant`
// does not export a default
// https://github.com/rollup/rollup/issues/1267
import * as invariant_ from 'invariant';
const invariant: typeof invariant_ = (invariant_ as any).default || invariant_;
import {getFormatter as getDateTimeFormatter} from '../formatters/dateTime';
import {getFormatter as getNumberFormatter} from '../formatters/number';

enum DisplayName {
formatDate = 'FormattedDate',
formatTime = 'FormattedTime',
formatNumber = 'FormattedNumber',
}

enum DisplayNameParts {
formatDate = 'FormattedDateParts',
formatTime = 'FormattedTimeParts',
formatNumber = 'FormattedNumberParts',
}

type Formatter = {
formatDate: FormatDateOptions;
formatTime: FormatDateOptions;
formatNumber: FormatNumberOptions;
};

export default function createFormattedComponent<Name extends keyof Formatter>(
export const FormattedNumberParts: React.FC<
Formatter['formatNumber'] & {
value: Parameters<IntlShape['formatNumber']>[0];

children(val: Intl.NumberFormatPart[]): React.ReactElement | null;
}
> = props => (
<Context.Consumer>
{intl => {
invariantIntlContext(intl);
const {value, children} = props;
let formattedParts: Intl.NumberFormatPart[] = [];
try {
formattedParts = getNumberFormatter(
intl,
intl.formatters.getNumberFormat,
props
).formatToParts(value);
} catch (e) {
intl.onError(createError(`Error formatting number.`, e));
}
return children(formattedParts);
}}
</Context.Consumer>
);
FormattedNumberParts.displayName = 'FormattedNumberParts';

export function createFormattedDateTimePartsComponent<
Name extends keyof Formatter
>(name: Name) {
type FormatFn = IntlShape[Name];
type Props = Formatter[Name] & {
value: Parameters<FormatFn>[0];
children(val: Intl.DateTimeFormatPart[]): React.ReactElement | null;
};

const ComponentParts: React.FC<Props> = props => (
<Context.Consumer>
{intl => {
invariantIntlContext(intl);
const {value, children} = props;
const date = typeof value === 'string' ? new Date(value || 0) : value;
let formattedParts: Intl.DateTimeFormatPart[] = [];
try {
formattedParts = getDateTimeFormatter(
intl,
name === 'formatDate' ? 'date' : 'time',
intl.formatters.getDateTimeFormat,
props
).formatToParts(date as Date);
} catch (e) {
intl.onError(
createError(
`Error formatting ${name === 'formatDate' ? 'date' : 'time'}.`,
e
)
);
}

return children(formattedParts);
}}
</Context.Consumer>
);
ComponentParts.displayName = DisplayNameParts[name];
return ComponentParts;
}

export function createFormattedComponent<Name extends keyof Formatter>(
name: Name
) {
type Options = Formatter[Name];
type FormatFn = IntlShape[Name];
type Props = Options & {
shouldFormatToParts?: boolean;
type Props = Formatter[Name] & {
value: Parameters<FormatFn>[0];
children?: (val: string) => React.ReactElement | null;
children?(val: string): React.ReactElement | null;
};

const Component: React.FC<Props> = props => (
<Context.Consumer>
{intl => {
invariantIntlContext(intl);
let formattedParts;
if (props.shouldFormatToParts) {
if (name === 'formatDate') {
formattedParts = formatDateFactory(
intl,
intl.formatters.getDateTimeFormat
)(props.value, props);
} else if (name === 'formatTime') {
formattedParts = formatTimeFactory(
intl,
intl.formatters.getDateTimeFormat
)(props.value, props);
} else {
formattedParts = formatNumberFactory(
intl,
intl.formatters.getNumberFormat
)(props.value as number, props);
}
invariant(
typeof props.children === 'function',
'render props must be a function when `shouldFormatToParts` is `true`'
);
return props.children!(formattedParts);
}
const formattedValue = intl[name](props.value as any, props);
const {value, children} = props;
const formattedValue = intl[name](value as any, props);

if (typeof props.children === 'function') {
return props.children(formattedValue);
if (typeof children === 'function') {
return children(formattedValue as any);
}
const Text = intl.textComponent || React.Fragment;
return <Text>{formattedValue}</Text>;
Expand Down
29 changes: 20 additions & 9 deletions src/components/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import {
} from '../utils';
import {IntlConfig, IntlShape, Omit, IntlCache} from '../types';
import areIntlLocalesSupported from 'intl-locales-supported';
import {formatNumberFactory} from '../formatters/number';
import {formatRelativeTimeFactory} from '../formatters/relativeTime';
import {formatDateFactory, formatTimeFactory} from '../formatters/dateTime';
import {formatPluralFactory} from '../formatters/plural';
import {formatNumber} from '../formatters/number';
import {formatRelativeTime} from '../formatters/relativeTime';
import {formatDate, formatTime} from '../formatters/dateTime';
import {formatPlural} from '../formatters/plural';
import {formatMessage, formatHTMLMessage} from '../formatters/message';
import * as shallowEquals_ from 'shallow-equal/objects';
const shallowEquals: typeof shallowEquals_ =
Expand Down Expand Up @@ -130,17 +130,28 @@ export function createIntl(
return {
...resolvedConfig,
formatters,
formatNumber: formatNumberFactory(
formatNumber: formatNumber.bind(
null,
resolvedConfig,
formatters.getNumberFormat
),
formatRelativeTime: formatRelativeTimeFactory(
formatRelativeTime: formatRelativeTime.bind(
null,
resolvedConfig,
formatters.getRelativeTimeFormat
),
formatDate: formatDateFactory(resolvedConfig, formatters.getDateTimeFormat),
formatTime: formatTimeFactory(resolvedConfig, formatters.getDateTimeFormat),
formatPlural: formatPluralFactory(
formatDate: formatDate.bind(
null,
resolvedConfig,
formatters.getDateTimeFormat
),
formatTime: formatTime.bind(
null,
resolvedConfig,
formatters.getDateTimeFormat
),
formatPlural: formatPlural.bind(
null,
resolvedConfig,
formatters.getPluralRules
),
Expand Down
Loading

0 comments on commit a1b5ff1

Please sign in to comment.