Skip to content

Commit 4664d25

Browse files
Add intersperse function and Join component (#25)
Fixes #24 Co-authored-by: Richie Bendall <richiebendall@gmail.com>
1 parent 463f76a commit 4664d25

6 files changed

Lines changed: 357 additions & 0 deletions

File tree

index.d.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type ComponentClass,
44
type HTMLProps,
55
type ReactNode,
6+
type JSX,
67
} from 'react';
78

89
/**
@@ -358,5 +359,97 @@ body.dark-mode {
358359
*/
359360
export class BodyClass extends ReactComponent<ElementClassProps> {}
360361

362+
/**
363+
Inserts a separator between each element of the children.
364+
365+
@param children - The elements to intersperse with separators.
366+
@param separator - The separator to insert between elements. Can be a ReactNode or a function that returns a ReactNode.
367+
368+
@example
369+
```
370+
import {intersperse} from 'react-extras';
371+
372+
const items = ['Apple', 'Orange', 'Banana'];
373+
const list = intersperse(
374+
items.map(item => <li key={item}>{item}</li>),
375+
', '
376+
);
377+
// => [<li>Apple</li>, ', ', <li>Orange</li>, ', ', <li>Banana</li>]
378+
```
379+
380+
@example
381+
```
382+
import {intersperse} from 'react-extras';
383+
384+
const items = ['Apple', 'Orange', 'Banana'];
385+
const list = intersperse(
386+
items.map(item => <li key={item}>{item}</li>),
387+
(index, count) => index === count - 2 ? ' and ' : ', '
388+
);
389+
// => [<li>Apple</li>, ', ', <li>Orange</li>, ' and ', <li>Banana</li>]
390+
```
391+
*/
392+
export function intersperse(
393+
children: ReactNode,
394+
separator?: ReactNode | ((index: number, count: number) => ReactNode)
395+
): ReactNode[];
396+
397+
type JoinProps = {
398+
/**
399+
The separator to insert between elements.
400+
401+
Can be a ReactNode or a function that returns a ReactNode.
402+
403+
@default ', '
404+
*/
405+
readonly separator?: ReactNode | ((index: number, count: number) => ReactNode);
406+
407+
/**
408+
The elements to join with separators.
409+
*/
410+
readonly children: ReactNode;
411+
};
412+
413+
/**
414+
React component that renders the children with a separator between each element.
415+
416+
@example
417+
```
418+
import {Join} from 'react-extras';
419+
420+
<Join>
421+
<li>Apple</li>
422+
<li>Orange</li>
423+
<li>Banana</li>
424+
</Join>
425+
// => <li>Apple</li>, <li>Orange</li>, <li>Banana</li>
426+
```
427+
428+
@example
429+
```
430+
import {Join} from 'react-extras';
431+
432+
<Join separator=" | ">
433+
<a href="#">Home</a>
434+
<a href="#">About</a>
435+
<a href="#">Contact</a>
436+
</Join>
437+
// => <a href="#">Home</a> | <a href="#">About</a> | <a href="#">Contact</a>
438+
```
439+
440+
@example
441+
```
442+
import {Join} from 'react-extras';
443+
444+
<Join separator={(index, count) => index === count - 2 ? ' and ' : ', '}>
445+
<span>Apple</span>
446+
<span>Orange</span>
447+
<span>Banana</span>
448+
</Join>
449+
// => <span>Apple</span>, <span>Orange</span> and <span>Banana</span>
450+
```
451+
*/
452+
export function Join(props: JoinProps): JSX.Element;
453+
361454
export {default as classNames} from '@sindresorhus/class-names';
362455
export {default as autoBind} from 'auto-bind/react';

readme.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,88 @@ body.dark-mode {
245245
}
246246
```
247247

248+
### intersperse(children, separator?)
249+
250+
Inserts a separator between each element of the children.
251+
252+
#### children
253+
254+
Type: `ReactNode`
255+
256+
The elements to intersperse with separators.
257+
258+
#### separator
259+
260+
Type: `ReactNode | ((index: number, count: number) => ReactNode)`\
261+
Default: `', '`
262+
263+
The separator to insert between elements. Can be a React node or a function that returns a React node.
264+
265+
```jsx
266+
import {intersperse} from 'react-extras';
267+
268+
const items = ['Apple', 'Orange', 'Banana'];
269+
const list = intersperse(
270+
items.map(item => <li key={item}>{item}</li>),
271+
', '
272+
);
273+
// => [<li>Apple</li>, ', ', <li>Orange</li>, ', ', <li>Banana</li>]
274+
```
275+
276+
With a function separator:
277+
278+
```jsx
279+
import {intersperse} from 'react-extras';
280+
281+
const items = ['Apple', 'Orange', 'Banana'];
282+
const list = intersperse(
283+
items.map(item => <li key={item}>{item}</li>),
284+
(index, count) => index === count - 2 ? ' and ' : ', '
285+
);
286+
// => [<li>Apple</li>, ', ', <li>Orange</li>, ' and ', <li>Banana</li>]
287+
```
288+
289+
### `<Join/>`
290+
291+
React component that renders the children with a separator between each element.
292+
293+
```jsx
294+
import {Join} from 'react-extras';
295+
296+
<Join>
297+
<li>Apple</li>
298+
<li>Orange</li>
299+
<li>Banana</li>
300+
</Join>
301+
// => <li>Apple</li>, <li>Orange</li>, <li>Banana</li>
302+
```
303+
304+
With a custom separator:
305+
306+
```jsx
307+
import {Join} from 'react-extras';
308+
309+
<Join separator=" | ">
310+
<a href="#">Home</a>
311+
<a href="#">About</a>
312+
<a href="#">Contact</a>
313+
</Join>
314+
// => <a href="#">Home</a> | <a href="#">About</a> | <a href="#">Contact</a>
315+
```
316+
317+
With a function separator:
318+
319+
```jsx
320+
import {Join} from 'react-extras';
321+
322+
<Join separator={(index, count) => index === count - 2 ? ' and ' : ', '}>
323+
<span>Apple</span>
324+
<span>Orange</span>
325+
<span>Banana</span>
326+
</Join>
327+
// => <span>Apple</span>, <span>Orange</span> and <span>Banana</span>
328+
```
329+
248330
### isStatelessComponent(Component)
249331

250332
Returns a boolean of whether the given `Component` is a [functional stateless component](https://javascriptplayground.com/functional-stateless-components-react/).

source/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export {default as For} from './for.js';
2525
export {default as Image} from './image.js';
2626
export {default as RootClass} from './root-class.js';
2727
export {default as BodyClass} from './body-class.js';
28+
export {intersperse, Join} from './intersperse.js';

source/intersperse.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, {Children, Fragment} from 'react';
2+
3+
export function intersperse(
4+
children,
5+
separator = ', ',
6+
) {
7+
const items = Children.toArray(children);
8+
9+
const count = items.length;
10+
11+
// Short-circuit if no separators needed or separator is falsy (and not a function)
12+
// Note: undefined check won't trigger due to default parameter, but null and false will
13+
if (count <= 1 || separator === null || separator === false) {
14+
return items;
15+
}
16+
17+
const result = [];
18+
for (const [index, child] of items.entries()) {
19+
result.push(child);
20+
21+
// Early return on last item
22+
if (index === count - 1) {
23+
return result;
24+
}
25+
26+
const separatorNode
27+
= typeof separator === 'function' ? separator(index, count) : separator;
28+
29+
if (separatorNode === undefined || separatorNode === null || separatorNode === false) {
30+
continue;
31+
}
32+
33+
result.push(
34+
<Fragment key={`__react_extras_separator_${index}`}>{separatorNode}</Fragment>,
35+
);
36+
}
37+
38+
return result;
39+
}
40+
41+
export function Join({
42+
separator,
43+
children,
44+
}) {
45+
return <>{intersperse(children, separator)}</>;
46+
}

test-d/index.test-d.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
Image,
1414
RootClass,
1515
BodyClass,
16+
intersperse,
17+
Join,
1618
} from '../index.js';
1719

1820
class Bar extends ReactComponent {
@@ -85,3 +87,45 @@ const RootTest = (props: {isDarkMode: boolean}) => (
8587
<BodyClass add='logged-in paid-user' remove='promo'/>
8688
</If>
8789
);
90+
91+
// Test intersperse function
92+
const items = ['Apple', 'Orange', 'Banana'].map(item => <li key={item}>{item}</li>);
93+
94+
// Test with array of ReactNodes
95+
expectType<React.ReactNode[]>(intersperse(items, ', '));
96+
97+
// Test with single ReactNode
98+
expectType<React.ReactNode[]>(intersperse(<div>single</div>, ', '));
99+
100+
// Test with separator function
101+
expectType<React.ReactNode[]>(intersperse(items, (index, count) =>
102+
index === count - 2 ? ' and ' : ', ',
103+
));
104+
105+
// Test with no separator
106+
expectType<React.ReactNode[]>(intersperse(items));
107+
108+
// Test Join component
109+
const JoinTest = (
110+
<Join>
111+
<li>Apple</li>
112+
<li>Orange</li>
113+
<li>Banana</li>
114+
</Join>
115+
);
116+
117+
const JoinWithCustomSeparator = (
118+
<Join separator=' | '>
119+
<a href='#'>Home</a>
120+
<a href='#'>About</a>
121+
<a href='#'>Contact</a>
122+
</Join>
123+
);
124+
125+
const JoinWithFunctionSeparator = (
126+
<Join separator={(index, count) => index === count - 2 ? ' and ' : ', '}>
127+
<span>Apple</span>
128+
<span>Orange</span>
129+
<span>Banana</span>
130+
</Join>
131+
);

0 commit comments

Comments
 (0)