-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
151 lines (118 loc) · 3.53 KB
/
index.ts
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
import React, {useLayoutEffect} from "react"
import {
createDomain,
createEvent,
createStore,
Event,
Store,
Domain,
} from "effector"
import {createGate, useGate, useUnit} from "effector-react"
// Types
type Params = {
name?: string
domain?: Domain
}
type FillProps = {
children: React.ReactElement
order?: Order
}
type Order = number
type FillPayload = { node: React.ReactElement; order: Order }
type RemovePayload = Order
type EffectorUnits<Props> = {
add: Event<FillPayload>
remove: Event<RemovePayload>
set: Event<FillPayload[]>
props: Store<Props>
fills: Store<Array<React.ReactElement>>
}
type Slot<P> = React.FC<FillProps> & {
Host: React.FC<React.PropsWithChildren<P>>
units: EffectorUnits<P>
useProps: () => P
}
// Used for generating unique keys for slots names
let nextSlot = 0
export function createSlot<Props>(params: Params = {}): Slot<Props> {
const {name = `slot-${nextSlot++}`, domain = createDomain(name)} = params
// stores fills
const $fills = createStore(Array<React.ReactElement>(), {
domain,
name: `${name}::fills`,
})
// events for fills
const add = createEvent<FillPayload>({domain, name: `${name}::add`})
const set = createEvent<FillPayload[]>({domain, name: `${name}::set`})
const remove = createEvent<RemovePayload>({domain, name: `${name}::remove`})
// reducer for fills
$fills
.on(add, (list, fill) => {
list[fill.order] = fill.node
return [...list]
})
.on(remove, (list, order) => {
delete list[order]
return [...list]
})
.on(set, (_, fills) => fills.map((fill) => fill.node))
// Gate for passing props to the slot
const PropsGate = createGate<Props>({
domain,
name: `${name}::props_gate`,
})
// Host component used for rendering fills
const Host: Slot<Props>["Host"] = (props) => {
const {children, ...rest} = props
const fills = useUnit($fills)
// Pass props to the gate
useGate(PropsGate, rest as Props)
const hasFills = fills.some(Boolean)
// If there are no fills, render the default children
if (!hasFills) {
return children
}
// Render fills
return React.Children.map(fills, (fill, index) => {
if (!fill) {
return null
}
return React.cloneElement(fill, {
key: fill.key ?? index,
})
})
}
// Used to keep track of the order of fills
let nextOrder = 0
// Slot component used for filling the slot
const Slot: Slot<Props> = (props) => {
const {children: node, order: forcedOrder} = props
const orderRef = React.useRef(forcedOrder)
// If the order is not forced, generate a new order
if (orderRef.current === undefined) {
orderRef.current = nextOrder++
}
// Add fill to the slot on mount and remove on unmount
useLayoutEffect(() => {
const order = orderRef.current as number
add({node, order})
return () => {
remove(order)
}
}, [node, orderRef])
return null
}
// Expose host component, hooks and units
Slot.Host = Host
Slot.useProps = () => {
return useUnit(PropsGate.state)
}
Slot.units = {
add,
remove,
set,
fills: $fills,
props: PropsGate.state,
}
return Slot
}