...
183 | ...
186 | ```
187 |
188 | Example Usage of `LayoutDefault`:
189 |
190 | ```jsx
191 | /*
192 | A page component that uses LayoutDefault to structure its content. This
193 | will render a page that looks like this:
194 |
195 | ...
232 | ```
233 |
234 | ...or just the `inner` slot.
235 |
236 | ```jsx
237 | ....
238 | ```
239 |
240 | But if the `outer` slot is overrdden then the entirety of its contents will be
241 | replaced.
242 |
243 | ### slot(name, children)
244 |
245 | This function will pull out the children of any React subtree designated by the
246 | `slot` prop that matches the `name` argument. This function will not render a
247 | root node at all, this is left up to the parent component to provide. This gives
248 | you more control over a slot's root element.
249 |
250 | ```jsx
251 | ...
261 |
262 | // ...or just replace the copyright year
263 | with className, id and data- attributes', function () {
9 | const id = 'my-id'
10 | const name = 'MySlot'
11 | const className = 'testing'
12 | const dataset = { target: '#thing', scroll: 'spy' }
13 | const dataAttributes = prefixKeys(dataset, 'data-')
14 | const wrapper = shallow(
15 |
20 | {''}
21 |
22 | )
23 |
24 | assert.ok(wrapper.equals(
25 |
29 | {''}
30 |
31 | ))
32 | })
33 |
34 | it('should render with default content', function () {
35 | const wrapper = render(
36 |
37 |
Layout
38 |
39 | Hello World!
40 |
41 |
42 | The Footer
43 |
44 |
45 | {''}
46 |
47 |
48 | )
49 |
50 | assert.strictEqual(wrapper.html(), [
51 | '
',
52 | '
Layout
',
53 | '
',
54 | 'Hello World!',
55 | '
',
56 | '',
59 | '
',
60 | '
'
61 | ].join(''))
62 | })
63 |
64 | it('should render with concrete and default content', function () {
65 | const LayoutDefault = props => {
66 | const { children } = props
67 | return (
68 |
69 |
70 | Hello World!
71 |
72 |
73 | The Footer
74 |
75 |
76 |
77 | )
78 | }
79 | const wrapper = render(
80 |
81 |
82 | The New Footer
83 |
84 |
85 | Gonzo
86 |
87 |
88 | )
89 |
90 | assert.strictEqual(wrapper.html(), [
91 | '
',
92 | '
',
93 | 'Hello World!',
94 | '
',
95 | '',
98 | '',
101 | '
'
102 | ].join(''))
103 | })
104 |
105 | it('should render unamed default slot with concrete content', function () {
106 | const LayoutDefault = props => {
107 | const { children } = props
108 | return (
109 |
110 |
111 | Hello World!
112 |
113 |
114 | The Footer
115 |
116 |
117 | )
118 | }
119 | const wrapper = render(
120 |
121 |
122 | The New Body
123 |
124 |
125 | )
126 |
127 | assert.strictEqual(wrapper.html(), [
128 | '
',
129 | '
',
130 | 'The New Body',
131 | '
',
132 | '',
135 | '
'
136 | ].join(''))
137 | })
138 |
139 | it('should render named default slot with concrete content', function () {
140 | const LayoutDefault = props => {
141 | const { children } = props
142 | return (
143 |
144 |
145 | Hello World!
146 |
147 |
148 | The Footer
149 |
150 |
151 | )
152 | }
153 | const wrapper = render(
154 |
155 |
156 | The New Body
157 |
158 |
159 | )
160 |
161 | assert.strictEqual(wrapper.html(), [
162 | '
',
163 | '
',
164 | 'The New Body',
165 | '
',
166 | '',
169 | '
'
170 | ].join(''))
171 | })
172 |
173 | it('should render default slot with unslotted concrete content', function () {
174 | const LayoutDefault = props => {
175 | const { children } = props
176 | return (
177 |
178 |
179 | Hello World!
180 |
181 |
182 | The Footer
183 |
184 |
185 | )
186 | }
187 | const wrapper = render(
188 |
189 | The New Body
190 |
191 | The New Footer
192 |
193 | Headline
194 |
195 | )
196 |
197 | assert.strictEqual(wrapper.html(), [
198 | '
',
199 | '
',
200 | '
The New Body
',
201 | '
Headline
',
202 | '
',
203 | '',
206 | '
'
207 | ].join(''))
208 | })
209 |
210 | it('should render nested
elements', function () {
211 | const Footer = props => ()
212 | class Body extends React.Component {
213 | render () {
214 | return ()
215 | }
216 | }
217 | const LayoutDefault = props => {
218 | const { children } = props
219 | return (
220 |
221 |
222 | Hello World!
223 |
224 |
225 |
226 | The Footer
227 |
228 |
229 | )
230 | }
231 | const wrapper = render(
232 |
233 |
234 | The New Body
235 |
236 | Boom
237 |
238 | )
239 |
240 | assert.strictEqual(wrapper.html(), [
241 | '',
242 | '',
243 | 'The New Body',
244 | '',
245 | '',
249 | '
'
250 | ].join(''))
251 | })
252 |
253 | it('should override nested elements when parent slots are overridden', function () {
254 | const Footer = props => ()
255 | class Body extends React.Component {
256 | render () {
257 | return ()
258 | }
259 | }
260 | const LayoutDefault = props => {
261 | const { children } = props
262 | return (
263 |
264 |
265 | Hello World!
266 |
267 |
268 |
269 | The Footer
270 |
271 |
272 | )
273 | }
274 | const wrapper = render(
275 |
276 |
277 | The New Body
278 |
279 | The Footer Boom
280 |
281 | )
282 |
283 | assert.strictEqual(wrapper.html(), [
284 | '',
285 | '',
286 | 'The New Body',
287 | '',
288 | '',
291 | '
'
292 | ].join(''))
293 | })
294 |
295 | it('should render nested layouts as expected', function () {
296 | const LayoutDefault = props => {
297 | const { children } = props
298 | return (
299 |
300 |
301 | Hello World!
302 |
303 |
304 | The Footer
305 |
306 |
307 | )
308 | }
309 | const wrapper = render(
310 |
311 |
312 |
The New Body
313 |
314 |
315 | Hello Footer!
316 |
317 |
318 |
319 |
320 | )
321 |
322 | assert.strictEqual(wrapper.html(), [
323 | '',
324 | '
',
325 | '
The New Body',
326 | '
',
327 | '
',
328 | 'Hello World!',
329 | '
',
330 | '',
333 | '
',
334 | '
',
335 | '',
338 | '
'
339 | ].join(''))
340 | })
341 | })
342 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as slot } from './renderSlot'
2 | export { default as Slot } from './Slot'
3 |
--------------------------------------------------------------------------------
/src/mergeProps.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Merges two React property objects into a new object and returns it. The
3 | * property names in the ignore array option will not be merged from the second
4 | * property object.
5 | *
6 | * Example:
7 | * const p mergeProps(
8 | * { id: 'id', className: 'one' },
9 | * { className: 'two three one', id: 'id2 },
10 | * { ignore: [ 'id' ] }
11 | * )
12 | * // p will be: { id: 'id', className: 'one two three' }
13 | *
14 | * @param {{[key:string]:any}} a
15 | * @param {{[key:string]:any}} b
16 | * @return {{[key:string]:any}}
17 | */
18 | export default function mergeProps (a, b, { ignore = [] } = {}) {
19 | b = Object.assign({}, b)
20 |
21 | ignore.forEach(prop => delete b[prop])
22 | const className = typeof b.className === 'string'
23 | ? b.className.split(' ')
24 | : b.className
25 | delete b.className
26 |
27 | if (a.className || className) {
28 | a.className = [].concat(
29 | typeof a.className === 'string'
30 | ? a.className.split(' ')
31 | : a.className
32 | )
33 | .concat(className)
34 | .filter(Boolean)
35 | .reduce((a, b) => a.indexOf(b) < 0 ? a.concat(b) : a, [])
36 | .join(' ')
37 | }
38 |
39 | return Object.assign({}, a, b)
40 | }
41 |
--------------------------------------------------------------------------------
/src/mergeProps.test.js:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert'
2 | import mergeProps from './mergeProps'
3 |
4 | describe('mergeProps', function () {
5 | it('should merge accept arrays as source objects', function () {
6 | const a = { one: 1, two: 2 }
7 | const b = [ 0, 1, 2 ]
8 | const c = mergeProps(a, b)
9 |
10 | assert.ok(c !== a, 'result is referentially equal to source object')
11 | assert.deepStrictEqual(c, {
12 | one: 1,
13 | two: 2,
14 | 0: 0,
15 | 1: 1,
16 | 2: 2
17 | })
18 |
19 | const e = mergeProps(b, a)
20 | assert.strictEqual(typeof e, 'object')
21 | assert.deepStrictEqual(e, {
22 | one: 1,
23 | two: 2,
24 | 0: 0,
25 | 1: 1,
26 | 2: 2
27 | })
28 | })
29 |
30 | it('should merge properties into a new object', function () {
31 | const a = { one: 1, two: 2 }
32 | const b = { three: 3, four: 4, one: 'one' }
33 | const c = mergeProps(a, b)
34 |
35 | assert.ok(c !== a, 'result is referentially equal to source object')
36 | assert.deepStrictEqual(c, {
37 | one: 'one',
38 | two: 2,
39 | three: 3,
40 | four: 4
41 | })
42 | })
43 |
44 | it('should merge properties into a new object and ignore the specified properties', function () {
45 | const a = { one: 1, two: 2, className: 'a b' }
46 | const b = { three: 3, four: 4, one: 'one', className: 'a c d' }
47 | const c = mergeProps(a, b, { ignore: [ 'one', 'four' ] })
48 |
49 | assert.deepStrictEqual(c, {
50 | one: 1,
51 | two: 2,
52 | three: 3,
53 | className: 'a b c d'
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/src/prefixKeys.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Prefix all keys in the specified object and save them on a new object.
3 | *
4 | * Example:
5 | * const o = prefixKeys({ toggle: true, target: 'spy' }, 'data-')
6 | * // o will be: { 'data-toggle': true, 'data-target': 'spy' }
7 | *
8 | * @param {object} obj The object whoes keys/properties will be prefixed
9 | * @param {string} prefix The prefix to apply to each key in obj
10 | * @param {object} dest The optional destination object to save the new keys to
11 | * @return {{[key:string]:any}}
12 | */
13 | export default function prefixKeys (obj, prefix, dest = {}) {
14 | return Object.keys(obj).reduce((o, key) => {
15 | o[`${prefix}${key}`] = obj[key]
16 | return o
17 | }, dest || {})
18 | }
19 |
--------------------------------------------------------------------------------
/src/prefixKeys.test.js:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert'
2 | import prefixKeys from './prefixKeys'
3 |
4 | describe('prefixKeys', function () {
5 | it('should prefix each key with "data-"', function () {
6 | const src = { one: 'one', two: 'two', three: 37 }
7 | const o = prefixKeys(src, 'data-')
8 |
9 | assert.ok(src !== o, 'result is referentially equal to src object')
10 | assert.deepStrictEqual(o, {
11 | 'data-one': 'one',
12 | 'data-two': 'two',
13 | 'data-three': 37
14 | })
15 | })
16 |
17 | it('should prefix each key with "data-" and save each key to dest', function () {
18 | const src = { one: 'one', two: 'two', three: 37 }
19 | const dest = { prop: 'value' }
20 | const o = prefixKeys(src, 'data-', dest)
21 |
22 | assert.ok(o === dest, 'result is not referentially equal to dest object')
23 | assert.deepStrictEqual(o, {
24 | 'data-one': 'one',
25 | 'data-two': 'two',
26 | 'data-three': 37,
27 | 'prop': 'value'
28 | })
29 | })
30 | })
31 |
--------------------------------------------------------------------------------
/src/renderSlot.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export default function slot (name, children) {
4 | const slotNode = findNamedSlotNode(name, children)
5 |
6 | if (slotNode) {
7 | return slotNode.props.children
8 | } else {
9 | return null
10 | }
11 | }
12 |
13 | function findNamedSlotNode (name, children) {
14 | const node = React.Children.toArray(children).filter(child => {
15 | const node = child
16 | return node.props && node.props.slot === name
17 | })[0]
18 |
19 | return node
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "module": "commonjs",
5 | "target": "es2015",
6 | "moduleResolution": "node",
7 | "jsx": "react",
8 | "reactNamespace": "React",
9 | "outDir": "lib",
10 | "typeRoots": [
11 | "node_modules/@types"
12 | ]
13 | },
14 | "include": [
15 | "./src/**/*.js"
16 | ]
17 | }
--------------------------------------------------------------------------------