1

I haven't found a good resource on extending Vue.js components. In every project I've worked on, regardless of the UI component library that's used, there are application Base components which extend the UI library components to enforce company/application defaults and standards.

I'm trying to extend Vue-Multiselect: https://vue-multiselect.js.org/ which has about 30 props and 12 slots. The component I'm extending doesn't matter -- I only mention it because ideally I don't want to have to repeat 30 props and 12 slots in my implementation.

I simply want to make two changes to the behavior of the component:

Make disabled prop a bit smarter

The Vue-Multiselect component has a standard disabled prop which works as expected:

<Multiselect :disabled="isDisabled" ...>

In our application, we have global state in Vuex which determines if the application is read-only. What I want to avoid is requiring developers to pass this state to every form field:

<Multiselect :disabled="readOnly || isDisabled" ...>
<OtherComponent :disabled="readOnly || someOtherCondition" ...>
...

So the user of my base component should only need to be concerned about their local UI state which affect the disabled status:

<BaseCombo :disabled="!emailValid" ...>

This would handle the 90% case of form fields that are locked down when the application is read-only and I can use an additional prop for cases where we want to ignore the global read-only status.

<BaseCombo :disabled="!emailValid" :ignoreReadOnly="true" ...>

Provide defaults

Secondly, I simply want to override some of the default prop values. This post addresses the question of supplying defaults:

https://stackoverflow.com/a/52592047/695318

And this works perfectly until I tried to modify the behavior of the disabled prop I mentioned previously.


My attempt to solve this was to either wrap or extend the component. I'd really want to avoid redeclaring all of the props if possible.

<template>
  <Multiselect
    :disabled="myCustomDisabled"
    :value="value"
    @input="$emit('input', $event)"
    :options="options"
    :label="label"
    :track-by="trackBy"
    :placeholder="placeholder"
    ... repeat for all 30 options

<script>
import Multiselect from 'vue-multiselect'

export default {
  name: "BaseCombo",
  extends: Multiselect, // extend or simply wrap?
  computed: {
    myCustomDisabled() {
      this.props.disabled || ... use disabled from Vuex state
    }
  },
  props: {
    disabled: Boolean,
    placeholder: {
      type: String,
      default: 'My Default Value',
    },
  ... repeat for all props

The problem I ran into is I don't know how to handle the slots. The user of this BaseCombo should still be able to use all 12 slots in the VueMultiselect component.

Is there a better solution for extending components?

nogridbag
  • 3,521
  • 4
  • 38
  • 51
  • When using `extends: Multiselect` you don't need to `... repeat for all props` as the props given in the extension are merged with the originals. – Richard Matsen Jul 11 '19 at 20:38
  • The template change does require repeating all the attributes, but for your scenario you may not have to touch the template (just omit it in the extended component). Instead, you might achieve the requirement by overriding the `disabled` prop and adding a default function to it. – Richard Matsen Jul 11 '19 at 20:43
  • I can't see a problem with slots, in my extended component they just work. – Richard Matsen Jul 11 '19 at 20:44

2 Answers2

0

You can use this.$props to access props defined in the props attribute. Similarly you can access attributes (things you haven't defined as props) with this.$attrs. Finally you can bind props with v-bind="someVariable".

If you combine this you can do something like this:

<!-- App.vue -->
<template>
  <component-a msg="Hello world" :fancy="{ test: 1 }" />
</template>
<!-- ComponentA.vue -->
<template>
  <component-b v-bind="$attrs" />
</template>

<script>
export default {
  name: 'componentA'
}
</script>
<!-- ComponentB.vue -->
<template>
  <div>
    {{ msg }}
    {{ fancy }}
  </div>
</template>

<script>
export default {
  props: {
    msg: String,
    fancy: Object
  },
  mounted () {
    console.log(this.$props);
  }
}
</script>

In this example, component B would be the component you try to extend.

Sumurai8
  • 20,333
  • 11
  • 66
  • 100
  • Thanks. What if ComponentB has slots? The Multiselect component has 12 slots. – nogridbag Jul 11 '19 at 18:31
  • The same idea, except now with `this.$slots`. I wrote an answer about that a while ago at https://stackoverflow.com/a/53431262/2209007 – Sumurai8 Jul 11 '19 at 18:35
  • Thanks it seems to be working well. I borrowed the scopedSlot example from this gist which is similar to your post: https://gist.github.com/Loilo/73c55ed04917ecf5d682ec70a2a1b8e2 – nogridbag Jul 11 '19 at 18:46
  • @nogridbag, I think you will need to alsoe expose the events emitted by Mulitselect. – motia Jul 11 '19 at 18:56
  • 1
    Thanks. I posted an answer which seems to be working. I believe the only remaining thing is to expose the remaining events like you said. – nogridbag Jul 11 '19 at 18:58
  • It would better with `v-on="$listeners"` to catch all the listeners, if you need to remove one of them, you can do that inside a computed property . – motia Jul 11 '19 at 19:00
0

Here's a complete example based on Sumurai8's answer and motia's comments.

<template>
  <Multiselect v-bind="childProps" v-on="$listeners">
    <slot v-for="(_, name) in $slots" :name="name" :slot="name" />
    <template v-for="(_, name) in $scopedSlots" :slot="name" slot-scope="slotData">
      <slot :name="name" v-bind="slotData" />
    </template>
  </Multiselect>
</template>

<script>
  import Multiselect from 'vue-multiselect'

  export default {
    name: "BaseCombo",
    props: {
      placeholder: {
        type: String,
        default: 'This is my default',
      },
      disabled: {
        type: Boolean,
        default: false,
      },
    },
    components: {
      Multiselect,
    },
    computed: {
      childProps() {
        return { ...this.$props, ...this.$attrs, disabled: this.isDisabled };
      },
      appReadOnly() {
        return this.$store.state.appReadOnly;
      },
      isDisabled() {
        return this.disabled || this.appReadOnly;
      }
    },
  }
</script>
nogridbag
  • 3,521
  • 4
  • 38
  • 51