Skip to content

Commit 3f9e74e

Browse files
committed
fix: mapper function support typings and trade-offs
1 parent 29e9a1d commit 3f9e74e

File tree

3 files changed

+105
-43
lines changed

3 files changed

+105
-43
lines changed

src/index.test.tsx

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,57 @@ test('return one component with children props as function', () => {
2323
})
2424

2525
test('rendering children component', () => {
26-
const Foo = ({ children }) =>
27-
children && typeof children === 'function' && children('foo')
26+
const Foo = ({ children, tor }) =>
27+
children && typeof children === 'function' && children(tor + 'foo')
2828

29-
const Bar = ({ children }) =>
30-
children && typeof children === 'function' && children('bar')
29+
const Bar = ({ render, tor }) =>
30+
render && typeof render === 'function' && render(tor + 'bar')
3131

3232
interface RenderProps {
3333
foo: 'foo'
3434
bar: 'bar'
3535
}
3636

37-
const Composed = adopt<RenderProps>({
38-
bar: <Bar />,
39-
foo: <Foo />,
37+
interface Props {
38+
tor: string
39+
}
40+
41+
const Composed = adopt<RenderProps, Props>({
42+
bar: ({ tor, render }) => <Bar tor={tor} render={render} />,
43+
foo: ({ tor, render }) => <Foo tor={tor}>{render}</Foo>,
4044
})
4145

4246
const result = shallow(
43-
<Composed>
47+
<Composed tor="tor">
4448
{props => (
4549
<div>
46-
{props.foo}
47-
{props.bar}
50+
<div>{props.foo}</div>
51+
<div>{props.bar}</div>
4852
</div>
4953
)}
5054
</Composed>
5155
)
5256

5357
expect(result.children().length).toBe(1)
54-
expect(result.html()).toBe('<div>foobar</div>')
58+
expect(result.html()).toBe('<div><div>torfoo</div><div>torbar</div></div>')
5559
})
5660

57-
test('should allow a function as mapper', () => {
61+
test('passing a function', () => {
5862
const Foo = ({ children }) => children('foo')
59-
const foo = jest.fn(({ renderProp }) => <Foo children={renderProp} />)
63+
const foo = jest.fn(({ render }) => <Foo>{render}</Foo>)
64+
const children = jest.fn(() => null)
65+
const Composed = adopt({ foo })
66+
67+
mount(<Composed>{children}</Composed>)
68+
69+
expect(foo).toHaveBeenCalled()
70+
expect(children).toHaveBeenCalledWith({ foo: 'foo' })
71+
})
72+
73+
test('passing a function changing the render prop on mapper', () => {
74+
const Foo = ({ render }) => render('foo')
75+
76+
const foo = jest.fn(({ render }) => <Foo render={render} />)
6077
const children = jest.fn(() => null)
6178
const Composed = adopt({ foo })
6279

@@ -69,7 +86,7 @@ test('should allow a function as mapper', () => {
6986
test('should provide a function mapper with all previous render prop results', () => {
7087
const Foo = ({ children }) => children('foo')
7188
const Bar = ({ children }) => children('bar')
72-
const bar = jest.fn(({ renderProp }) => <Bar children={renderProp} />)
89+
const bar = jest.fn(({ render }) => <Bar>{render}</Bar>)
7390
const children = jest.fn(() => null)
7491

7592
interface RenderProps {
@@ -90,9 +107,20 @@ test('should provide a function mapper with all previous render prop results', (
90107

91108
test('should provide mapper functions with Composed component props', () => {
92109
const Foo = ({ children }) => children('foo')
93-
const foo = jest.fn(({ renderProp }) => <Foo children={renderProp} />)
110+
const foo = jest.fn(({ render }) => <Foo>{render}</Foo>)
94111
const children = jest.fn(() => null)
95-
const Composed = adopt({ foo })
112+
113+
type RenderProps = {
114+
foo: string
115+
}
116+
117+
type Props = {
118+
bar: string
119+
}
120+
121+
const Composed = adopt<RenderProps, Props>({
122+
foo,
123+
})
96124

97125
mount(<Composed bar="bar">{children}</Composed>)
98126

@@ -102,8 +130,8 @@ test('should provide mapper functions with Composed component props', () => {
102130

103131
test('throw with a wrong value on mapper', () => {
104132
expect(() => {
105-
const Composed = adopt({ foo: 'helo' })
106-
return shallow(<Composed>{props => <div>{props.foo}</div>}</Composed>)
133+
const Composed = adopt({ foo: 'helo' } as any)
134+
return shallow(<Composed>{props => <div>foo</div>}</Composed>)
107135
}).toThrowError(
108136
'The render props object mapper just accept valid elements as value'
109137
)

src/index.tsx

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,74 @@
11
import * as React from 'react'
22
import { ComponentType, ReactNode, ReactElement } from 'react'
33

4-
export type ChildrenFn<P> = (props: P) => ReactNode
4+
const { values, keys, assign } = Object
55

6-
export type RPC<Props> = ComponentType<{
7-
children?: ChildrenFn<Props>
8-
}>
6+
export type ChildrenFn<P = any> = (props: P) => ReactNode
97

10-
export type Mapper<R> = Record<keyof R, ReactElement<any> | any>
8+
export type MapperValue =
9+
| ReactElement<any>
10+
| ChildrenFn<{
11+
renderProp?: ChildrenFn
12+
[key: string]: any
13+
}>
1114

12-
const isValidRenderProp = (prop: ReactNode | ChildrenFn): boolean =>
13-
React.isValidElement(prop) || typeof prop === 'function'
15+
export type Mapper<R> = Record<keyof R, MapperValue>
1416

15-
const Children = ({ children }: any) => children()
17+
export type RPC<RenderProps, Props = {}> = ComponentType<
18+
Props & {
19+
children: ChildrenFn<RenderProps>
20+
}
21+
>
22+
23+
const isFn = (val: any): boolean => Boolean(val) && typeof val === 'function'
24+
25+
function omit<R = object>(obj: any, omitProps: string[]): R {
26+
const newObj = keys(obj)
27+
.filter((key: string): boolean => omitProps.indexOf(key) === -1)
28+
.reduce(
29+
(returnObj: any, key: string): R => ({ ...returnObj, [key]: obj[key] }),
30+
{}
31+
)
32+
33+
return newObj as R
34+
}
1635

17-
export function adopt<RP extends Record<string, any>>(
36+
const isValidRenderProp = (prop: ReactNode | ChildrenFn<any>): boolean =>
37+
React.isValidElement(prop) || isFn(prop)
38+
39+
export function adopt<RP extends Record<string, any>, P = {}>(
1840
mapper: Mapper<RP>
19-
): RPC<RP> {
20-
if (!Object.values(mapper).some(isValidRenderProp)) {
41+
): RPC<RP, P> {
42+
if (!values(mapper).some(isValidRenderProp)) {
2143
throw new Error(
2244
'The render props object mapper just accept valid elements as value'
2345
)
2446
}
2547

26-
return Object.keys(mapper).reduce(
27-
(Component: RPC<RP>, key: keyof RP): RPC<RP> => ({ children, ...rest }) => (
28-
<Component>
29-
{props => {
30-
const renderProp = (childProps: any) =>
31-
children(Object.assign({}, props, { [key]: childProps }))
32-
33-
return typeof mapper[key] === 'function'
34-
? mapper[key](Object.assign({}, rest, props, { renderProp }))
35-
: React.cloneElement(mapper[key], { children: renderProp })
36-
}}
37-
</Component>
38-
),
39-
Children
48+
const Children: RPC<RP, P> = ({ children, ...rest }: any) =>
49+
isFn(children) && children(rest)
50+
51+
const reducer = (Component: RPC<RP, P>, key: keyof RP): RPC<RP, P> => ({
52+
children,
53+
...rest
54+
}: {
55+
children: ChildrenFn<RP>
56+
}) => (
57+
<Component {...rest}>
58+
{props => {
59+
const element: any = mapper[key]
60+
const propsWithoutRest = omit<RP>(props, keys(rest))
61+
62+
const render = (cProps: Partial<RP>) =>
63+
isFn(children) &&
64+
children(assign({}, propsWithoutRest, { [key]: cProps }))
65+
66+
return isFn(element)
67+
? React.createElement(element, assign({}, rest, props, { render }))
68+
: React.cloneElement(element, null, render)
69+
}}
70+
</Component>
4071
)
72+
73+
return keys(mapper).reduce(reducer, Children)
4174
}

tslint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"prefer-object-spread": false,
1212
"no-console": false,
1313
"no-shadowed-variable": false,
14+
"max-classes-per-file": false,
1415

1516
// Recommended built-in rules
1617
"no-var-keyword": true,

0 commit comments

Comments
 (0)