creating abstract components that can manage external data - javascript

Currently I use Vuetify for base components and would like to create reusable extensions. For example a list containing checkboxes, a datatable column with some functionality etc.
For this question I will take the list containing checkboxes example. I created the following component called CheckboxGroup.vue
<template>
<v-container>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
<script>
export default {
props: {
items: Array,
required: true
}
};
</script>
This component takes an array of objects as a property and creates a checkbox for each entry.
Important parts are v-model="item.state" and :label="item.title". Most of the time the state attribute will have a different name, same for the title attribute.
For testing purposes I created a view file called Home.vue holding an array of documents.
<template>
<v-container>
<CheckboxGroup :items="documents"/>
<v-btn #click="saveSettings">Save</v-btn>
</v-container>
</template>
<script>
import CheckboxGroup from "../components/CheckboxGroup";
export default {
components: {
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
};
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
}
};
</script>
This time title is called name and state is called deleted. Obviously CheckboxGroup is not able to manage the documents because the attribute names are wrong.
How would you solve this problem? Would you create a computed property and rename these attributes? Would be a bad idea I think...
And by the way, is using v-model a good idea? A different solution would be to listen to the changed event of a checkbox and emit an event with the item index. Then you would have to listen for the change in the parent component.
I don't think there is a way to create something like
<CheckboxGroup :items="documents" titleAttribute="name" stateAttribute="deleted"/>
because it would be bad design anyway. I hope that this is a very trivial problem and every Vue developer has been confronted with it, since the primary goal should always be to develop abstract components that can be reused multiple times.
Please keep in mind that this checkbox problem is just an example. A solution for this problem would also solve same or similar problems :)

If I understood what you wanted, it`s not so trivial. Using props is a good idea. You dont need to manage the documents attribute names, just set the attribute name to your component.
Note
Renaming the attributes or using proxies is more resource-intensive like this solution, because you need to run loop to rename the attribute names or apply aliases to data array objects.
Example
CheckboxGroup.vue
<template>
<v-container fluid>
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item[itemModel]"
:label="item[itemValue]"
></v-checkbox>
<hr>
{{items}}
</v-container>
</template>
<script>
export default {
name: "CheckboxGroup",
props: {
items: {
type: Array,
required:true
},
itemValue:{
type:String,
default: 'title',
// validate props if you need
//validator: function (value) {
// return ['title', 'name'].indexOf(value) !== -1
// }
// or make required
},
itemModel:{
type:String,
default: 'state',
// validate props if you need
//validator: function (value) {
// validate props if you need
// return ['state', 'deleted'].indexOf(value) !== -1
// }
// or make required
}
}
};
</script>
Home.vue
<template>
<div id="app">
<checkbox-group :items="documents"
item-value="name"
item-model="deleted"
>
</checkbox-group>
</div>
</template>
<script>
import CheckboxGroup from "./CheckboxGroup.vue";
export default {
name: "App",
components: {
// HelloWorld,
CheckboxGroup
},
data: function() {
return {
documents: [
{
id: 1,
name: "Doc 1",
deleted: false
},
{
id: 2,
name: "Doc 2",
deleted: false
},
{
id: 3,
name: "Doc 3",
deleted: true
}
]
}
}
};
</script>
Based on your example I`v tried to show how to create component to managing object attributes in child component. If you need more information, please let me know.

Some good answers here that definitely solve your issue - you are essentially wanting to pass data down to a child (which isn't bad design - you were on the right track!)..
I am kind of shocked that slots or scoped-slots haven't been mentioned yet... so I figured I would chime in..
Scoped-slots allow you to take advantage of data you are passing to a child - but within the parent. The child essentially "reflects" data back to the parent, which allows you to style the child component/slot however you wish, from the parent.
This is different than just passing data via a prop attribute, because you would have to rely on styling within the child - you couldn't change the styles on a 'per-use' basis. The styles you set in the child would be "hard coded"..
In this example I am riding on top of the already provided label slot that Vuetify provides - just passing my own custom scoped-slot to it.. How to find documentation on v-checkbox slots
I made some minor changes to help spice some things up, and to show how you have greater control over styles this way (and you can use any object prop for the label you want .name, .whatever, .label, etc..)
Lastly, it is important to note that Vuetify already provides a "grouped checkbox" component - v-radio-group - I know it's called "radio-group" but it supports checkboxes...
Edit: fixed the state "issue"...
Scoped Slots With Render Function - Original Answer Moved To Bottom
Thanks to #Estradiaz for collaborating on this with me!
Vue.component('checkboxgroup', {
props: {
items: { type: Array, required: true }
},
render (h) {
return h('v-container', this.items.map((item) => {
return this.$scopedSlots.checkbox({ item });
}));
},
})
new Vue({
el: "#app",
data: {
documents: [{
id: 1,
name: "Doc 1 - delete",
deleted: false,
icon: "anchor",
},
{
id: 12,
title: "Doc 1 - state",
state: false,
icon: "anchor",
},
{
id: 2,
name: "Doc 2 - delete",
deleted: false,
icon: "mouse"
},
{
id: 3,
name: "Doc 3 - delete",
deleted: true,
icon: "watch"
}
]
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' rel="stylesheet" type="text/css">
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet" type="text/css"></link>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://use.fontawesome.com/releases/v5.0.8/css/all.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/MaterialDesign-Webfont/2.1.99/css/materialdesignicons.min.css" rel="stylesheet" />
<div id="app">
<v-app>
<v-container>
<CheckboxGroup :items="documents">
<template #checkbox={item}>
<v-checkbox
v-model="item[(item.name && 'deleted') || (item.title && 'state') ]" color="red">
<template #label>
<v-icon>mdi-{{item.icon}}</v-icon>
{{ item.name || item.title }}
{{ item }}
</template>
</v-checkbox>
</template>
</CheckboxGroup>
</v-container>
</v-app>
</div>

My attempt of a json to component parser
usefull names are welcome
so basically you can target element tagnames as slot #[slotname] or put slot names and target entries to overwrite the default component.
omiting tag property in the component will append children to the parent vnode
Consider:
[
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
title: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]
Example :
Vue.component('Liste', {
props:["tag", "items"],
render(h){
console.log(this.items)
let tag = this.tag || (this.$parent.$vnode && this.$parent.$vnode.tag)
if(tag === undefined) throw Error(`tag property ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
return (
this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item})
)
|| h(ElementTag, {
attrs: attrs,
scopedSlots: this.$scopedSlots
})
}))
}
})
new Vue({
data(){
return {
items: [
{
ElementTag: 'Liste',
id: 1,
tag: 'p',
items: [
{
ElementTag: 'input',
id: 11,
type: 'checkbox',
text: "Sub Doc 1 - state",
state: true,
slotName: "slotvariant"
},
{
ElementTag: 'input',
id: 12,
type: 'date',
title: "Sub Doc 2 - Date",
date: "",
}
]
},
{
ElementTag: 'input',
id: 2,
type: 'checkbox',
title: "Doc 2 - deleted",
deleted: true,
slotName: 'deleted'
}
]}
}
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<Liste tag="p" :items="items">
<template #input="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.date"/>
</template>
<template #slotvariant="{item}">
slotvariant - {{item.text}}<br>
</template>
<template #deleted="{item}">
<label :for="item.id">
{{ item.title }}
</label>
<input :type="item.type" :id="item.id" v-model="item.deleted"/>
</template>
</Liste>
</div>
Typescript:
import {Vue, Component, Prop} from 'vue-property-decorator'
export type AbstractElement = {
[key: string]: any // passed as $attrs | useable for assigned $props
ElementTag: string
slotName?: string
}
#Component<List>({
render(h){
let tag = this.tag
|| (this.$parent.$vnode && this.$parent.$vnode.tag)
|| (this.$parent.$el && this.$parent.$el.tagName)
if(tag === undefined) throw Error(`tag prperty: ${tag} is invalid. Scope within valid vnode tag or pass valid component/ html tag as property`)
return h(tag, this.items.map(item => {
const {ElementTag, slotName, ...attrs} = item;
console.log("slotName", slotName)
return (this.$scopedSlots[slotName || ElementTag]
&& this.$scopedSlots[slotName || ElementTag]({item}))
|| h(ElementTag, {
attrs: attrs,
slot: slotName || ElementTag,
scopedSlots: this.$scopedSlots
})
}))
}
})
export default class List extends Vue{
#Prop(String) readonly tag?: string
#Prop(Array) readonly items!: Array<AbstractElement>
}
will raise this here

You can use a Proxy to map the document property names during access.
Note
In my original answer, I used Proxy handlers for get and set, which is sufficient for plain javascript objects, but fails when used with Vue data properties because of the observer wrappers that Vue applies.
By also trapping has in the Proxy, this can be overcome. I left the original answer below for anyone interested in this problem.
Here is a demo of how to use Proxy to 'alias' Vue reactive properties to different names
without affecting the original data structure
without having to copy the data
console.clear()
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.component('checkboxgroup', {
template: '#checkboxGroup',
props: { items: Array, required: true },
});
const aliasProps = (obj, aliasMap) => {
const handler = {
has(target, key) {
if (key in aliasMap) {
return true; // prevent Vue adding aliased props
}
return key in target;
},
get(target, prop, receiver) {
const propToGet = aliasMap[prop] || prop;
return Reflect.get(target, propToGet);
},
set(target, prop, value, receiver) {
const propToSet = aliasMap[prop] || prop;
return Reflect.set(target, propToSet, value)
}
};
return new Proxy(obj, handler);
}
new Vue({
el: '#app',
data: {
documents: [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
},
computed: {
checkBoxItems() {
const aliases = {
title: 'name',
state: 'deleted'
}
return this.documents.map(doc => aliasProps(doc, aliases));
}
},
methods: {
saveSettings: function() {
console.log(this.documents);
}
},
});
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuetify/dist/vuetify.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons" rel="stylesheet"/>
<link href="https://unpkg.com/vuetify/dist/vuetify.min.css" rel="stylesheet"/>
<div id="app">
<v-app id="theapp">
<v-container>
<checkboxgroup :items="checkBoxItems"></checkboxgroup>
<v-btn color="info"
#click="saveSettings">Save</v-btn>
</v-container>
</v-app>
</div>
<template id="checkboxGroup">
<v-container style="display: flex">
<v-checkbox
v-for="(item, index) in items"
:key="index"
v-model="item.state"
:label="item.title"
></v-checkbox>
</v-container>
</template>
Original answer
You can use a Proxy to map the document property names during access.
<template>
...
<CheckboxGroup :items="checkBoxItems"/>
...
</template>
<script>
export default {
...
computed: {
checkBoxItems() {
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
return documents.map(doc => new Proxy(doc, handler))
},
},
...
}
</script>
Demo
const documents = [
{ id: 1, name: "Doc 1", deleted: false },
{ id: 2, name: "Doc 2", deleted: false },
{ id: 3, name: "Doc 3", deleted: true },
]
const handler = {
get: function(target, prop) {
return prop === 'title' ? target.name :
prop === 'state' ? target.deleted :
target[prop];
},
set(obj, prop, value) {
const propToSet =
prop === 'title' ? 'name' :
prop === 'state' ? 'deleted' :
prop;
obj[propToSet] = value;
}
};
const checkItems = documents.map(doc => new Proxy(doc, handler))
console.log('Accessing new property names via checkItems')
checkItems.forEach(ci => console.log(ci.id, ci.title, ci.state))
console.log('After update, values of documents')
checkItems.forEach(ci => ci.state = !ci.state )
documents.forEach(doc => console.log(doc.id, doc.name, doc.deleted))

Related

Vue: existing array, add click effect and set true?

I am trying my hands at Nuxt and having a problem that I can't seem to figure out.
The concept:
I am having two arrays, one called items and one called selectedItems. I loop through the array of items to showcase them, if I click them, I simply add them to the array of selectItems and if I click them again, then they are removed from the array. Now I also added a click variable which helps enable coloring in the text to showcase which are selected. I tried simplifying the example with array of [A,B,C] in the code snippet down below!
The problem:
Now in the page, you will already have existing items in the selectedItems like in my example code, whereas [B] already exists in the array. Now how would I on a pageload, set the objects in the array SelectItems, in this case B to have already been clicked? I tried doing a comparing of the items in array Item and array selectedItem via a computed and then a if statement on the component for a class. This worked sorta EXCEPT the element wasn't actually clicked, so you would have to click TWICE in order for the object to be removed in the array. So how do I make the objects from selectedItem be click=true?
Vue.component('Item', {
template: `
<div>
<div :class="{'clicked': clicked,}" #click="selectItem()">
{{item.name}}
</div>
</div>
`,
props: {
item: {
type: Object,
default: null,
},
},
data() {
return {
clicked: false,
}
},
methods: {
selectItem() {
this.clicked = !this.clicked
const clickedItem = this.item
const id = clickedItem.id
if (this.clicked === true) {
this.$emit('add-item', clickedItem)
} else {
this.$emit('remove-item', id)
}
},
},
})
new Vue({
el: '#demo',
data() {
return {
items: [{id: 1, name: 'a'}, {id: 2,name: 'b'}, {id: 3, name: 'c'}],
selectedItems: [{id: 2, name: 'b'}],
}
},
methods: {
addItemToArray(clickedItem){
this.selectedItems = [...this.selectedItems, clickedItem]
},
removeItemFromArray(id){
this.selectedItems = this.selectedItems.filter(
(clickedItem) => clickedItem.id !== id
)
},
}
})
.clicked {background-color: coral;}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<template>
<div v-for="item in items" :key="item.id">
<Item :item="item" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
</div>
</template>
</div>
The code:
https://stackblitz.com/edit/nuxt-starter-cxakbv?file=pages/index.vue
You can try this way:
In index you pass selectedItems to Item component:
<Item :item="item" :selectedItems="selectedItems" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
In Item component add new property selectedItem:
selectedItems: {
type: Array,
default: null,
},
And in mounted hook you check is item in selectItems array:
mounted() {
if (this.selectedItems.some(item => item.id === this.item.id)) this.clicked = true
}
Vue.component('Item', {
template: `
<div>
<div :class="{'clicked': clicked,}" #click="selectItem()">
{{item.name}}
</div>
</div>
`,
props: {
item: {
type: Object,
default: null,
},
selecteditems: {
type: Array,
default: null,
},
},
data() {
return {
clicked: false,
}
},
methods: {
selectItem() {
this.clicked = !this.clicked
const clickedItem = this.item
const id = clickedItem.id
if (this.clicked === true) {
this.$emit('add-item', clickedItem)
} else {
this.$emit('remove-item', id)
}
},
},
mounted() {
if (this.selecteditems.some(item => item.id === this.item.id)) this.clicked = true
}
})
new Vue({
el: '#demo',
data() {
return {
items: [{id: 1, name: 'a'}, {id: 2,name: 'b'}, {id: 3, name: 'c'}],
selectedItems: [{id: 2, name: 'b'}],
}
},
methods: {
addItemToArray(clickedItem){
this.selectedItems = [...this.selectedItems, clickedItem]
},
removeItemFromArray(id){
this.selectedItems = this.selectedItems.filter(
(clickedItem) => clickedItem.id !== id
)
},
}
})
.clicked {background-color: coral;}</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<template>
<div v-for="item in items" :key="item.id">
<Item :item="item" :selectedItems="selectedItems" #add-item="addItemToArray"
#remove-item="removeItemFromArray"> </Item>
</div>
</template>
</div>

how to alter a single row in v-data-table (not styling, the content)

I wasn't able to find an answer for this use case of v-data-table.I know that you can use template and slots to modify a certain column but what if i want my value to be reflected only in one row? So in my code, everytime a user right clicks on the name column it adds a logo to show the value is copied and then after 3 seconds it removes it from the name -kind of like a toggle effect.
It works well whenever i click on a name on a certain row, and it copies the link value for that specific link by using vue-clipboard's library. However, it also does the same thing for all the other columns that have link. I would like to do it for only one. I couldn't make the vue-clipboard library run in sandbox so i'm sharing my code snippets.
In order to better show the current behavior, this is a screenshot from the v-data-table. (as you can see, it shows the check icon in both rows even though i only click on the first one. The expected behavior would only show the check icon the cell that has been clicked on .
template;
<template>
<v-data-table
:headers="headers"
:items="tableData"
class="display-stats"
:items-per-page="5"
:footer-props="{
'items-per-page-options': rowsPerPageItems,
}"
>
<template v-slot:[`item.name`]="{ item }">
<span v-if="item.link" class="link-span" #contextmenu="copyLink(item.link)">
<a class="preview-link" :href="item.preview" target="_blank">{{ item.name }}</a>
<p v-show="copied">
<v-icon small :color="green">fas fa-check</v-icon>
</p>
</span>
<span v-else>
{{ item.name }}
</span>
</template>
</v-data-table>
</template>
script;
<script lang="ts">
import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'
VueClipboard.config.autoSetContainer = true // add this line
Vue.use(VueClipboard)
interface PriceStats {
rowsPerPageItems: Number[]
copied: boolean
}
export default Vue.extend({
name: 'Component',
props: {
priceData: {
type: Array as () => Array<PriceStats>,
default: () => {},
},
loading: {
type: Boolean,
default: false,
},
},
data(): PriceData {
return {
rowsPerPageItems: [10, 20, 30],
copied: false,
}
},
computed: {
tableData:{
get():PriceStats[]{
if (this.priceData) {
return this.priceData
} else {
return []
}
},
set(newVal:PriceStats){
this.tableData=newVal
}
},
headers(): DataTableHeader[] {
return [
{
text: 'Name',
value: 'name',
},
{
text: 'Age',
value: 'age',
align: 'center',
},
{
text: 'Salary',
value: 'salary',
},
{
text: 'Position',
value: 'format',
},
{
text: 'Date',
value: 'date',
},
{
text: 'Premium',
value: 'premium',
align: 'right',
},
]
},
},
methods: {
copyLink(previewLink: string) {
this.$copyText(previewLink).then(
(e) => {
this.copied = true
setTimeout(()=> {
this.copied = false
},3000)
},
(e) => {
need an error logic here
this.copied = false
}
)
},
},
})
</script>
Let's assume that users cannot have the same name, you can check if the name is equal to the one on the row copied then display the icon there.
like this:
<v-data-table ...>
<span v-if="item.link" class="link-span" #contextmenu="copyLink(item.link,item.name)">
<a class="preview-link" :href="item.preview" target="_blank">{{ item.name }}</a>
<p v-show="item.name == copiedName">
<v-icon small :color="green">fas fa-check</v-icon>
</p>
</span>
</v-data-table>
copiedName can be an external variable that you assign the name of the user using the function copyLink
...
copyLink(previewLink: string,name) {
this.$copyText(previewLink).then(
(e) => {
this.copied = true
this.copiedName = name
setTimeout(()=> {
this.copied = false
},3000)
},
(e) => {
need an error logic here
this.copied = false
}
)
},

Using VMenu from vuetify with render function (scoped slot)

I'm trying to use Vuetify's VMenu component and I would like that when a user clicks the button the VMenu shows up. As far as the docs goes it says we should add a scoped slot. Doing with a normal template it works but when I switch to a render function approach it never renders the button.
I have been following the Vue's docs and ended up with:
h(VMenu, { props: { value: isMenuOpen.value } }, [
h(
"template",
{
scopedSlots: {
activator: ({ on, attrs }) => {
debugger; // it never reaches this debugger
return h(VButton, { on, attrs }, 'click me');
}
},
},
[]
),
h(VList, [h(VListItem, [h(VListItemTitle, ["Logout"])])]),
]),
I have tried using a non-arrow function as well:
scopedSlots: { activator: function({ on, attrs }) { return h('div', 'click me'); } }
and return a simple h('div', 'click me') in both non-arrow function and arrow function and nothing seems to work.
How can I pass the scoped slot activator to VMenu component?
Scoped slots are passed through the scopedSlots property of createElement's 2nd argument in the form of { name: props => VNode | Array<VNode> }. In your case, scopedSlots should have two entries: one for activator, and another for default:
import { VMenu, VList, VListItem, VBtn } from 'vuetify/lib'
export default {
render(h) {
return h(VMenu, {
scopedSlots: {
activator: props => h(VBtn, props, 'Open'),
default: () => h(VList, [
h(VListItem, 'item 1'),
h(VListItem, 'item 2'),
h(VListItem, 'item 3'),
]),
},
})
}
}
which is equivalent to this template:
<template>
<v-menu>
<template v-slot:activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on">Open</v-btn>
</template>
<v-list>
<v-list-item>item 1</v-list-item>
<v-list-item>item 2</v-list-item>
<v-list-item>item 3</v-list-item>
</v-list>
</v-menu>
</template>
demo
I wasn't able to fully understand the problem described in my question. This is an answer not to answer the fully original question but to guide future users that may come to this question.
Instead of using a scoped slot I have used the value prop in combination with attach prop. This solution in the end ended up working without no problem.
button(
{
attrs: { "data-account-setting": true },
props: { plain: true, rounded: true, icon: true },
on: { click: onOpenMenuClick },
},
[h(VIcon, ["mdi-account-outline"])]
),
h(
VMenu,
{
props: {
value: isMenuOpen.value,
// waiting on answer on SO
// #see https://stackoverflow.com/questions/67405594/using-vmenu-from-vuetify-with-render-function-scoped-slot
attach: "[data-account-setting]",
minWidth: "300px",
left: true,
offsetY: true,
closeOnContentClick: false,
rounded: true,
},
on: {
input: (value: boolean) => {
isMenuOpen.value = value;
},
},
},
[
h(VList, { props: { dense: true } }, [
h(VListItem, { props: { to: { name: "logout" } } }, [
h(VListItemTitle, { attrs: { 'data-cy-logout': true } }, ["Logout"]),
]),
]),
]
),

Vue reactivity, prevent re-rendering component on prop update with input v-model

I am trying to workout how I can mutate a prop in a text input without causing the component to re-render.
Here is my code
//App.vue
<template>
<div class="row">
<category-component v-for="category in categories" :key="category.title" :category="category" />
</div>
</template>
export default {
name: "App",
data() {
return {
categories: [
{
title: "Cat 1",
description: "description"
},
{
title: "Cat 2",
description: "description"
},
{
title: "Cat 3",
description: "description"
}
]
};
},
components: {
CategoryComponent
}
}
// CategoryComponent.vue
<template>
<div>
<input type="text" v-model="category.title">
<br>
<textarea v-model="category.description"></textarea>
</div>
</template>
When I update the text input, the component re-renders and I lose focus. However, when I update the textarea, it does not re-render.
Can anyone shed any light on this? I am obviously missing something simple with vue reactivity.
Here is a basic codesandbox of my issue.
The issue you met is caused by :key="category.title". Check Vue Guide: key
The key special attribute is primarily used as a hint for Vue’s
virtual DOM algorithm to identify VNodes when diffing the new list of
nodes against the old list. Without keys, Vue uses an algorithm that
minimizes element movement and tries to patch/reuse elements of the
same type in-place as much as possible. With keys, it will reorder
elements based on the order change of keys, and elements with keys
that are no longer present will always be removed/destroyed.
When change the title, the key is changed, then the VNode will be destroyed then created, that is why it lost the focus and it would not lose the focus when changing the summary.
So the fix is uses second parameter=index of v-for as the key instead.
And don't mutate the props directly, so in below snippet, I used one computed setter to emit one input event to inform the parent component update the binding values (by v-model).
Also you can check this question: updating-a-prop-inside-a-child-component-so-it-updates-on-the-parent for more details on updating a prop inside the component.
Vue.component('category-component', {
template:`
<div class="hello">
<input type="text" v-model="innerTitle">
<br>
<textarea v-model="innerSummary"></textarea>
</div>
`,
props: {
value: {
default: () => {return {}},
type: Object
}
},
computed: {
innerTitle: {
get: function () {
return this.value.title
},
set: function (val) {
this.$emit('input', {summary: this.value.summary, title: val})
}
},
innerSummary: {
get: function () {
return this.value.summary
},
set: function (val) {
this.$emit('input', {title: this.value.title, summary: val})
}
}
}
})
new Vue ({
el:'#app',
data () {
return {
categories: [
{
title: "Cat 1",
summary: "description"
},
{
title: "Cat 2",
summary: "description"
},
{
title: "Cat 3",
summary: "description"
}
]
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
{{ categories }}
<category-component v-for="(category, index) in categories" :key="index" v-model="categories[index]" />
</div>

Props not being assigned to data() attribute in Vue

I am creating a Vue component, which should refresh restaurants depending on user dynamically selected filters.
Therefor I have to update the filteredRestaurants in the data() function of my Vue component.
However, at first, when the Vue component is rendered, it takes the restaurant information from the "restaurants" prop.
I have tried to insert the "restaurants" into the filteredRestaurants data attribute to set it as a default value. Unfortunatelly then the stores wouldnt show at tall, as if the "restaurants" prop is inserted after the filteredRestaurants is assigned its value.
My question is, how can i get the "restaurants" prop into filteredRestaurants so that I can later on, re-render the Vue component when the user changes the filters.
<template lang="html">
<div class="test">
<Filters></Filters>
<div>
<ul class="o-list c-stores">
<Result v-bind:total="restaurants.length" v-bind:open="isOpen" v-on:toggle="toggleRestaurantList"></Result>
<li v-for="(restaurant, index) in restaurants" class="c-stores__location" :class="{'first': isFirst(index), 'last': isLast(index, restaurants)}">
<Location :index="index" :store="restaurant" :link="() => setCurrentRestaurant(restaurant)"></Location>
</li>
</ul>
</div>
</div>
</template>
<script>
import eventHub from './../../event-hubs/storefinder'
import Location from './Location'
import Filters from './Filters'
import Result from './Result'
export default {
props: ["restaurants", "isOpen", "currentSearch"],
data() {
return {
attributes : [],
// Here I am assigning the prop
filteredRestaurants : this.restaurants
}
},
head: {
title: function () {
return {
inner: this.$t('storefinder.overview')
}
},
meta: function functionName() {
return [{
name: 'og:title',
content: this.$t('storefinder.overview') + ' - ' + this.$t('storefinder.name'),
id: "og-title"
},
{
name: 'description',
content: this.$t('storefinder.description'),
id: "meta-description"
},
{
name: 'og:description',
content: this.$t('storefinder.description'),
id: "og-description"
},
]
}
},
components: {
Location,
Filters,
Result
},
methods: {
toggleRestaurantList() {
eventHub.$emit('showRestaurantList');
},
setCurrentRestaurant(restaurant) {
this.trackRestaurantSelect(restaurant.publicNameSlug);
this.$router.push({
name: "store",
params: {
restaurant: restaurant.publicNameSlug
}
});
},
trackRestaurantSelect(restaurantName) {
dataLayer.push({
'event': 'GAEvent',
'eventCategory': 'restaurants',
'eventAction': 'clickResult',
'eventLabel': restaurantName,
'eventValue': undefined,
'searchTerm': this.currentSearch && this.currentSearch.toLowerCase(),
'amountSearchResults': 1
});
},
created() {
eventHub.$on('addFilterTheRestaurants', (attribute) => this.attributes.push(attribute));
eventHub.$on('removeFilterTheRestaurants', (attribute) => this.attributes = this.attributes.filter(item => item !== attribute));
},
isLast: function (idx, list) {
return idx === list.length - 1;
},
isFirst: function (idx) {
return idx === 0;
},
}
}
</script>
The only way this worked, was when I had the filteredRestaurants as a function which returned "restaurants", and I called it inside the Vue template:
filteredRestaurants(){
return this.restaurants
}
Any help appreciated.
If I understand your question correctly, you are looking for a computed property:
computed: {
filteredRestaurants() {
return this.restaurants;
}
}
This will update whenever the value of this.restaurants changes.

Categories

Resources