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: add scoped slots option #507

Merged
merged 22 commits into from
Apr 14, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/en/api/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,15 @@ Please use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#hea

Provide an object of scoped slots contents to the component. The key corresponds to the slot name. The value can be a template string.

There are two limitations.
There are three limitations.

* This option is only supported in vue@2.5+.

* You can not use `<template>` tag as the root element in the `scopedSlots` option.

* This does not support PhantomJS.
Please use [Puppeteer](https://github.com/karma-runner/karma-chrome-launcher#headless-chromium-with-puppeteer).
Copy link
Member

Choose a reason for hiding this comment

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

Can you change to:

You can use Puppeteer as an alternative.


Example:

```js
Expand Down
3 changes: 3 additions & 0 deletions packages/create-instance/add-scoped-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export function addScopedSlots (vm: Component, scopedSlots: Object): void {
if (template.substr(0, 9) === '<template') {
throwError('the scopedSlots option does not support a template tag as the root element.')
}
const domParser = new window.DOMParser()
const document = domParser.parseFromString(template, 'text/html')
Copy link
Member

Choose a reason for hiding this comment

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

Can you rename this variable to avoid collisions with window.document?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed it.

vm.$_vueTestUtils_scopedSlots[key] = compileToFunctions(template).render
vm.$_vueTestUtils_slotScopes[key] = document.body.firstChild.getAttribute('slot-scope')
})
}
2 changes: 1 addition & 1 deletion packages/create-instance/add-slots.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function addSlotToVm (vm: Component, slotName: string, slotValue: Component | st
throwError('vueTemplateCompiler is undefined, you must pass components explicitly if vue-template-compiler is undefined')
}
if (window.navigator.userAgent.match(/PhantomJS/i)) {
throwError('option.slots does not support strings in PhantomJS. Please use Puppeteer, or pass a component')
throwError('the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.')
}
const domParser = new window.DOMParser()
const document = domParser.parseFromString(slotValue, 'text/html')
Expand Down
17 changes: 15 additions & 2 deletions packages/create-instance/create-instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,33 @@ export default function createInstance (
addListeners(vm, options.listeners)

if (options.scopedSlots) {
if (window.navigator.userAgent.match(/PhantomJS/i)) {
throwError('the scopedSlots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.')
}
const vueVersion = Number(`${Vue.version.split('.')[0]}.${Vue.version.split('.')[1]}`)
if (vueVersion >= 2.5) {
vm.$_vueTestUtils_scopedSlots = {}
vm.$_vueTestUtils_slotScopes = {}
const renderSlot = vm._renderProxy._t

vm._renderProxy._t = function (name, feedback, props, bindObject) {
const scopedSlotFn = vm.$_vueTestUtils_scopedSlots[name]
const slotScope = vm.$_vueTestUtils_slotScopes[name]
if (scopedSlotFn) {
props = { ...bindObject, ...props }
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to set vm._renderProxy.props?

Copy link
Contributor Author

@38elements 38elements Apr 7, 2018

Choose a reason for hiding this comment

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

scopedSlotFn needs vm._renderProxy.props.
If vm._renderProxy.prop is not setted, the render function can not use props.

with(this){return _c('p',{},[_v(_s(props.index)+","+_s(props.text))])}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a bug.
When slot-scope is other than props, this is not work.

vm._renderProxy.props = props
return scopedSlotFn.call(vm._renderProxy)
const proxy = {}
Object.keys(vm._renderProxy).concat(Object.keys(Vue.prototype)).forEach((key) => {
if (key[0] === '_') {
proxy[key] = vm._renderProxy[key]
}
})
proxy[slotScope] = props
Copy link
Contributor Author

@38elements 38elements Apr 8, 2018

Choose a reason for hiding this comment

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

Since when slot name and slot scope is same, vm._renderProxy.[<slot name>] is changed in _t function, proxy is created.

<ul>
  <slot name="foo"
    v-for="(item, index) in foo"
    :text="item.text"
    :index="index">
  </slot>
</ul>
function render() {
  with(this) {
    return _c('ul', [_l((foo), function (item, index) {
      return _t("foo", null, {
        text: item.text,
        index: index
      })
    })], 2)
  }
}

scopedSlotFn

function anonymous() {
  with(this){return _c('p',{},[_v(_s(foo.index)+","+_s(foo.text))])}
}

Copy link
Member

Choose a reason for hiding this comment

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

Does Vue throw an error if there are two conflicting slot scopes? If not, we should throw an error here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is impossible to set a word that starts with "_" in Vue.

[Vue warn]: Property or method "_foo" is not defined on the instance but referenced during render. 
Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. 
See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.

Copy link
Member

Choose a reason for hiding this comment

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

I meant if you mounted with this option:

{
scopedSlots: {
  foo: '<div />',
  foo: '<input />'
}
}

The second scopedSlot would overwrite the first scopedSlots in _vueTestUtils_slotScopes. Although thinking about it, this is probably fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not possible to register the same key.

{ foo: 1, foo: 2 }
// => { foo: 2 }

Copy link
Member

Choose a reason for hiding this comment

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

🤦‍♂️ Of course ;p

return scopedSlotFn.call(proxy)
} else {
return renderSlot.call(vm._renderProxy, name, feedback, props, bindObject)
}
}

// $FlowIgnore
addScopedSlots(vm, options.scopedSlots)
} else {
Expand Down
16 changes: 12 additions & 4 deletions test/resources/components/component-with-scoped-slots.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
<template>
<div>
<div id="scopedSlots">
<slot name="item"
v-for="(item, index) in items"
<div id="foo">
<slot name="foo"
v-for="(item, index) in foo"
:text="item.text"
:index="index">
</slot>
</div>
<div id="bar">
<slot name="bar"
v-for="(item, index) in bar"
:text="item.text"
:index="index">
</slot>
Expand All @@ -18,7 +25,8 @@
name: 'component-with-scoped-slots',
data () {
return {
items: [{ text: 'a1' }, { text: 'a2' }, { text: 'a3' }]
foo: [{ text: 'a1' }, { text: 'a2' }, { text: 'a3' }],
bar: [{ text: 'A1' }, { text: 'A2' }, { text: 'A3' }]
}
}
}
Expand Down
46 changes: 40 additions & 6 deletions test/specs/mounting-options/scopedSlots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,34 @@ import { describeWithShallowAndMount, vueVersion, itDoNotRunIf } from '~resource
import ComponentWithScopedSlots from '~resources/components/component-with-scoped-slots.vue'

describeWithShallowAndMount('scopedSlots', (mountingMethod) => {
let _window

beforeEach(() => {
_window = window
})

afterEach(() => {
if (!window.navigator.userAgent.match(/Chrome/i)) {
window = _window // eslint-disable-line no-native-reassign
}
})

itDoNotRunIf(vueVersion < 2.5,
'mounts component scoped slots', () => {
const wrapper = mountingMethod(ComponentWithScopedSlots, {
slots: { default: '<span>123</span>' },
scopedSlots: {
'item': '<p slot-scope="props">{{props.index}},{{props.text}}</p>'
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>',
'bar': '<p slot-scope="bar">{{bar.text}},{{bar.index}}</p>'
}
})
expect(wrapper.find('#slots').html()).to.equal('<div id="slots"><span>123</span></div>')
expect(wrapper.find('#scopedSlots').html()).to.equal('<div id="scopedSlots"><p>0,a1</p><p>1,a2</p><p>2,a3</p></div>')
wrapper.vm.items = [{ text: 'b1' }, { text: 'b2' }, { text: 'b3' }]
expect(wrapper.find('#scopedSlots').html()).to.equal('<div id="scopedSlots"><p>0,b1</p><p>1,b2</p><p>2,b3</p></div>')
expect(wrapper.find('#foo').html()).to.equal('<div id="foo"><p>0,a1</p><p>1,a2</p><p>2,a3</p></div>')
expect(wrapper.find('#bar').html()).to.equal('<div id="bar"><p>A1,0</p><p>A2,1</p><p>A3,2</p></div>')
wrapper.vm.foo = [{ text: 'b1' }, { text: 'b2' }, { text: 'b3' }]
wrapper.vm.bar = [{ text: 'B1' }, { text: 'B2' }, { text: 'B3' }]
expect(wrapper.find('#foo').html()).to.equal('<div id="foo"><p>0,b1</p><p>1,b2</p><p>2,b3</p></div>')
expect(wrapper.find('#bar').html()).to.equal('<div id="bar"><p>B1,0</p><p>B2,1</p><p>B3,2</p></div>')
}
)

Expand All @@ -22,7 +38,7 @@ describeWithShallowAndMount('scopedSlots', (mountingMethod) => {
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
'item': '<template></template>'
'foo': '<template></template>'
}
})
}
Expand All @@ -36,12 +52,30 @@ describeWithShallowAndMount('scopedSlots', (mountingMethod) => {
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
'item': '<p slot="item" slot-scope="props">{{props.index}},{{props.text}}</p>'
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
}
})
}
const message = '[vue-test-utils]: the scopedSlots option is only supported in vue@2.5+.'
expect(fn).to.throw().with.property('message', message)
}
)

itDoNotRunIf(!window.navigator.userAgent.match(/PhantomJS/i),
'throws exception when using PhantomJS', () => {
if (window.navigator.userAgent.match(/Chrome/i)) {
return
}
window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
const fn = () => {
mountingMethod(ComponentWithScopedSlots, {
scopedSlots: {
'foo': '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
}
})
}
const message = '[vue-test-utils]: the scopedSlots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.'
expect(fn).to.throw().with.property('message', message)
}
)
})
2 changes: 1 addition & 1 deletion test/specs/mounting-options/slots.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describeWithMountingMethods('options.slots', (mountingMethod) => {
return
}
window = { navigator: { userAgent: 'PhantomJS' }} // eslint-disable-line no-native-reassign
const message = '[vue-test-utils]: option.slots does not support strings in PhantomJS. Please use Puppeteer, or pass a component'
const message = '[vue-test-utils]: the slots option does not support strings in PhantomJS. Please use Puppeteer, or pass a component.'
const fn = () => mountingMethod(ComponentWithSlots, { slots: { default: 'foo' }})
expect(fn).to.throw().with.property('message', message)
})
Expand Down