Vue2: Unable to bind a prop to a named slot - javascript

I'm not sure why this isn't working... should be straight forward. I'm unable to bind a simple object to a named slot in one of my components:
I should be able to do the following:
Create a named slot and then bind a property to it:
<slot name="actions" :item="item" />
data(){
return {
item: {val1: 1, val2: 2}
}
}
Use it in this fashion:
<template #actions="{ item }">
<pre>{{ item }}</pre>
</template>
However, when I do this, this slot will not even render...
Below is my entire component code:
<template>
<v-dialog v-model="dialog" :persistent="persistent" :width="width">
<template #activator="{ on: dialogActivator, attrs: dialogAttrs }">
<v-tooltip bottom :disabled="!tooltipText">
<template #activator="{ on: tooltipActivator, tooltipAttrs }">
<v-btn
v-bind="{ ...dialogAttrs, ...tooltipAttrs, ...$attrs }"
v-on="{ ...dialogActivator, ...tooltipActivator }"
#click="$emit('handle-dialog-open-click')"
>
<slot name="activator"> Open </slot>
</v-btn>
</template>
<span>{{ tooltipText }}</span>
</v-tooltip>
</template>
<v-card :height="cardHeight">
<v-card-title
v-if="hasTitleSlot"
class="d-flex justify-space-between"
>
<slot name="title" />
</v-card-title>
<slot v-if="dialog" />
<v-card-actions v-if="hasActionsSlot" class="d-flex justify-end">
<slot name="actions" :item="obj" />
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
inheritAttrs: false,
props: {
width: {
type: String,
default: '500',
},
tooltipText: {
type: String,
default: '',
},
persistent: {
type: Boolean,
default: false,
},
cardHeight: {
type: String,
default: '',
},
},
data() {
return {
dialog: false,
obj: {
val1: 1,
val2: 2,
},
};
},
computed: {
hasTitleSlot() {
return !!this.$slots.title;
},
hasActionsSlot() {
return !!this.$slots.actions;
},
},
created() {
this.$root.$on('close-dialog', this.closeModal);
},
beforeDestroy() {
this.$root.$off('close-dialog', this.closeModal);
},
methods: {
closeModal() {
this.dialog = false;
},
},
};
</script>
It feels like its a simple typo somewhere...
EDIT:
Confirming that it works fine if I do not try to extract the prop like this:
<template #actions>
Some awesome action goes here
</template>

Problem is is in v-if="hasActionsSlot"
hasActionsSlot() {
return !!this.$slots.actions;
},
This method always returns false as your <slot name="actions" :item="obj" /> is not a regular slot, it is a scoped slot! And because this is not Vue 3 (where all slots, scoped or not are part of $slots), you need to use:
hasActionsSlot() {
return !!this.$scopedSlots.actions;
},
See $slots VS $scopedSlots
since 2.6.0+: All $slots are now also exposed on $scopedSlots as functions. If you work with render functions, it is now recommended to always access slots via $scopedSlots, whether they currently use a scope or not. This will not only make future refactors to add a scope simpler, but also ease your eventual migration to Vue 3, where all slots will be functions.
So in Vue 2.6+ it is always safer to work with $scopedSlots (as it contains all the slots)

Related

Pass $route as parameter in axios post request

So I have a vue project with a dashboard that contains many tests and i want to pass the test name as a parameter in an axios request when the user clicks on a button and gets redirected on another page.
I already did the first part and passed the name of the test in route as a parameter name and now i'm trying to fetch the corresponding item in the collection.When using $route.params.name I get an error that says $route is not defined
here's my code so far
<template>
<v-app>
<app-navbar />
<v-main>
<div class="text-center">
<h3>
test {{ $route.params.name }}, {{ $route.query.status }},{{
$route.query.tag
}}
</h3>
<h3 v-if="this.loadAPI">{{failreason()}}</h3>
</div>
<v-data-table
:headers="headers"
:items="imagesref"
:items-per-page="5"
class="elevation-1"
>
<template v-slot:[`item.index`]="{ index }">
{{index+1}}
</template>
<template v-slot:[`item.status`]="{ index }">
{{imagesresult[index][2]}}
</template>
<template v-slot:[`item.ref`]="{ index }">
<v-img :src="imagesref[index]" max-width="750" max-height="750" #click="expref[index] = !expref[index]"/>
<v-overlay :value="expref[index]"><v-img :src="imagesref[index]" max-width="1300" max-height="900" #click="expref[index] = !expref[index]"/> </v-overlay>
</template>
<template v-slot:[`item.test`]="{ index }">
<v-img :src="imagestest[index]" max-width="750" max-height="750" #click="exptest[index] = !exptest[index]"/>
<v-overlay :value="exptest[index]"><v-img :src="imagestest[index]" max-width="1300" max-height="900" #click="exptest[index] = !exptest[index]"/> </v-overlay>
</template>
<template v-slot:[`item.res`]="{ index }">
<v-img :src="imagesresult[index][0]" max-width="750" max-height="750" #click="expres[index] = !expres[index]"/>
<v-overlay :value="expres[index]"><v-img :src="imagesresult[index][0]" max-width="1300" max-height="900" #click="expres[index] = !expres[index]"/> </v-overlay>
</template>
<template #[`item.mis`]="{ index }">
{{Math.round(imagesresult[index][1]*100)/100}}
</template>
<template #[`item.Scrubber`]="{ index }">
<nuxt-link :to="{ path: 'scrubber', query: { imageref: imagesref[index],imagetest:imagestest[index],imageres:imagesresult[index] }}">Show Scrubber</nuxt-link>
</template>
</v-data-table>
</v-main>
</v-app>
</template>
<script>
import appNavbar from "../../../components/appNavbar.vue"
import axios from "axios"
export default {
components: { appNavbar },
name: "App",
data() {
return {
loadAPI:false,
dialog:false,
expref:[],
exptest:[],
expres:[],
items: [],
imagesref: [],
imagestest: [],
imagesresult: [],
headers: [
{ text: 'index',value: 'index',sortable:false},
{ text: 'Status',value: 'status',sortable:false},
{ text: 'Imagesref', value: 'ref',sortable:false },
{ text: 'Imagestest', value: 'test',sortable:false },
{ text: 'Imagesresult', value: 'res',sortable:false },
{ text: 'Mismatch percent', value: 'mis',sortable:false },
{ text: 'Scrubber', value: 'Scrubber',sortable:false },
]
}
},
async created() {
try {
const res = await axios({
method: 'post',
url: 'http://localhost:3002/backend/gettestbyname',
data: {name: $route.params.name}
})
this.items = res.data.data;
this.imagesref = res.data.data[0].refimages;
this.imagestest = res.data.data[0].testimages;
this.imagesresult = res.data.data[0].testresults;
for (let i of this.imagesref){
this.expref.push(false);
this.exptest.push(false);
this.expres.push(false);
}
this.loadAPI=true;
} catch (error) {
console.log(error);
}
},
methods:{
failreason()
{
if (this.items[0].status=="failed"){
let index=0;
for (let i of this.items[0].testresults)
{ console.log(i);
index++;
if (i[2]=="failed")
{
return 'Visual test failed at step number '+index;
}
}
return 'Test set missing screenshots';
}
}
}
}
</script>
<style scoped>
</style>
Globally injected properties in Vue are available from the Vue context, not the global javascript context (like window).
So, in the <script> tag, you have to use this.$router to access it.
In your created hook:
// replace
data: {name: $route.params.name}
//by
data: {name: this.$route.params.name}
From the vue-router docs:
By calling app.use(router), we get access to it as this.$router as well as the current route as this.$route inside of any component:

TextArea Avoid mutating a prop directly since the value will be overwritten

I know that a similar question has already been dealt with on stackoverflow. But I could not put together a solution from the proposed one. I am very ashamed.
The essence is this: I have a component and another one inside it.
The child component-VClipboardTextField is a ready-made configured text-area. I couldn't get the input out of there and I don't get an error when I try to enter it.
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:
"message"
code tabs-item.vue
<template>
<v-container fluid>
<v-row align="center">
<v-col cols="9">
<v-card flat>
<v-card-text>
<h1>Request</h1>
<v-container>
<v-textarea v-model="message"
placeholder="Placeholder"
label="Request"
auto-grow
clear-icon="mdi-close-circle-outline"
clearable
rows="10"
row-height="5"
#click:clear="clearMessage"
></v-textarea>
<v-textarea v-model="response"
placeholder="Placeholder"
label="Request2"
auto-grow
counter
rows="10"
row-height="5"
color="success"
></v-textarea>
<VClipboardTextField ></VClipboardTextField>
<VClipboardTextField isReadOnly></VClipboardTextField>
</v-container>
<v-row>
<v-btn
dark
color="primary"
elevation="12"
justify="end"
float-right
#click="sendRequest"
>
Send Request
</v-btn>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="3">
<schema-selector #changeSchema="onChangeSchema"></schema-selector>
</v-col>
</v-row>
</v-container>
</template>
<script>
export default {
name: 'tabs-item',
props: ['response'],
data() {
return {
schema: String,
message: '',
}
},
methods: {
sendRequest() {
const message = {
message: this.message,
method: this.schema.state
}
this.$emit('sendRequest', message)
},
clearMessage() {
this.message = ''
},
onChangeSchema(selectS) {
console.log("get schema: ", selectS.state)
this.schema = selectS
}
},
}
</script>
and child VClipboardTextField.vue
<template>
<v-container>
<v-tooltip bottom
v-model="show">
<template v-slot:activator="{ on, attrs }">
<v-textarea
v-model="message"
:append-outer-icon="'mdi-content-copy'"
:readonly="isReadOnly"
auto-grow
filled
counter
clear-icon="mdi-close-circle-outline"
clearable
label="Response message"
type="text"
#click:append-outer="copyToBuffer"
#click:clear="clearMessage"
></v-textarea>
</template>
<span>Tooltip</span>
</v-tooltip>
</v-container>
</template>
<script>
export default {
name: 'VClipboardTextField',
props: {
isReadOnly: Boolean,
message : { type :String, default: "msg"}
},
data() {
return {
show: false,
// messageLocal: 'Response!',
iconIndex: 0,
}
},
methods: {
copyToBuffer() {
console.log("this: ", this)
navigator.clipboard.writeText(this.message);
this.toolTipChange()
setTimeout(() => this.toolTipChange(), 1000)
},
clearMessage() {
this.message = ''
},
toolTipChange() {
if (this.show)
this.show = false
}
}
}
</script>
I will be glad to see an example of the code that will explain how to correctly solve this problem without crutches!
Thanks.
you cannot modify props in components, if the initial value of message is needed as placeholder (meaning the input might not be empty at the begining), you can store the data in message prop to another data variable and use that as v-model to the textarea.
if there is no initial value for message, just use another data variable for textarea and emit an update for message in a watcher.
in code tabs-item.vue
<VClipboardTextField :message.sync="message"></VClipboardTextField>
and child VClipboardTextField.vue
<template>
<v-container>
<v-tooltip bottom
v-model="show">
<template v-slot:activator="{ on, attrs }">
<v-textarea
v-model="message_slug"
:append-outer-icon="'mdi-content-copy'"
:readonly="isReadOnly"
auto-grow
filled
counter
clear-icon="mdi-close-circle-outline"
clearable
label="Response message"
type="text"
#click:append-outer="copyToBuffer"
#click:clear="clearMessage"
></v-textarea>
</template>
<span>Tooltip</span>
</v-tooltip>
</v-container>
</template>
<script>
export default {
name: 'VClipboardTextField',
props: {
isReadOnly: Boolean,
message : { type :String, default: "msg"}
},
data() {
return {
show: false,
// messageLocal: 'Response!',
iconIndex: 0,
message_slug: '',
}
},
watch: {
message_slug(x) {
this.$emit('update:message', x)
}
},
}
</script>
It will bind the value to message_slug and update the message on parent component when it's value changes.
Instead of watching for change every time, you can only emit when there are changes.
In your tabs-item.vue
<VClipboardTextField v-model="message"></VClipboardTextField>
In your VClipboardTextField.vue component, you can receive the v-model input prop as a "VALUE" prop and assign it to a local data property. That way u can manipulate the local property and emit only when in it is changed!
<template>
<v-container>
<v-tooltip bottom v-model="show">
<template v-slot:activator="{ on, attrs }">
<v-textarea
v-model="message"
:append-outer-icon="'mdi-content-copy'"
:readonly="isReadOnly"
v-on="on"
v-bind="attrs"
auto-grow
filled
counter
clear-icon="mdi-close-circle-outline"
clearable
label="Response message"
type="text"
#click:append-outer="copyToBuffer"
#click:clear="clearMessage"
#change="$emit('input', v)"
></v-textarea>
</template>
<span>Tooltip</span>
</v-tooltip>
</v-container>
</template>
<script>
export default {
name: "VClipboardTextField",
props: {
isReadOnly: { type: Boolean },
value: {
type: String,
},
},
data() {
return {
show: false,
message: this.value,
};
},
};
</script>

Dynamic binding of v-model to computed property

I have a component where i'd like to iterate over elements with a computed property.
Under normal circumstances you'd do something like this:
// Computed property
acquiredPrice: {
get () {
return value
},
set (value) {
// set the value with some vuex magic
},
},
And then reference it in the template like this:
<v-text-field
v-model="acquiredPrice"
>
</v-text-field>
And this works just as expected. However i would like to do the following
// computed property
steps () {
return [
{
allowed: true,
text: 'Some question?',
type: 'integer',
model: this.acquiredPrice, // reference to the computed property
},
]
},
<!-- inside template -->
<template v-for="step in steps">
<v-row
:key="step.text"
>
<v-col>
<h4>{{step.text}}</h4>
<!-- This does not work. Only in retrieving the value -->
<v-text-field
v-model="step.model"
>
</v-text-field>
</v-col>
</v-row>
</template>
So the core issue is that when i iterate over the steps and use the step.model to reference the computed property, i loose the setter. I.e when typing into the field the setter method is never hit.
Maybe there is some way to access computed properties by string names, so i can avoid the actual value in the dict?
I.e something like (this is just pseudo code for what i want) v-model=$computed['acquiredPrice']
A full PoC to illustrate the issue can be seen here:
<template>
<div class="">
<template v-for="step in steps">
<v-row
:key="step.text"
>
<v-col>
<h4>{{step.text}}</h4>
<!-- This does not work. Only in retrieving the value -->
<v-text-field
v-model="step.model"
>
</v-text-field>
</v-col>
</v-row>
</template>
<!-- This works just as expected -->
<v-text-field
v-model="acquiredPrice"
>
</v-text-field>
</div>
</template>
<script>
export default {
name: 'redacted',
props: {
},
data: () => ({
}),
computed: {
acquiredPrice: {
get () {
return value
},
set (value) {
// set the value with some vuex magic
// THIS IS NEVER HIT WHEN IT IS REFERENCED FROM step.model ON LINE 13
},
},
steps () {
return [
{
allowed: true,
text: 'Some question?',
type: 'integer',
model: this.acquiredPrice,
},
]
},
},
components: {
},
methods: {
},
mounted () {
},
}
</script>
Root issue is in this line:
model: this.acquiredPrice, // reference to the computed property
Because you are not assigning reference to acquiredPrice computed property into model, you are calling its getter and assigning value it returns...
There is nothing like $computed because its not needed - computed prop is just another member of your component so you can access it like this["acquiredPrice"] and since this is not available in templates, you can use a little trick using the method and returning this from it....
Whole solution:
new Vue({
data() {
return {
counter: 10
};
},
computed: {
acquiredPrice: {
get() {
return this.counter;
},
set(value) {
// just placeholder as we dont have Vuex here
this.counter = value;
console.log("Updating counter", value)
}
},
steps() {
return [{
allowed: true,
text: 'Some question?',
type: 'integer',
model: 'acquiredPrice'
}, ]
},
},
methods: {
self() {
return this
}
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
{{ counter }}
<div v-for="step in steps" :key="step.text">
<h4>{{step.text}}</h4>
<input v-model="self()[step.model]" />
</div>
</div>

I can't display properly v-data-table data: ''Invalid prop: type check failed for prop "items". Expected Array, got Object''

I'm starting a project in which I had to use Vue. I'm actually really new to this, so I'm learning on the go. I do apologize in advance since this question have answered before, however, I didn't really understand the solutions provided, which is why I'm here asking myself.
Well, I was trying to display some data on my Data Table (more specifically, v-data-table from Vuetify). I was able to get the data from the API, but, for some reason it doesn't show me anything. Thanks to Vuex I can see that the mutation worked because on the console on Google Chrome I can see the Array of objects. But as I said, it still does't show me a single thing on the table, it even says 'no data available'. Some errors that I get are things like '[Vue warn]: Invalid prop: type check failed for prop "items". Expected Array, got Object' and 'TypeError: this.items.slice is not a function'.
Here is the code from List.vue
<template>
<v-container id="data-tables" tag="section">
<div class="text-right">
<v-btn class="mx-2" fab dark color="primary" :to="{ name: 'UserCreate' }">
<v-icon dark>mdi-plus</v-icon>
</v-btn>
</div>
<base-material-card
color="indigo"
icon="mdi-vuetify"
inline
class="px-5 py-3"
>
<template v-slot:after-heading>
<div class="display-2 font-weight-light">
Lista de Empleados
</div>
</template>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
class="ml-auto"
label="Search"
hide-details
single-line
style="max-width: 250px;"
/>
<v-divider class="mt-3" />
<v-data-table
:headers="headers"
:items="users"
:search.sync="search"
:sort-by="['name', 'office']"
:sort-desc="[false, true]"
multi-sort
>
<template v-slot:item.actions="{ item }">
<v-icon small class="mr-2" #click="editItem(item)">
mdi-eye
</v-icon>
<v-icon
small
class="mr-2"
#click="editItem(item)"
:to="{ name: 'UserUpdate' }"
>
mdi-pencil
</v-icon>
<v-icon small #click="deleteItem(item)">
mdi-delete
</v-icon>
</template>
</v-data-table>
</base-material-card>
</v-container>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'UsersTable',
data() {
return {
headers: [
{
text: 'Nombre',
value: 'empleado.nombre',
},
{
text: 'Apellido',
value: 'empleado.apellido',
},
{
text: 'Dirección',
value: 'empleado.direccion',
},
{
text: 'Correo Electrónico',
value: 'email',
},
{
text: 'Teléfono',
value: 'empleado.telefono',
},
{
sortable: false,
text: 'Actions',
value: 'actions',
},
],
loader: true,
search: undefined,
}
},
created() {
this.$store.dispatch('users/fetchUsers')
},
computed: {
...mapState(['users']),
},
methods: {},
mounted() {},
}
</script>
And the code from user.js, where the fetchUsers it's coming from.
import auth from '#/api/auth'
export const namespaced = true
export const state = {
users: [],
}
export const mutations = {
SET_USERS(state, users) {
state.users = users
},
}
export const actions = {
fetchUsers({ commit, dispatch }) {
auth
.getAllAccounts()
.then((response) => {
commit('SET_USERS', response.data)
})
.catch((error) => {
const notification = {
type: 'error',
message: 'There was a problem fetching users: ' + error.message,
}
dispatch('notification/add', notification, { root: true })
})
},
}
Thanks in advance.
You are not getting the correct user from vuex, because is namespaced, change to:
computed: {
...mapState('users',['users']),
},
MapState helper dosen't work the same way like the other helpers because the state module isn't registred in the global namespace. So namespacing your module will help or you do it in this way:
computed: {
...mapState({
users: state => state.FilenameOfYourModule.users
})
}

vuejs How to move v-slot references into a component?

I don't quite get how to move the v-slot data into a component.
Let's say I want to refactor the follwoing code:
<template v-slot:item="data">
<template v-if="typeof data.item !== 'object'">
<v-list-item-content v-text="data.item"></v-list-item-content>
</template>
<template v-else>
<v-list-item-avatar>
<img :src="data.item.avatar">
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-html="data.item.name"></v-list-item-title>
<v-list-item-subtitle v-html="data.item.group"></v-list-item-subtitle>
</v-list-item-content>
</template>
How do I pass the data inside a new component? I tried it with props but the component wouldn't show up:
<ListElementAvatar
:item="data.item"
:imgSrc="data.item.avatar"
:title="data.item.name"
:subtitle="data.item.group"
:source="data" />
ListElementAvatar:
<template>
<div>
<template v-if="typeof item !== 'object'">
<v-list-item-content :v-text="item"></v-list-item-content>
</template>
<template v-else>
<ListItemAvatar :imgSrc="imgSrc" />
<v-list-item-content>
<v-list-item-title :v-html="title"></v-list-item-title>
<v-list-item-subtitle :v-html="subtitle"></v-list-item-subtitle>
</v-list-item-content>
</template>
</div>
</template>
<script>
export default {
name: "ListElementAvatar",
props: {
item: {
type: Object,
default: () => {},
},
imgSrc: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
subtitle: {
type: String,
default: '',
},
source: {
type: Object,
default: () => {},
},
},
};
</script>
What I want to achieve:
I'm trying to refactor the code, i.e. creating small components. The code in the first listing should be put inside a vue component, called ListElementAvatar. Because I want to reuse it later. When I want to reuse it, I just call instead of the long code in the first listing.
Context:
https://codepen.io/thadeuszlay/pen/gOYevRZ?editors=1010
You need to put your component inside template with scoped-slot:
<template v-slot:item="data">
<ListElementAvatar
:item="data.item"
:imgSrc="data.item.avatar"
:title="data.item.name"
:subtitle="data.item.group"
:source="data"
/>
</template>
This should do the trick.

Categories

Resources