-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmap.tsx
More file actions
260 lines (246 loc) · 8.98 KB
/
map.tsx
File metadata and controls
260 lines (246 loc) · 8.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import {pipe, String, Struct, type Types} from 'effect'
import type {FC} from 'react'
import {wrapDisplayName} from './displayName.js'
import {
type RenameProps,
type RenameRestProps,
type WithDefaultProp,
type WithRequiredProp,
} from './map/types.js'
import {cloneComponent} from './util/react.js'
export type {
RenameProps,
RenameRestProps,
WithDefaultProp,
WithRequiredProp,
} from './map/types.js'
/**
* Convert a component that takes an optional prop `prop` into a component where
* the prop is required. The opposite of {@link withDefault}.
* @typeParam Prop - Type of prop name. Must exist as optional field on `Base`
* props.
* @param prop - Name of optional prop to convert.
*/
export const requireProp =
<Prop extends string>(prop: Prop) =>
<BaseProps extends {[key in Prop]?: unknown}>(Base: FC<BaseProps>) =>
cloneComponent(
Base,
`requireProp${String.capitalize(prop)}(${Base.displayName ?? Base.name})`,
) as FC<Types.Simplify<WithRequiredProp<Prop, BaseProps>>>
/**
* Convert a component that takes a required prop `prop` into a component where
* the prop is optional by providing a default value. The opposite of {@link requireProp}.
* When the returned component receives the prop, it will use it, but when none
* is given it will use the default given here.
* @typeParam Prop - Type of prop name. Must exist as a required field on `Base`
* props.
* @typeParam Value - Prop type.
* @param prop - Name of required prop to convert.
* @param value - Default value for prop.
*/
export const withDefault =
<Prop extends string, Value>(prop: Prop, value: Value) =>
<BaseProps extends Record<Prop, Value>>(Base: FC<BaseProps>) =>
pipe(
Base,
mapProps(
(props: BaseProps) => ({
...props,
[prop]: prop in props ? (props[prop] ?? value) : value,
}),
`withDefault${String.capitalize(prop)}`,
),
) as FC<Types.Simplify<WithDefaultProp<Prop, Value, BaseProps>>>
/**
* @typeParam Map - Type of new prop name ⇒ base component prop name map.
* @param map - Map of new prop names to mapped base component prop name.
* @param displayName - Optional `displayName` wrapper. Defaults to `renameProps`.
*/
export const renameProps =
<const Map extends Record<string, string>>(
map: Map,
displayName = 'renameProps',
) =>
/* Base component will be given renamed props. */
<BaseProps extends Record<Map[string & keyof Map], unknown>>(
Base: FC<BaseProps>,
) => {
type Props = RenameProps<Map, BaseProps>
const rename = (props: Props): BaseProps => {
const result = {} as BaseProps
for (const [key, value] of Object.entries(props)) {
if (key in map) {
result[map[key as string & keyof Map]] = value as never
} else {
result[key as keyof RenameRestProps<Map, BaseProps>] = props[
key as keyof Props
] as never
}
}
return result
}
return pipe(Base, mapProps(rename, displayName)) as FC<
Types.Simplify<RenameProps<Map, BaseProps>>
>
}
/**
* Rename a single named prop.
* @param oldName - Old prop name. Must be present in the given props.
* @param newName - New prop name. Must be present in the props of the given component.
* @param displayName - Optional `displayName` wrapper. Defaults to
* `renameProp[OLD_NAME][NEW_NAME]`.
* @returns
*/
export const renameProp =
<const O extends string, const N extends string>(
oldName: O,
newName: N,
displayName = `renameProp${String.capitalize(oldName)}${String.capitalize(newName)}`,
) =>
/* Base component will be given renamed prop. */
<Props extends Record<N, unknown>>(
Base: FC<Props>,
): FC<Omit<Props, N> & Record<O, unknown>> => {
const Component = (oldProps: Omit<Props, N> & Record<O, unknown>) => {
const value = oldProps[oldName]
const rest = Struct.omit(oldName)(oldProps) as Omit<Props, O>
const props = {[newName]: value, ...rest} as Props
return <Base {...props} />
}
return pipe(displayName, wrapDisplayName(Component, Base))
}
/** Omit the given prop names from for the given component. */
export const omitProps =
<Props extends object>(Base: FC<Props>, displayName = 'omitProps') =>
<const Names extends readonly [string, ...string[]]>(...names: Names) => {
const Component = (props: Props) => (
<Base {...(Struct.omit(...names)(props) as Props)} />
)
return pipe(displayName, wrapDisplayName(Component, Base))
}
/**
* Lifts a function that maps the prop type `B` into the prop type `A`, into a
* function the maps a component of type `FC<A>` into a component of type
* `FC<B>`.
*
* This is the opposite of how `map` works. For example, `Array.map` lifts:
*
* ```ts
* (A ⇒ B) -into→ (Array<A> ⇒ Array<B>)
* ```
*
* While `mapProps`, called`mapInput` in
* [`effect`](https://github.com/Effect-TS/effect/blob/c407726f79df4a567a9631cddd8effaa16b3535d/packages/effect/src/Predicate.ts#L92),
* and [`contramap` in `@effect/typeclass`](https://github.com/Effect-TS/effect/blob/main/packages/typeclass/src/Contravariant.ts#L13),
* (“contra” means “counter to” in greek), is of type:
*
* ```ts
* (B ⇒ A) -into→ (FC<A> ⇒ FC<B>)
* ```
*
* Useful when:
*
* 1. You have a component that takes props of type `A`.
* 2. But you want a component that takes props of type `B`.
* 3. And the props you have are of type `B`.
* 4. But you do have some way of converting `B` ⇒ `A`.
*
* For example:
*
* ```tsx
* import {mapProps} from 'react-compinators'
* interface B { foo: string }
* interface A { bar: number }
*
* const ComponentA: FC<A> = ({ bar }) => <div>{bar + 1}</div>;
*
* // The function mapping B ⇒ A
* const mapper = (a: B): A => ({ bar: a.foo.length })
*
* // We now have a component of B
* const ComponentB: FC<B> = pipe(ComponentA, mapProps(mapper));
* ```
*/
export const mapProps =
<B extends object, A extends object>(
f: (props: B) => A,
displayName = 'mapProps',
) =>
(Base: FC<A>): FC<B> => {
const Component = (props: B) => <Base {...f(props)} />
return pipe(displayName, wrapDisplayName(Component, Base))
}
/**
* Just like `mapProps` but for a single prop. The component will be given its
* props unmodified, except for a single prop where the value will be the result
* of the given function. The given function is given the original prop value.
* @typeParam Prop - Type of prop name.
* @typeParam B - Type of given prop value.
* @typeParam A - Type of prop value as expected by the base component.
* @typeParam Props - Prop type of the base component.
* @param f - The function that will be run over the prop value.
* @param prop - The prop name to be mapped over.
* @param displayName - Optional `displayName` wrapper. Defaults to `mapProp`.
*/
export const mapProp =
<Prop extends string, B, A>(
f: (b: B) => A,
prop: Prop,
displayName = `mapProp${String.capitalize(prop)}`,
) =>
<Props extends Record<Prop, A>>(
/** Base component that accepts a prop of type `Props`. */
Base: FC<Props>,
): FC<Omit<Props, Prop> & Record<Prop, B>> => {
type NewProps = Omit<Props, Prop> & Record<Prop, B>
const mapper = (props: NewProps) =>
Object.assign({[prop]: f(props[prop])}, Struct.omit(props, prop)) as Props
return mapProps<NewProps, Props>(mapper, displayName)(Base)
}
/**
* Modify the props input of the given component by applying a function of type
* `<A>(a: A) => A` over a named prop value. Useful when you can compute the new
* value from the old one and nothing else, and you do not need to change the
* prop type.
* @typeParam Prop - Type of modified prop name.
* @typeParam Value - Modified prop value type.
* @param propName - Name of prop to modify.
* @param modify - The mapping function will be applied to the prop value.
* @param maybeNameWrapper - Optional `displayName` wrapper will be added to
* base component `displayName`. Default is computed modified given prop name.
*/
export const modProp =
<Prop extends string, Value>(
propName: Prop,
modify: (value: Value) => Value,
maybeNameWrapper?: string,
) =>
<Props extends Record<Prop, Value>>(
/** Component to modify. */
Base: FC<Props>,
): typeof Base => {
const Component = (props: Props) => (
<Base {...({...props, [propName]: modify(props[propName])} as Props)} />
)
return wrapDisplayName(
Component,
Base,
)(maybeNameWrapper ?? `modProp${String.capitalize(propName)}`)
}
/** Just like `modProp` but for _optional_ props. */
export const modOptionalProp =
<Prop extends string>(propName: Prop) =>
<Value,>(modify: (value?: Value) => Value, maybeNameWrapper?: string) =>
<Props extends Partial<Record<Prop, Value>>>(
/** Component to modify. */
Base: FC<Props>,
): typeof Base => {
const Component = (props: Props) => (
<Base {...({...props, [propName]: modify(props[propName])} as Props)} />
)
return wrapDisplayName(
Component,
Base,
)(maybeNameWrapper ?? `modProp${String.capitalize(propName)}`)
}