Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: array of slots #599

Merged
merged 5 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion docs/guide/advanced/slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ test('layout full page layout', () => {
})
```

## Multiple Slots

You can pass an array of slots, too:

```js
test('layout full page layout', () => {
const wrapper = mount(Layout, {
slots: {
default: [
'<div id="one">One</div>',
'<div id="two">Two</div>'
]
}
})

expect(wrapper.find('#one').exists()).toBe(true)
expect(wrapper.find('#two').exists()).toBe(true)
})
```

## Advanced Usage

You can also pass a render function to a slot mounting option, or even an SFC imported from a `vue` file:
Expand All @@ -104,7 +124,7 @@ test('layout full page layout', () => {
slots: {
header: Header
main: h('div', 'Main content')
footer: '<div>Footer</div>'
footer: '<div>Footer</div>',
}
})

Expand Down
71 changes: 46 additions & 25 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import {
MethodOptions,
AllowedComponentProps,
ComponentCustomProps,
ExtractDefaultPropTypes
ExtractDefaultPropTypes,
Component,
VNode
} from 'vue'

import { config } from './config'
Expand All @@ -27,7 +29,8 @@ import {
isFunctionalComponent,
isHTML,
isObjectComponent,
mergeGlobalProperties
mergeGlobalProperties,
isObject
} from './utils'
import { processSlot } from './utils/compileSlots'
import { createWrapper, VueWrapper } from './vueWrapper'
Expand Down Expand Up @@ -261,6 +264,33 @@ export function mount(
to.appendChild(el)
}

function slotToFunction(slot: Slot): Function {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Function is rarely used as a type. We usually prefer () => string for example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will change this. I suppose it'll be like type SlotLike = () => string | (() => string) | { render: () => string } or something to that meaning.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type here is very complicated, it wants me to specify some very large union of public component instance, functional components... I just removed the entire return type and will let TS figure it out.

if (typeof slot === 'object' && 'render' in slot && slot.render) {
return slot.render
}

if (typeof slot === 'function') {
return slot
}

if (typeof slot === 'object') {
return () => slot
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would write a single if (typeof slot === 'object') and then deal with two cases inside.


if (typeof slot === 'string') {
// if it is HTML we process and render it using h
if (isHTML(slot)) {
return (props: VNodeProps) => h(processSlot(slot), props)
}
// otherwise it is just a string so we just return it as-is
else {
return () => slot
}
}

throw Error(`Invalid slot received.`)
}

// handle any slots passed via mounting options
const slots =
options?.slots &&
Expand All @@ -269,33 +299,24 @@ export function mount(
acc: { [key: string]: Function },
[name, slot]: [string, Slot]
): { [key: string]: Function } => {
// case of an SFC getting passed
if (typeof slot === 'object' && 'render' in slot && slot.render) {
acc[name] = slot.render
return acc
}
if (Array.isArray(slot)) {
const normalized = slot.reduce<Array<Function | VNode>>(
(acc, curr) => {
const toF = slotToFunction(curr)
if (isObject(curr) && 'render' in curr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a hard time understanding the code. Maybe some comments would help. toF could probably be renamed to something more meaningful (slotAsFunction?).

It feels like this check is somewhat redundant with what is done in slotToFunction. I don't know exactly how, but it looks like this should be refactor/simplified.

Copy link
Member Author

@lmiller1990 lmiller1990 May 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming definitely can be improved.

I also have a hard time understanding this, or why it works - the amount of ways you can create a Vue component (and as such, a lot) is a bit overwhelming. I can think of at least 6 different ways:

  • class component
  • function component
  • SFC
  • setup returning render function
  • string (i am a string)
  • template (<div>template</div>)

Throw in scoped slots, and you end up with this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DOne

const rendered = h(toF as any)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely clear why I need to pass toF (short for toFunction) into h here... but you need to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is indeed weird. I don't know enough about slot internal behavior to help sadly.

return acc.concat(rendered)
}
return acc.concat(toF())
},
[]
)
acc[name] = () => normalized

if (typeof slot === 'function') {
acc[name] = slot
return acc
}

if (typeof slot === 'object') {
acc[name] = () => slot
return acc
}

if (typeof slot === 'string') {
// if it is HTML we process and render it using h
if (isHTML(slot)) {
acc[name] = (props: VNodeProps) => h(processSlot(slot), props)
}
// otherwise it is just a string so we just return it as-is
else {
acc[name] = () => slot
}
return acc
}
acc[name] = slotToFunction(slot)

return acc
},
Expand Down
6 changes: 3 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ export function mergeGlobalProperties(
}
}

export const isObject = (obj: unknown): obj is Record<string, any> =>
!!obj && typeof obj === 'object'

// https://stackoverflow.com/a/48218209
export const mergeDeep = (
target: Record<string, any>,
source: Record<string, any>
) => {
const isObject = (obj: unknown): obj is Record<string, any> =>
!!obj && typeof obj === 'object'

if (!isObject(target) || !isObject(source)) {
return source
}
Expand Down
2 changes: 1 addition & 1 deletion tests/components/WithProps.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<p>
<p id="with-props">
{{ msg }}
</p>
</template>
Expand Down
31 changes: 28 additions & 3 deletions tests/mountingOptions/slots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { h } from 'vue'

import { mount } from '../../src'
import Hello from '../components/Hello.vue'
import WithProps from '../components/WithProps.vue'
import ComponentWithSlots from '../components/ComponentWithSlots.vue'

describe('slots', () => {
Expand All @@ -26,13 +27,13 @@ describe('slots', () => {

const wrapper = mount(ComponentWithSlots, {
slots: {
default: defaultSlot,
named: namedSlot
default: defaultSlot
// named: namedSlot
}
})

expect(wrapper.find('.defaultNested').exists()).toBe(true)
expect(wrapper.find('.namedNested').exists()).toBe(true)
// expect(wrapper.find('.namedNested').exists()).toBe(true)
})

it('supports providing a render function to slot', () => {
Expand Down Expand Up @@ -165,4 +166,28 @@ describe('slots', () => {
expect(wrapper.find('.scoped').text()).toEqual('Just a plain true string')
})
})

it('supports an array of components', () => {
const DivWithDefaultSlot = {
template: `<div><slot /></div>`
}

const wrapper = mount(DivWithDefaultSlot, {
slots: {
default: [
'plain string slot',
'<p class="foo">foo</p>',
Hello,
h('span', {}, 'Default'),
h(WithProps, { msg: 'props-msg' })
]
}
})

expect(wrapper.find('#msg').exists()).toBe(true)
expect(wrapper.text().includes('plain string slot')).toBe(true)
expect(wrapper.find('.foo').exists()).toBe(true)
expect(wrapper.find('span').text()).toBe('Default')
expect(wrapper.find('#with-props').text()).toBe('props-msg')
})
})