Add checks to vue component/template in development mode - vue.js

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.

Related

How to transform an "extends" from the vue class-api into the vue composition-api?

I have two components, that I would like to transform into composition api
Base component:
<template>
<div>
<h3>prop type: {{ typeof message }}</h3>
</div>
</template>
<script>
export default {
name: "BaseComponent",
props: {
message: {
type: String,
required: false
}
}
};
</script>
Extended component:
<script>
import BaseComponent from "./BaseComponent";
export default {
name: "ExtendedComponent",
extends: BaseComponent,
props: {
message: {
type: Number,
required: false
}
}
};
</script>
When changing both components into composition API, I run into issues and the inheritance does not seem to work properly. It doesn't seem to pass the right prop either. Am I missing something here? I would appreciate any help.

2 Way Databind components within Components

I am struggling to reuse my components.
I want to pass the data passed to my component as a prop to another component.
If I do that vue complains about a mutation of the prop.
Example:
I have contacts that I want to show on multiple location of my app.
For that I created a contact component to reuse it:
<template>
<div>
<input :value="contact.firstName" #input="$emit('update:contact', {...contact, firstName: $event.target.value})">
<Mother v-model:mother="contact.mother"/>
</div>
</template>
<script>
import Mother from '#/components/Mother'
export default {
name: 'Contact',
components: {
Mother
},
props: {
contact: Object,
},
emit: ['update:contact'],
methods: {
}
}
</script>
Every contact has a mother, mother are shown in other places not only in the contact component.
That is why I created a mother component, that is used by the contact.
<template>
<div>
<input :value="mother.lastName" #input="$emit('update:mother', {...mother, lastName: $event.target.value})">
</div>
</template>
<script>
export default {
name: 'Mother',
props: {
mother: Object,
},
emit: ['update:mother'],
methods: {
}
}
</script>
Now I want to be able to mutate the contact an the mother as well, and I want to be able to use two contact components on the same site.
If I use it the way explained I get this error:
ERROR Failed to compile with 1 error 09:17:25
error in ./src/components/Contact.vue
Module Error (from ./node_modules/eslint-loader/index.js):
/tmp/vue-example/src/components/Contact.vue
4:27 error Unexpected mutation of "contact" prop vue/no-mutating-props
✖ 1 problem (1 error, 0 warnings)
I have an example project showing my problem:
https://gitlab.com/FirstWithThisName/vue-example.git
Thanks for your help
First I need to assume a few points.
You wanted to use v-model.
You wanted the component to be chained.
Working Example here on Vue SFC Playground.
*Note that the import path is different on the example site.
App.vue
<template>
<Contact v-model="contact" />
{{ contact }}
</template>
... remaining code omitted
Contact.vue
<template>
<div>
<input v-model="localValue"/>
<Mother v-model="childValue" />
</div>
</template>
<script>
import Mother from "./Mother.vue"
export default {
name: "Contact",
components: {
Mother
},
props: {
modelValue: Object,
},
mounted(){
this.childValue = this.modelValue.mother
},
data: () => ({
localValue: "",
childValue: null
}),
watch:{
updatedData(){
this.$emit('update:modelValue', this.updatedData)
}
},
computed: {
updatedData() {
return { firstName: this.localValue, mother: this.childValue };
},
},
};
</script>
Mother.vue
<template>
<div>
<input
v-model="localValue"
#input="$emit('update:modelValue', updatedData)"
/>
</div>
</template>
<script>
export default {
name: "Mother",
props: {
modelValue: Object,
},
data: () => ({
localValue: "",
}),
computed: {
updatedData() {
return { ...this.modelValue, lastName: this.localValue };
},
},
};
</script>
As you might know, props cannot be mutated, so you will need to "make a copy" of the value on each component to process locally.
If Mother component are never going to be used separately, v-model can be split into v-on and v-bind instead.
Lastly, as for recommendation, chaining like this can become very messy if the data starts to grow or the depth level increases. You could just make another Wrapper component that contains Contact and Mother component that scales horizontally instead.
Depends on how complex your application will get.
One option is two-way data-binding as explained here:
https://v3.vuejs.org/guide/component-basics.html#using-v-model-on-components
So you basically emit the changes to the parent.
For more complex applications I wouldn't pass data that are used in multiple components as props, but use a store. Either a simple reactive object; with provide/inject or use something like Vuex.

Vue 3 two way binding with select box

I'm trying to create a two way binding between my parent (create user form) and a child component (reusable selectbox).
The parent component
<template>
<Selectbox :selectedOption="selectedRole" :options="roles" />
<span>SelectedRole: {{ selectedRole }}</span>
</template>
<script>
import Selectbox from '#/components/formElements/Selectbox.vue';
export default {
components: {
Selectbox,
},
async created() {
await this.$store.dispatch('roles/fetchRoles');
this.selectedRole = this.roles[0].value;
},
data() {
return {
selectedRole: null,
};
},
computed: {
roles() {
return this.$store.getters['roles/roles'].map((role) => ({
value: role.id.toString(),
label: role.name,
}));
},
},
};
</script>
I'm passing down the roles as options and the selectedRole variable as selectedOption.
The child component
<template>
<select :value="selectedOption" #input="(event) => $emit('update:selectedOption', event.target.value)">
<option v-for="option in options" :value="option.value" :key="option.value">{{ option.label }}</option>
</select>
</template>
<script>
export default {
props: {
options: {
type: Array,
required: true,
},
selectedOption: {
type: String,
required: false,
},
},
};
</script>
The selectedOption is assigned to the value together. When another value is selected I want to update the passed down value in the parent component. Therefore I'm using an $emit function but that's not working right now.
I also tried to use v-model to combine the value and change attributes but without success.
<select v-model="selectedOption">
What's the correct way?
Code: Codesandbox
I guess this is the handling you want to achieve: https://codesandbox.io/s/practical-orla-i8n3t?file=/src/components/Selectbox.vue
If you use v-model on a sub-component, you have to handle it properly in the sub-component.
<custom-select v-model="value" />
<!-- IS THE SAME AS -->
<custom-select
:modelValue="value"
#update:modelValue="value = $event"
/>
So if you use v-model, a property with the name modelValue gets passed down to the sub-component. If the modelValue changes (which means another option in the select list gets selected) you have to emit a change event, indicating that the modelValue got changed: $emit('update:modelValue'). v-model automatically updates it's value if this event occurs.
Source: https://learnvue.co/2021/01/everything-you-need-to-know-about-vue-v-model/

How to bind a local component's data object to an external component

how do you use a local component's data attriutes to bind an external component's v-model
for example i have this component
<publish-blog>
<VueTrix v-model="form.editorContent">
</publish-blog>
so the form.editorContent there refers to the publish-blog component's form.editorContent inside data, how do I do that ?
You can pass a prop to the publish-blog component.
This would be what ever page or component you are using the publish blog on, though to be honest I'm not sure why you would not just put the VueTrix component inside of the publish-blog component.
This would be on what ever page/component you are wanting it on.
<template>
<PublishBlog :trix="trix">
<VueTrix v-model="trix" />
</PublishBlog>
</template>
<script>
import PublishBlog from './PublishBlog.vue';
export default {
components: {
PublishBlog,
},
data() {
return {
trix: '',
};
},
};
</script>
and inside of the publish blog component make the form.editorContent the prop passed or a default value.
But without a global store/state you are stuck with props.
UPDATE: Showing what a publish blog component might look like
PublishBlog.vue
<template>
<section>
what ever goes here.
<slot />
</section>
</template>
<script>
export default {
name: 'PublishBlog',
props: {
trix: {
type: String,
default: '',
},
},
data() {
return {
form: {
editorContent: this.trix
},
};
},
};
</script>

Extending Vue.js SFC components

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>