'How to extend Vue component with default values for props and slots?

I'm using bootstrap-vue in my application, and I have a lot of tables, which all have the same boilerplate code. Here's an example of what that boilerplate might look like:

<b-table
  :items="someItemList"
  :busy="someItemList === null"
  :empty-text="$t('notFound')"
  :empty-filtered-text="$t('notFound')"
  no-sort-reset
  show-empty
  striped
  hover
>
  <div slot="table-busy" class="text-center my-3">
    <b-spinner class="align-middle"/>
  </div>
</b-table>

I would of course like to factor out this boilerplate into some sort of common module, like a custom component, so that my starting point for a new table would look something more like this:

<my-awesome-table :items="someItemList">
</my-awesome-table>

Ultimately, I would like my-awesome-table to act just like a normal b-table, but with all of this boilerplate already set, and where I can still set extra props and slots as needed.

However, I can't figure out a way to make this work. I've tried:

  • making a wrapper component, but then I struggle to expose all of the functionality of the underlying b-table
  • extending the b-table component, but then I struggle to set the prop and slot values as I've set them in my boilerplate template

How can I create a custom component which allows me to set default values for props and slots?



Solution 1:[1]

This situation calls for a functional component. Untested, but try something like this:

my-awesome-table.vue

export default {
  functional: true,

  render(h, ctx) {
    // Get data from the parent component
    const {
      someItemList,
      $t,
    } = ctx.parent

    return h('b-table', {
      // Pass on the full data object
      ...ctx.data,

      // Extend the props
      props: {
        items: someItemList,
        busy: someItemList === null,
        emptyText: $t('notFound'),
        emptyFilteredText: $t('notFound'),
        noSortReset: true,
        showEmpty: true,
        striped: true,
        hover: true,

        // Override the above default prop values with any props provided
        ...ctx.props,
      },
    }, [
      // Provide a default rendering for the table-busy slot
      // if one is not provided
      !ctx.slots()['table-busy'] && h('div', {
        slot: 'table-busy',
        staticClass: 'text-center my-3',
      }, [
        h('b-spinner', { staticClass: 'align-middle' })
      ],

      // Append any additional children
      ...(ctx.children || [])
    ])
  }
}

Then you can use it like this:

<my-awesome-table
  :items="otherList"
  :busy="isBusy"
>
</my-awesome-table>

<my-awesome-table>
  <div slot="table-busy">My custom busy slot</div>
  <div slot="something-else">Some other slot</div>
</my-awesome-table>

Keep in mind that the default prop values that <my-awesome-table> uses is strongly dependent on the parent component, but it's up to you how tightly-coupled you want it to be.

A disadvantage of this approach is you need to write the render function by hand. The Vue template compiler does have very limited support for functional components, but every time I have attempted to compose a functional component that way I have regretted it (the template can get messy with things that can be expressed in code with ease).

Solution 2:[2]

Maybe there is a better solution, but I managed to solve the problem by sending a self.template through the meta argument through yield.

Maybe this is helpful to somebody...

if response.request.url.split('/')[-2] == 'tab_1':
    yield scrapy.Request(content_page, callback=self.tab_1, meta={'template':self.template})
if response.request.url.split('/')[-2] == 'tab_2':
    yield scrapy.Request(content_page, callback=self.tab_2, meta={'template':self.template})

And the values are assigned this way:

def tab_1(self, response):
        response.meta.get['template']['tab_1_value'] = self.valueSeparation(response.xpath('//div/h2/strong/span/text()').get())
        response.meta.get['template']['tab_1_description'] = response.xpath('//div/div/p/text()').get()

def tab_2(self, response):
        response.meta.get['template']['tab_2_value'] = self.valueSeparation(response.xpath('//div/h2/strong/span/text()').get())
        response.meta.get['template']['tab_2_description'] = response.xpath('//div/div/p/text()').get()
        

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Marko