Skip to content
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
93 changes: 93 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type ComponentClass,
type HTMLProps,
type ReactNode,
type JSX,
} from 'react';

/**
Expand Down Expand Up @@ -358,5 +359,97 @@ body.dark-mode {
*/
export class BodyClass extends ReactComponent<ElementClassProps> {}

/**
Inserts a separator between each element of the children.

@param children - The elements to intersperse with separators.
@param separator - The separator to insert between elements. Can be a ReactNode or a function that returns a ReactNode.

@example
```
import {intersperse} from 'react-extras';

const items = ['Apple', 'Orange', 'Banana'];
const list = intersperse(
items.map(item => <li key={item}>{item}</li>),
', '
);
// => [<li>Apple</li>, ', ', <li>Orange</li>, ', ', <li>Banana</li>]
```

@example
```
import {intersperse} from 'react-extras';

const items = ['Apple', 'Orange', 'Banana'];
const list = intersperse(
items.map(item => <li key={item}>{item}</li>),
(index, count) => index === count - 2 ? ' and ' : ', '
);
// => [<li>Apple</li>, ', ', <li>Orange</li>, ' and ', <li>Banana</li>]
```
*/
export function intersperse(
children: ReactNode,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, shouldn't this be array?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

separator?: ReactNode | ((index: number, count: number) => ReactNode)
): ReactNode[];

type JoinProps = {
/**
The separator to insert between elements.

Can be a ReactNode or a function that returns a ReactNode.

@default ', '
*/
readonly separator?: ReactNode | ((index: number, count: number) => ReactNode);

/**
The elements to join with separators.
*/
readonly children: ReactNode;
Comment thread
sindresorhus marked this conversation as resolved.
};

/**
React component that renders the children with a separator between each element.

@example
```
import {Join} from 'react-extras';

<Join>
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</Join>
// => <li>Apple</li>, <li>Orange</li>, <li>Banana</li>
```

@example
```
import {Join} from 'react-extras';

<Join separator=" | ">
<a href="#">Home</a>
<a href="#">About</a>
<a href="#">Contact</a>
</Join>
// => <a href="#">Home</a> | <a href="#">About</a> | <a href="#">Contact</a>
```

@example
```
import {Join} from 'react-extras';

<Join separator={(index, count) => index === count - 2 ? ' and ' : ', '}>
<span>Apple</span>
<span>Orange</span>
<span>Banana</span>
</Join>
// => <span>Apple</span>, <span>Orange</span> and <span>Banana</span>
```
*/
export function Join(props: JoinProps): JSX.Element;

export {default as classNames} from '@sindresorhus/class-names';
export {default as autoBind} from 'auto-bind/react';
82 changes: 82 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,88 @@ body.dark-mode {
}
```

### intersperse(children, separator?)

Inserts a separator between each element of the children.

#### children

Type: `ReactNode`

The elements to intersperse with separators.

#### separator

Type: `ReactNode | ((index: number, count: number) => ReactNode)`\
Default: `', '`

The separator to insert between elements. Can be a React node or a function that returns a React node.

```jsx
import {intersperse} from 'react-extras';

const items = ['Apple', 'Orange', 'Banana'];
const list = intersperse(
items.map(item => <li key={item}>{item}</li>),
', '
);
// => [<li>Apple</li>, ', ', <li>Orange</li>, ', ', <li>Banana</li>]
```

With a function separator:

```jsx
import {intersperse} from 'react-extras';

const items = ['Apple', 'Orange', 'Banana'];
const list = intersperse(
items.map(item => <li key={item}>{item}</li>),
(index, count) => index === count - 2 ? ' and ' : ', '
);
// => [<li>Apple</li>, ', ', <li>Orange</li>, ' and ', <li>Banana</li>]
```

### `<Join/>`

React component that renders the children with a separator between each element.

```jsx
import {Join} from 'react-extras';

<Join>
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</Join>
// => <li>Apple</li>, <li>Orange</li>, <li>Banana</li>
```

With a custom separator:

```jsx
import {Join} from 'react-extras';

<Join separator=" | ">
<a href="#">Home</a>
<a href="#">About</a>
<a href="#">Contact</a>
</Join>
// => <a href="#">Home</a> | <a href="#">About</a> | <a href="#">Contact</a>
```

With a function separator:

```jsx
import {Join} from 'react-extras';

<Join separator={(index, count) => index === count - 2 ? ' and ' : ', '}>
<span>Apple</span>
<span>Orange</span>
<span>Banana</span>
</Join>
// => <span>Apple</span>, <span>Orange</span> and <span>Banana</span>
```

### isStatelessComponent(Component)

Returns a boolean of whether the given `Component` is a [functional stateless component](https://javascriptplayground.com/functional-stateless-components-react/).
Expand Down
1 change: 1 addition & 0 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export {default as For} from './for.js';
export {default as Image} from './image.js';
export {default as RootClass} from './root-class.js';
export {default as BodyClass} from './body-class.js';
export {intersperse, Join} from './intersperse.js';
46 changes: 46 additions & 0 deletions source/intersperse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, {Children, Fragment} from 'react';

export function intersperse(
children,
separator = ', ',
) {
const items = Children.toArray(children);

const count = items.length;

// Short-circuit if no separators needed or separator is falsy (and not a function)
// Note: undefined check won't trigger due to default parameter, but null and false will
if (count <= 1 || separator === null || separator === false) {
return items;
}

const result = [];
for (const [index, child] of items.entries()) {
result.push(child);

// Early return on last item
if (index === count - 1) {
return result;
}

const separatorNode
= typeof separator === 'function' ? separator(index, count) : separator;

if (separatorNode === undefined || separatorNode === null || separatorNode === false) {
continue;
}

result.push(
<Fragment key={`__react_extras_separator_${index}`}>{separatorNode}</Fragment>,
);
}

return result;
Comment thread
sindresorhus marked this conversation as resolved.
}

export function Join({
separator,
children,
}) {
return <>{intersperse(children, separator)}</>;
}
44 changes: 44 additions & 0 deletions test-d/index.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
Image,
RootClass,
BodyClass,
intersperse,
Join,
} from '../index.js';

class Bar extends ReactComponent {
Expand Down Expand Up @@ -85,3 +87,45 @@ const RootTest = (props: {isDarkMode: boolean}) => (
<BodyClass add='logged-in paid-user' remove='promo'/>
</If>
);

// Test intersperse function
const items = ['Apple', 'Orange', 'Banana'].map(item => <li key={item}>{item}</li>);

// Test with array of ReactNodes
expectType<React.ReactNode[]>(intersperse(items, ', '));

// Test with single ReactNode
expectType<React.ReactNode[]>(intersperse(<div>single</div>, ', '));

// Test with separator function
expectType<React.ReactNode[]>(intersperse(items, (index, count) =>
index === count - 2 ? ' and ' : ', ',
));

// Test with no separator
expectType<React.ReactNode[]>(intersperse(items));

// Test Join component
const JoinTest = (
<Join>
<li>Apple</li>
<li>Orange</li>
<li>Banana</li>
</Join>
);

const JoinWithCustomSeparator = (
<Join separator=' | '>
<a href='#'>Home</a>
<a href='#'>About</a>
<a href='#'>Contact</a>
</Join>
);

const JoinWithFunctionSeparator = (
<Join separator={(index, count) => index === count - 2 ? ' and ' : ', '}>
<span>Apple</span>
<span>Orange</span>
<span>Banana</span>
</Join>
);
Loading