Extending Vue.js SFC components - vue.js

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?

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.

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>

Related

Why do I need the v-bind when I have the v-on?

In the tutorial of vue.js, we have this code
<script>
export default {
data() {
return {
text: ''
}
},
methods: {
onInput(e) {
this.text = e.target.value
}
}
}
</script>
<template>
<input :value="text" #input="onInput" placeholder="Type here">
<p>{{ text }}</p>
</template>
And I don't understand why when I delete the bind on value, the two way binding is still working ?
In the tuto, it says that using the v-on & v-bind allow to do two way binding
Am I missing something ?
The Vue example is sort of a bad use case, a little simple for what it's trying to convey:
v-on is for assigning event listeners, so v-on:click="doSomething(value)"
v-bind is binding the actual value of vue data/state. So example:
<button v-on:click="setUserDetails(value)" v-bind:value="user.id">Click</button>
Imagine this component:
<template>
<input :value="value"/>
</template>
<script>
export default {
name: 'MyComp',
props:{
value: String
}
}
</script>
And now a simple usage of it:
<template>
<MyComp v-model="passwd" type="password" minlength="3" #focus="onFocus"/>
</template>
<script>
export default {
name: 'MyOtherComp',
data(){
return {
passwd: ''
}
},
methods:{
onFocus(){}
}
}
</script>
As you can see, value, type, and minlength properties and focus event are bidden to MyComp.
Now question: How can I handle extra props in MyComp? they are not defined in MyComp props. Vue gathers them in a special variable called $attrs, which is a normal JS object. Vue also gathers all events into $listeners variable.
Now inside MyComp these special variables are:
$atrrs:{
type: 'password',
minlength: '3'
}
$listerners:{
focus: /* function onFocus from parent */
}
To redirect these values:
<template>
<input :value="value" v-bind="$attrs" v-on="$listeners"/>
</template>
<script>
export default {
name: 'MyComp',
props:{
value: String
}
}
</script>
As you can see, we use v-bind to bind extra props, and we use v-on to bind (redirect) events. The result is:
<input :value="value" :type="$attrs.type" :minlength="$attrs.minlength" #focus="$listeners.focus"/>
Of course you can use these directions to bind you objects too:
<template>
<input :value="value" v-bind="$attrs" v-bind="accumulated" v-on="$listeners"/>
</template>
<script>
export default {
name: 'MyComp',
props:{
value: String
},
data(){
return {
accumulated:{
maxlenght: (+this.$attrs.minlength || 2) + 30, // It's just for a practice to use extra props inside JS code :-)
rows: 5,
}
}
}
}
</script>
Keep in mind that duplicate props will replace and the last one wins.

Parent variable not updated when updating trough child component

I am trying to create a few custom form fields for my page and i learned that i cannot use props to do so so i am trying to find a way to update my parent component variable when i use my child component. Whe i check the parent variable it is always empty.
Here is my component:
<template>
<input
v-model="value"
:placeholder="placeHolder"
class="form-field"
>
</template>
<script>
export default {
props: ['placeHolder'],
data() {
return {
value: ''
}
},
methods: {
updateValue(){
this.$emit("update-text", this.value);
}
},
watch: {
value: function(){
this.updateValue
}
}
}
</script>
And this is how i use the component:
<TextField placeholder="Nome" :update-text="name = value"/>
what exactly am i doing wrong?
I am using vue.js with nuxt.js
I think a simpler approach in this case might be emitting an input event from your custom text field and binding the component to the variable using v-model.
TextField.vue
<template>
<input
#input="$emit('input', $event.target.value)"
:placeholder="placeHolder"
class="form-field"
>
</template>
<script>
export default {
props: ['placeHolder']
}
</script>
Usage
<template>
<TextField placeholder="Nome" v-model="name"/>
</template>
<script>
export default {
data: () => ({
name: '',
}),
}
</script>
Read more about using v-model on custom components here.

Vue - v-model inside a component within a component

I'm trying to separate my project now into components to make the code readable when adjusting into a responsive app. The problem is passing the v-model from base-select -> child -> parent. How do I store the data selected to the Parent.vue items: ''? Here is my code below.
Parent.vue
<template>
<child></child>
</template>
<script>
import Child from './components/Child'
export default {
components: {
Child,
},
data: ()=> ({
item: ''
})
}
</script>
Child.vue
<template>
// Random HTML
// Random HTML 2
<base-select
:items="select"
>
</template>
<script>
import BaseSelect from '#/components/BaseSelect'
export default {
components: {
BaseSelect,
},
data: ()=> ({
select: ['Select 1', 'Select 2']
})
}
</script>
BaseSelect.vue
<template>
<v-select
v-bind="$attrs"
v-on="$listeners"
class="body-2"
solo
dense
clearable
/>
</template>
To implement v-model you need to add a value property to each child component. Each component will also need to emit an input event so that the parent component can pick up the change (read more here). Note that if you are passing data down through too many components, you should probably look at using Vuex however in this case it would probably still be fine.
Your components would have to look something like this to pass v-model all the way to the base component:
Parent.vue
<template>
<!-- Pass the data item below -->
<child v-model="item"></child>
</template>
<script>
import Child from './components/Child'
export default {
components: {
Child,
},
data: ()=> ({
item: ''
})
}
</script>
Child.vue
<template>
// Random HTML
// Random HTML 2
<base-select
:items="select"
value="value"
#input="e => $emit('input', e)"
>
</template>
<script>
import BaseSelect from '#/components/BaseSelect'
export default {
components: {
BaseSelect,
},
// We add the value prop below to work with v-model
props: {
value: String
},
data: ()=> ({
select: ['Select 1', 'Select 2']
}),
}
</script>
BaseSelect.vue
<template>
<v-select
v-bind="$attrs"
v-on="$listeners"
value="value"
#input="e => $emit('input', e)"
class="body-2"
solo
dense
clearable
/>
</template>
<script>
export default {
props: {
value: String
}
}
</script>
You can find a similar working example that I did here.
You need to use $emit (documentation) to passing data back to parent components. Or you can start using Vuex (state manager for Vue.js).
You also can check the live demo here.

Add checks to vue component/template in development mode

I would like to add additional check to a vue component.
They should appears like this:
Those checks should be executed only in development mode.
Here an example:
<template>
<div>
<slot />
</div>
</template>
<script>
export default {
name: 'FuiButton',
props: {
labeled: {
type: Boolean,
},
labeledIcon: {
type: Boolean,
},
},
};
</script>
And the check could be, for instance, the two properties cannot be simultaneously true.
Another type of check could be: MyCompB can only exists as child of MyCompA.

How to use v-model and props passed to parent from child?

I've been learning vue for a few days now and I'm trying out passing data/props between child and parent.
Now I have the following child:
<template>
<div>
<input v-model="name1" placeholder="string">
<input v-model="number1" placeholder="number">
<p v-text="name1"></p>
<p v-text="number1"></p>
</div>
</template>
<script>
export default {
name: "child",
props: {
name1 : String,
number1 : Number
}
}
</script>
And then parent:
<template>
<div>
<child/>
</div>
</template>
<script>
import child from "#/components/complexComponent4/child.vue"
export default{
name: "parent",
components: {
child
}
}
</script>
Now when I enter some text into the input fields, it displays correctly in the paragraphs, since the props bound to the paragraphs have changed.
However, I get this warning:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "name1"
found in
---> <Child>
<Parent> at src/components/complexComponent4/parent.vue
<MyComplexView4.vue> at src/views/myComplexView4.vue
<App> at src/App.vue
<Root>
I read about this error in multiple places on the internet and also in the documentation, and I found that mutating props is deemed an anti-pattern now:
https://michaelnthiessen.com/avoid-mutating-prop-directly
Unfortunately I didn't really find anything specific and/or helpful on how to deal with this problem. Especially in context of vue handling primitive data and objects/arrays differently (objects/arrays are passed by reference).
v-model seems to play an important role in leveraging the power of vue, since it enables two-way binds. Therefore I wouldn't want to omit it entirely, unless its use has become so difficult that it doesnt justify the gains.
As the warning says, you should avoid mutating props directly in child component.
So you should emit an event from child to parent to let parent know that prop value had been changed. Parent will change the prop and pass it down to child.
For such purpose there is a syntactic sugar in Vue called .sync modifier.
So your components could be something like this.
Child:
<template>
<div>
<input
:value="name1"
#change="$emit('update:name1', $event.target.value)"
placeholder="string"
/>
<input
:value="number1"
#change="$emit('update:number1', $event.target.value)"
placeholder="number"
/>
<p v-text="name1"></p>
<p v-text="number1"></p>
</div>
</template>
<script>
export default {
name: "child",
props: {
name1 : String,
number1 : Number
}
}
</script>
And parent:
<template>
<div>
<child :name1.sync="name1" :number1.sync="number1"/>
</div>
</template>
<script>
import child from "#/components/complexComponent4/child.vue"
export default{
name: "parent",
components: {
child
},
data() {
return {
name1: '',
number1: ''
}
}
}
</script>
Or for more complicated cases you can either use v-model and computed properties with setters in child component:
<template>
<div>
<input
v-model="computedName1"
placeholder="string"
/>
<input
v-model="computedNumber1"
placeholder="number"
/>
<p v-text="name1"></p>
<p v-text="number1"></p>
</div>
</template>
<script>
export default {
name: "child",
props: {
name1 : String,
number1 : Number
},
computed: {
computedName1: {
get() { return this.name1 },
set(value) {
// some logic
this.$emit('update:name1', value)
},
computedNumber1: {
get() { return this.number1 },
set(value) {
// some logic
this.$emit('update:number1', value)
}
}
}
}
</script>
If you intend to change prop passed down to child assign it first to child data.
<template>
<div>
<input v-model="name" placeholder="string">
<input v-model="number" placeholder="number">
<p v-text="name"></p>
<p v-text="number"></p>
</div>
</template>
<script>
export default {
name: "child",
data() {
return {
name: null,
number: null
}
},
props: {
name1 : String,
number1 : Number
},
mounted() {
this.name = this.name1;
this.number = this.number1;
}
}
</script>
When data is changed you can $emit those changes to parent component
With sync
Parent
<child :number1.sync="number1" :name1.sync="name1" />
Child
watch: {
name: value => this.$emit('update:name1', value)
number : value => this.$emit('update:number1', value)
},
With events
Parent
<child :number1="number1" :name1="name1" #changeNumber="value => number1 = value" #changeName="value => name1 = value" />
Child
watch: {
name: value => this.$emit('changeName', value)
number : value => this.$emit('updateNumber', value)
},
A guideline for vue.js is that you can use props to automatically alter data in the child from the parent, but not vice-versa. For altering data of the parent-component, the child-component is supposed to use events. You could consider using two different components for name1 and number1 respectively and bind the values in a two-way-manner by making these components applicable for v-model, as it is described here.