Skip to content

JSX $scopedSlots support #657

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

Closed
panganibanpj opened this issue May 25, 2018 · 15 comments
Closed

JSX $scopedSlots support #657

panganibanpj opened this issue May 25, 2018 · 15 comments

Comments

@panganibanpj
Copy link

What problem does this feature solve?

Being able to use $scopedSlots with render function (specifically for JSX usage)

shallowMount({
    render() {
        return this.$scopedSlots.foo({ bar: 'baz' });
    },
}, {
    scopedSlots: {
        foo: '<div>{{ bar }}</div>',
    },
})

It will complain that $scopedSlots.foo is not a function, but if you make it a function, it will say $scopedSlots.foo.trim() is not a function

What does the proposed API look like?

I prefer if scopedSlots can be passed function values like:

scopedSlots: {
  foo: (createElement, props) => { return createElement('div', props); },
},

Otherwise, at least make it so the above syntax would still work (with string scopedSlot.foo values but the this.$scopedSlots.foo() in the render function still work)

@38elements
Copy link
Contributor

What is benefit for supporting JSX $scopedSlots?

@davestaab
Copy link
Contributor

This feature would be useful for making renderless components such as outlined by Adam here: https://adamwathan.me/renderless-components-in-vuejs/

It would be helpful to test the component api.
Currently I get the above error in jest in my unit test.

@kayandra
Copy link

Hi, please can someone help me with how I can test scoped slots?

Currently, my render function looks like this

  render() {
    return this.$scopedSlots.default({ ...this.computedStateAndHelpers })
  },

In my test (with Jest), I'm trying to mount like this:

    const wrapper = shallowMount(Component, {
      scopedSlots: {
        default: '<p></p>'
      }
    })
    expect(wrapper).toBeAViewComponent()

But no luck.

I've also tried making default property a method and no luck there either. I believe I followed the docs properly. How do I do this? Thanks!

@panganibanpj
Copy link
Author

I've worked around it by wrapping the component like

shallowMount({
  render() {
    return (
      <Component
        scopedSlots={{
          default: <p></p>
        }}
      />
    );
  }
})

@kayandra
Copy link

kayandra commented Jun 14, 2018

hi @panganibanpj I still get

TypeError: this.$scopedSlots.default is not a function

Also, won't I lose direct access to the components vm?

@davestaab
Copy link
Contributor

davestaab commented Jun 15, 2018

@kayandrae07 You should be able to call find to get the nested component (may have to mount instead of shallowMount)

Something like this:

const wrapper = mount({
  render() {
    return (
      <Component
        scopedSlots={{
          default: <p></p>
        }}
      />
    );
  }
});
const componentWrapper = wrapper.find(Component);

@kayandra
Copy link

@davestaab thank you. This was what worked for me

    const wrapper = mount({
      render() {
        return <Component scopedSlots={{ default: () => <p></p> }}/>
      }
    })
    const componentWrapper = wrapper.find(Component)
    expect(componentWrapper.isVueInstance()).toBeTruthy()

I had to make the scoped slot parameter a function otherwise it failed for me

@davestaab
Copy link
Contributor

So that method works until I need to mock dependencies.

When I pass options to mount/shallowMount they are set on the outer component and not on the true component I'm trying to test. (Component in the above examples)

Looking forward to a first class solution to this.

@kayandra
Copy link

@davestaab 😄 I created a helper function that sends all the options to the Component

import { mount } from '@vue/test-utils'

export default (component, options = {}) => {
  return mount({
    render(h) {
      return h(component, {
        scopedSlots: { default: () => <p /> },
        ...options,
      })
    },
  }).find(component)
}

I'm also looking for a solution and will love to contribute in my spare time. Also, do you know how I can access the values exported from a scoped slot?

@davestaab
Copy link
Contributor

I'm able to do this:

  const wrapper = shallowMount(Component, {
    scopedSlots: {
        default:
          `<div slot-scope="props" class="content">
            slot content {{ props.message }} 
          </div>`
       }
    });
    expect(wrapper.text()).toContain("slot content hi");

where "hi" is bound to the slot as message inside Component.

So message is passed from Component to the scoped slot. Is that what you mean?

@kayandra
Copy link

Yeah, it's what I'm also doing. But assuming I'm exposing a method from the slot-scope. How do I access it?

@davestaab
Copy link
Contributor

@kayandrae07 I've ended up using a template based approach like in this project:
https://github.com/posva/vue-promised

Basically I make a test helper component that uses the component in question. To test methods, I use them and assert they had the desired results.

Hope this helps.

@kayandra
Copy link

@davestaab Thank you! Taking a look at Vue-promised and its tests and I found a suitable solution to my problem. Since it uses scoped-slots too, I can just mirror their implementation.

@38elements
Copy link
Contributor

38elements commented Jul 4, 2018

This feature would be useful for making renderless components such as outlined by Adam here: https://adamwathan.me/renderless-components-in-vuejs/

It would be helpful to test the component api.
Currently I get the above error in jest in my unit test.

It seems that this example does not use createElement().
I think the API as below using createElement() is not absolutely necessary.

scopedSlots: {
  foo: (createElement, props) => { return createElement('div', props); },
},

@aweber1
Copy link

aweber1 commented Aug 3, 2018

I was encountering similar scoped slot testing issues when using a functional component or component with a render function. After #808 landed, the final key for me ended up being to ensure that the scoped slot in a test component returns a VNode as that is what render functions ultimately need to return. The following sample code demonstrates returning a VNode from a scoped slot in a test mounted component. Hope someone else finds value in it!

// component

const MyComponent = {
  functional: true,
  props: {
    someComponentProp: { type: String },
  }
  render(createElement, context) {
    const { someComponentProp } = context.props;
    if (context.data.scopedSlots && context.data.scopedSlots.default) {
	  // scoped slot should return a VNode
	  return context.data.scopedSlots.default(someComponentProp);
	}
	return createElement('div', {}, 'default value if slot isn't defined');
  }
};
// test

const scopedComponent = {
  props: { someProp: { type: String } },
  render(createElement) {
	return createElement('em', {}, this.someProp);
  },
};

const props = { someComponentProp: 'somePropValue' };

const rendered = mount(MyComponent, {
  context: {
	props,
	scopedSlots: {
	  default: (someProp) => {
		const scoped = mount(scopedComponent, { propsData: { someProp } });
		// NOTE: `render` function needs VNode
		return scoped.vnode;
	  },
	},
  },
});

expect(rendered.html()).toBe(`<em>${props.someComponentProp}</em>`);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants