vuetify autocomplete allow unknown items between chips - vue.js

I am trying to modify the sample code at https://vuetifyjs.com/en/components/autocompletes#example-scoped-slots to allow arbitrary content not matching any autocomplete items in between chips (so user can tag other users in a message similar to slack and facebook)
So for example, the user could type "Sandra" and then select "sandra adams", then type "foo" and then type another space and start typing "John" and the autcomplete would pop up again and allow the user to select "John Smith".
I've been through all the properties in the docs and there doesn't seem to be support for this built in.
I tried using custom filtering to ignore the irrelevant parts of the message when displaying autocomplete options, but the autocomplete seems to remove non-chip content when it loses focus and I can't see a property that allows me to prevent this behavior.
not sure if the autcomplete is the thing to be using or if I would be better off hacking combo box to meet this requirement, because this sample seems closer to what I'm tryng to do https://vuetifyjs.com/en/components/combobox#example-no-data, but then I believe I lose the ajax capabilities that come with automcomplete.

You can achieve this by combining the async search of the autocomplete with the combobox.
For example:
new Vue({
el: '#app',
data: () => ({
activator: null,
attach: null,
colors: ['green', 'purple', 'indigo', 'cyan', 'teal', 'orange'],
editing: null,
descriptionLimit: 60,
index: -1,
nonce: 1,
menu: false,
count: 0,
model: [],
x: 0,
search: null,
entries: [],
y: 0
}),
computed: {
fields () {
if (!this.model) return []
return Object.keys(this.model).map(key => {
return {
key,
value: this.model[key] || 'n/a'
}
})
},
items () {
return this.entries.map(entry => {
const Description = entry.Description.length > this.descriptionLimit
? entry.Description.slice(0, this.descriptionLimit) + '...'
: entry.Description
return Object.assign({}, entry, { Description })
})
}
},
watch: {
search (val, prev) {
// Lazily load input items
axios.get('https://api.publicapis.org/entries')
.then(res => {
console.log(res.data)
const { count, entries } = res.data
this.count = count
this.entries = entries
})
.catch(err => {
console.log(err)
})
.finally(() => (this.isLoading = false))
/*if (val.length === prev.length) return
this.model = val.map(v => {
if (typeof v === 'string') {
v = {
text: v,
color: this.colors[this.nonce - 1]
}
this.items.push(v)
this.nonce++
}
return v
})*/
},
model (val, prev) {
if (val.length === prev.length) return
this.model = val.map(v => {
if (typeof v === 'string') {
v = {
Description: v
}
this.items.push(v)
this.nonce++
}
return v
})
}
},
methods: {
edit (index, item) {
if (!this.editing) {
this.editing = item
this.index = index
} else {
this.editing = null
this.index = -1
}
},
filter (item, queryText, itemText) {
const hasValue = val => val != null ? val : ''
const text = hasValue(itemText)
const query = hasValue(queryText)
return text.toString()
.toLowerCase()
.indexOf(query.toString().toLowerCase()) > -1
}
}
})
<link href='https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons' rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify/dist/vuetify.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js" integrity="sha256-mpnrJ5DpEZZkwkE1ZgkEQQJW/46CSEh/STrZKOB/qoM=" crossorigin="anonymous"></script>
<div id="app">
<v-app>
<v-content>
<v-container>
<v-combobox
v-model="model"
:filter="filter"
:hide-no-data="!search"
:items="items"
:search-input.sync="search"
hide-selected
label="Search for an option"
:allow-overflow="false"
multiple
small-chips
solo
hide-selected
return-object
item-text="Description"
item-value="API"
:menu-props="{ closeOnClick: false, closeOnContentClick: false, openOnClick: false, maxHeight: 200 }"
dark
>
<template slot="no-data">
<v-list-tile>
<span class="subheading">Create</span>
<v-chip
label
small
>
{{ search }}
</v-chip>
</v-list-tile>
</template>
<template
v-if="item === Object(item)"
slot="selection"
slot-scope="{ item, parent, selected }"
>
<v-chip
:selected="selected"
label
small
>
<span class="pr-2">
{{ item.Description }}
</span>
<v-icon
small
#click="parent.selectItem(item)"
>close</v-icon>
</v-chip>
</template>
<template
slot="item"
slot-scope="{ index, item, parent }"
>
<v-list-tile-content>
<v-text-field
v-if="editing === item.Description"
v-model="editing"
autofocus
flat
hide-details
solo
#keyup.enter="edit(index, item)"
></v-text-field>
<v-chip
v-else
dark
label
small
>
{{ item.Description }}
</v-chip>
</v-list-tile-content>
</template>
</v-combobox>
</v-container>
</v-content>
</v-app>
</div>

so I ended up building a renderless component that is compatible with vuetify as it goes through the default slot and finds any of the types of tags (textarea, input with type of text, or contenteditable) that tribute supports, and allows you to put arbitrary vue that will be used to build the tribute menu items via a scoped slot.
in future might try to wrap it as a small NPM package to anyone who wants a declarative way to leverage tribute.js for vue in a more flexible way than vue-tribute allows, but for now here's my proof of concept
InputWithMentions.vue
<script>
import Tribute from "tributejs"
// eslint-disable-next-line
import * as css from "tributejs/dist/tribute.css"
import Vue from "vue"
export default {
mounted() {
let menuItemSlot = this.$scopedSlots.default
let tribute = new Tribute({
menuItemTemplate: item =>
{
let menuItemComponent =
new Vue({
render: function (createElement) {
return createElement('div', menuItemSlot({ menuItem: item }))
}
})
menuItemComponent.$mount()
return menuItemComponent.$el.outerHTML
},
values: [
{key: 'Phil Heartman', value: 'pheartman'},
{key: 'Gordon Ramsey', value: 'gramsey'}
]})
tribute.attach(this.$slots.default[0].elm.querySelectorAll('textarea, input[type=text], [contenteditable]'))
},
render(createElement) {
return createElement('div', this.$slots.default)
}
}
</script>
User.vue
<InputWithMentions>
<v-textarea
box
label="Label"
auto-grow
value="The Woodman set to work at once, and so sharp was his axe that the tree was soon chopped nearly through.">
</v-textarea>
<template slot-scope="{ menuItem }">
<v-avatar size="20" color="grey lighten-4">
<img src="https://vuetifyjs.com/apple-touch-icon-180x180.png" alt="avatar">
</v-avatar>
{{ menuItem.string }}
</template>
</InputWithMentions>

Related

Vue-multiselect prevent selecting any items when using Single select (object)

I'm using Vue-multiselect 2.1.4
It works like a charm when I use single select with array options. But in case of using single select with array of objects, all items are green and they are not selectable! (They have "is-selected" class)
To clarify the problem, I used the sample code from the project website and replace the options with my data.
<multiselect v-model="value" deselect-label="Can't remove this value"
track-by="name" label="name" placeholder="Select one"
:options="options" :searchable="false" :allow-empty="false">
<template slot="singleLabel" slot-scope="{ option }">
<strong>{{ option.name }}</strong> is written in
<strong> {{ option.language }}</strong>
</template>
</multiselect>
const config = {
data() {
return {
value: null,
options: []
}
},
async mounted() {
await this.getTerminals();
},
methods: {
async getTerminals() {
await window.axios.get("/api/Operation/GetTerminals")
.then(resp => {
this.$data.options = resp.data;
})
.catch(err => {
console.error(err);
});
},
}
};
const app = Vue.createApp(config);
app.component('Multiselect', VueformMultiselect);
app.mount('#app');
In case of array of objects, first you need to populate the values in object and then push the object in options array. And there will be few changes in the template as well. For example if your object is like this, following will work:
data(){
return{
value: null,
option: {
value: "",
name: "",
icon: "",
},
options: [],
}
},
methods: {
getData(){
//call your service here
response.data.list.forEach((item, index) => {
self.option.value = item.first_name + item.last_name;
self.option.name = item.first_name + " " + item.last_name;
self.option.icon =
typeof item.avatar !== "undefined" && item.avatar != null
? item.avatar
: this.$assetPath + "images/userpic-placeholder.svg";
self.options.push({ ...self.option });
});
}
}
Then in the template fill the options like this:
<Multiselect
v-model="value"
deselect-label="Can't remove this value"
track-by="value"
label="name"
:options="options"
:searchable="false"
:allow-empty="false"
>
<template v-slot:singlelabel="{ value }">
<div class="multiselect-single-label">
<img class="character-label-icon" :src="value.icon" />
{{ value.name }}
</div>
</template>
<template v-slot:option="{ option }">
<img class="character-option-icon" :src="option.icon" />
{{ option.name }}
</template>
</Multiselect>
Call your getData() function in created hook.
For me the solution was to use the "name" and "value" keys for my object. Anything else and it doesn't work (even if they use different keys in the documenation). This seems like a bug, but that was the only change I needed to make.

Testing a wrapped vuetify autocomplete component v menu does not open in html

I want to test my custom component via vue-test-utils. I use vuetify autocomplete component in this case. I just wrapped with div and extract as a custom component. Nothing to do special so far.
This component is really works on development. I have no problem with it.
I used this component as shown below.
<my-component
id="brand-dropdown"
ref="brand-dropdown"
:items="brands"
labelKey="product.brand"
item-text="name"
item-value="name"
v-model="product.brandName"/>
My custom component looks like:
<template>
<div class="comboContainer">
<v-autocomplete
class="product-combobox"
ref="complete"
:items="localItems"
append-icon="$dropdownArrow"
outlined
dense
hide-details
color="#999999"
:item-text="itemText"
:item-value="itemValue"
:filter="autoCompleteFilter"
v-model="selected"
#change="onListItemSelected">
<template slot="label">
<span>{{$t(labelKey)}}</span>
</template>
<template slot="prepend-inner" v-if="showSearchIcon">
<div class="d-flex align-center justify-center"
style="height: 25px;">
<img src="~/assets/icons/search/search.png"/>
</div>
</template>
<template v-slot:no-data>
<v-list-item id="noMatchText">
<v-list-item-title>
<div :no-data-item="dataTestId">
{{ $t('selection.noMatchFound') }}
</div>
</v-list-item-title>
</v-list-item>
</template>
<template v-slot:item="{ item }">
<v-tooltip top color="transparent">
<template v-slot:activator="{ on, attrs }">
<div
v-on="item[itemText].length >36?on:null"
class="combobox-item comboboxOverFlow"
:data-testid="itemDataTestIdPrefix+item.id"
:parent-list="dataTestId">
<span>{{ item[itemText] }}</span>
</div>
</template>
<div class="popup-rectangle">
<span>{{ item[itemText] }}</span>
</div>
</v-tooltip>
</template>
</v-autocomplete>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: [],
},
labelKey: {
type: String,
default: '',
},
itemText: {
type: String,
default: '',
},
itemValue: {
type: String,
default: '',
},
value: {
type: [Number, String, Array],
},
disabled: {
type: Boolean,
default: false,
}
shouldTriggerWatch: {
type: Boolean,
default: false,
}
},
data() {
return {
selected: this.value,
showSearchIcon: false,
};
},
watch: {
value: {
immediate: true,
handler(val) {
if (this.shouldTriggerWatch) {
this.selected = val;
this.$emit('input', this.selected);
}
},
},
},
computed: {
localItems() {
if (this.items && this.items.length) {
return this.items.map(x => ({
...x,
displayName: x.lang_key ? this.$t(x.lang_key) : x.name,
}));
}
return [];
},
},
methods: {
onListItemSelected() {
this.$emit('input', this.selected);
},
autoCompleteFilter(item, queryText, itemText) {
const re = new RegExp(`(\\s+|^)(${queryText.toLocaleLowerCase()})(.*|$)`);
return re.test(itemText.toLocaleLowerCase());
},
},
};
I want to test three test case.
When I pass empty list should render as empty dropdown and render no-data slot.
<template v-slot:no-data>
<v-list-item id="noMatchText">
<v-list-item-title>
<div :no-data-item="dataTestId">
{{ $t('selection.noMatchFound') }}
</div>
</v-list-item-title>
</v-list-item>
</template>
When I pass filled list with test data I want to test all items rendered correctly
<template v-slot:item="{ item }">
<v-tooltip top color="transparent">
<template v-slot:activator="{ on, attrs }">
<div
v-on="item[itemText].length >36?on:null"
class="combobox-item comboboxOverFlow"
:data-testid="itemDataTestIdPrefix+item.id"
:parent-list="dataTestId">
<span>{{ item[itemText] }}</span>
</div>
</template>
<div class="popup-rectangle">
<span>{{ item[itemText] }}</span>
</div>
</v-tooltip>
</template>
When I search I want to provide my filter function works as expected. And autocomplete renders items according to filtered value.
This is what my test file.
function mountComponent(options) {
return mount(ComboBox, {
vuetify: new Vuetify(),
sync: false,
mocks: {
$t: key => key,
$i18n: { locale: 'en' },
},
...options,
});
}
describe('Combobox unit tests', () => {
beforeEach(() => {
document.body.setAttribute('data-app', 'true');
});
test('should create component successfully', () => {
const wrapper = mountComponent({ items: [] });
expect(wrapper.exists()).toBeTruthy();
});
test('should list zero items if the item list is empty', async () => {
const wrapper = mountComponent({
propsData: {
items: [],
labelKey: 'labelKey',
dataTestId: 'test-dropdown',
itemText: 'name',
itemValue: 'id',
},
});
const autocomplete = wrapper.find('.product-combobox');
const autocompleteControls = autocomplete.find('.v-input__slot');
autocompleteControls.trigger('click');
await wrapper.vm.$nextTick();
await flushPromises();
**// v menu cant opened !!!!**
});
test('should list third items correctly', async () => {
const testItems = [{ name: 'item1', id: 1 }, { name: 'item2', id: 2 }, { name: 'item3', id: 3 }];
const wrapper = mountComponent({
attachToDocument: true,
propsData: {
eagerProp: true,
items: testItems,
dataTestId: 'test-dropdown',
itemDataTestIdPrefix: 'test-dropdown-item-',
itemText: 'name',
itemValue: 'id',
value: null,
},
});
const slot = wrapper.find('.v-input__slot');
const input = wrapper.find('input');
// Focus input should only focus
input.trigger('focus');
expect(wrapper.vm.$children[0].isFocused).toBe(true);
expect(wrapper.vm.$children[0].menuCanShow).toBe(true);
expect(wrapper.vm.$children[0].isMenuActive).toBe(false);
slot.trigger('click');
expect(wrapper.vm.$children[0].isMenuActive).toBe(true);
expect(wrapper.vm.$children[0].menuCanShow).toBe(true);
wrapper.setProps({ searchInput: 'foo' });
expect(wrapper.vm.$children[0].isMenuActive).toBe(true);
expect(wrapper.vm.$children[0].menuCanShow).toBe(true);
// v menu cant opened !!!!
// you think these expects is unnecesary. you are right I just explore this component
// if I success I'll delete all of them
});
test('should filter autocomplete search results', async () => {
});
});
I can't open v-menu in test enviroment.
I tried emit events and trigger('click')
When I console log wrapper.html() I cant see v menu is opened and all items rendered in html. Output is shown below.
<div class="comboContainer">
<div class="v-input product-combobox v-input--hide-details v-input--dense theme--light v-text-field v-text-field--enclosed v-text-field--outlined v-select v-autocomplete">
<div class="v-input__control">
<div role="combobox" aria-haspopup="listbox" aria-expanded="false" aria-owns="list-2" class="v-input__slot" style="height: 44px;">
<fieldset aria-hidden="true">
<legend style="width: 0px;"><span>​</span></legend>
</fieldset>
<div class="v-select__slot">
<label for="input-2" class="v-label theme--light" style="left: 0px; position: absolute;"><span></span></label><input data-testid="test-dropdown" id="input-2" type="text" autocomplete="off">
<div class="v-input__append-inner">
<div class="v-input__icon v-input__icon--append"><i aria-hidden="true" class="v-icon notranslate material-icons theme--light">$dropdownArrow</i></div>
</div>
<input type="hidden">
</div>
<div class="v-menu"></div>
</div>
</div>
</div>
</div>
My problem is where is items? <div class="v-menu"></div> Why I cant see in wrapper.html. I don't use shallowMount, I use mount. Because of this I cannot write my covered test cases.
How can I simulate opened menu and rendered all items and provide some assertions?
What am I missing?
Versions
"#nuxtjs/vuetify": "1.12.1",
"#vue/test-utils": "1.0.0-beta.29",
"vue-jest": "3.0.7"
Well, probably the better way would be mocking and substituting v-autocomplete with a test component (just control values that go to it and emit fake events from it). You are not developing Vuetify, so no need to test what happens inside a component.

Cannot get computed property (array)

Trying to get a 'displayImages' array as a computed property. Using a default 'selected' property = 0.
this.selected changes accordingly on mouseover and click events.
When trying to get the computed 'displayImages' it says:
"this.variations[this.selected] is undefined."
I'm using an api to get my product data and images.
<template>
<div id="product-page">
<v-card width="100%" class="product-card">
<div class="image-carousel">
<v-carousel height="100%" continuos hide-delimiters>
<v-carousel-item
v-for="(image, i) in displayImages"
:key="i"
:src="image"
>
</v-carousel-item>
</v-carousel>
</div>
<div class="details">
<h2>{{ this.title }}<br />Price: ${{ this.price }}</h2>
<p>{{ this.details }}</p>
<ul style="list-style: none; padding: 0">
<li
style="border: 1px solid red; width: auto"
v-for="(color, index) in variations"
:key="index"
#mouseover="updateProduct(index)"
#click="updateProduct(index)"
>
{{ color.color }}
</li>
</ul>
<div class="buttons">
<v-btn outlined rounded
>ADD TO CART<v-icon right>mdi-cart-plus</v-icon></v-btn
>
<router-link to="/shop">
<v-btn text outlined rounded> BACK TO SHOP</v-btn>
</router-link>
</div>
</div>
</v-card>
</div>
</template>
<script>
export default {
name: "Product",
props: ["APIurl"],
data: () => ({
title: "",
details: "",
price: "",
variations: [],
selected: 0,
}),
created() {
fetch(this.APIurl + "/products/" + this.$route.params.id)
.then((response) => response.json())
.then((data) => {
//console.log(data);
this.title = data.title;
this.details = data.details.toLowerCase();
this.price = data.price;
data.variations.forEach((element) => {
let imagesArray = element.photos.map(
(image) => this.APIurl + image.url
);
this.variations.push({
color: element.title,
images: imagesArray,
qty: element.qty,
productId: element.productId,
});
});
});
},
computed: {
displayImages() {
return this.variations[this.selected].images;
},
},
methods: {
updateProduct: function (index) {
this.selected = index;
console.log(index);
}
},
};
</script>
To properly expand on my comment, the reason why you are running into an error is because when the computed is being accessed in the template, this.variations is an empty array. It is only being populated asynchronously, so chances are, it is empty when VueJS attempts to use it when rendering the virtual DOM.
For that reason, accessing an item within it by index (given as this.selected) will return undefined. Therefore, attempting to access a property called images in the undefined object will return an error.
To fix this problem, all you need is to introduce a guard clause in your computed as such:
computed: {
displayImages() {
const variation = this.variations[this.selected];
// GUARD: If variation is falsy, return empty array
if (!variation) {
return [];
}
return variation.images;
},
}
Bonus tip: if you one day would consider using TypeScript, you can even simplify it as such... but that's a discussion for another day ;) for now, optional chaining and the nullish coalescing operator is only supported by bleeding edge versions of evergreen browsers.
computed: {
displayImages() {
return this.variations[this.selected]?.images ?? [];
},
}
For avoid this kind of error, you must to use the safe navigation property.
Remember, it's useful just when the app is loading.
Try something like that:
<script>
export default {
name: 'Product',
computed: {
displayImages() {
if (this.variations[this.selected]) {
return this.variations[this.selected].images;
}
return [];
},
},
};
</script>

Can't add Object to an Array

I'm having an empty array for my breadcrumb and wanna fill it with every step i take in my category tree.
But as i said in Title, it won't add or to be more precise, it add but won't show!!
it wont show on my template! it wont show on console! only when i console.log("bread", Array.from(this.breadcrumbs)) it shows in console.
how can i fill my this.breadcrumbs with the category obj that i send trough an event!!
my category structure is like this: {id: 1, title: "Cat1"}
here is the code of page:
my main problem is that i'm checking if the breadcrumbs has length show it, but since i get empty arr even after push, my breadcrumb section just show the all.
Update:
i removed the if statements in my template, breadcrumb section and i when i click a cat it's title will be shown on breadcrumb but length and array still empty even as trying to display on template!! ( {{breadcrumb}} and {{breadcrumb.length}} are []and0` ).
BTW i'm on Nuxtjs 2.13
<template>
<div class="ma-4">
<!-- Breadcrumb -->
<div class="d-flex">
<template v-if="breadcrumbs.length">
<span role="button" #click.prevent="getAll()">{{lang.all}} > </span>
<span role="button" v-for="(br, index) in newBreadCrumb" :key="br.id" #click.prevent="goBread(br, index)">{{br.title}} > </span>
<span>{{breadcrumbs[breadcrumbs.length - 1]}}</span>
</template>
<template v-else>
<span>{{lang.all}}</span>
</template>
</div>
<!--btn add-->
<addcatbtn v-if="showBtn" #clicked="addNewCat()" />
<!--cards-->
<categorylistcard v-for="(category, index) in cmsCat"
:key="category.id"
:cat="category"
:index="index"
#addnewsub="addNewCat()"
/>
<!-- dialog -->
<v-dialog v-model="dialog" persistent max-width="600px">
<v-card>
<v-card-title>
<span class="headline" v-if="editId">{{lang.edit}} </span>
<span class="headline" v-else>{{lang.addcat}} </span>
</v-card-title>
<v-card-text>
<v-container grid-list-md>
<v-layout wrap>
<v-flex xs12>
<v-text-field :label="lang.title" outlined v-model="title"></v-text-field>
</v-flex>
</v-layout>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn class="text_main_color theme__cta__color px-5" #click="closeDialog()">{{lang.close}}</v-btn>
<v-btn class="text_main_color theme__btn__s px-5" #click="insertData()">{{lang.save}}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<style>
</style>
<script>
import addcatbtn from '~/components/global/cms/addcatbtn'
const categorylistcard = ()=>import('~/components/' + process.env.SITE_DIRECTION + '/cms/categorylistcard')
export default {
layout:'cms',
components:{
'categorylistcard': categorylistcard,
'addcatbtn': addcatbtn
},
data(){
return{
dialog: false,
editId: 0,
newId: 0,
title: null,
catList: [],
editIndex: 0,
parentId: 0,
editSubCat: false,
showBtn: true,
onSubCat: false,
breadcrumbs: []
}
},
methods:{
addNewCat(){
this.dialog = true
this.title = null
},
closeDialog(){
this.dialog = false
this.title = null
this.editId = 0
this.editIndex = 0
},
getAll(){
this.$fetch()
this.showBtn = true
this.onSubCat = false
this.breadcrumbs = []
},
goBread(cat, index){
this.breadcrumbs.lenght = index
$nuxt.$emit('gobread',cat)
},
async insertData(){
if(this.notEmpty(this.title)){
if(this.editId){
let response = await this.axiosGet(`category/update/${this.editId}/${this.title}`)
if(this.resOk(response.status)){
const data = {index: this.editIndex , title: this.title}
if(this.editSubCat){
this.updateCmsSubCat(data)
}else{
this.updateCmsCat(data)
}
$nuxt.$emit('editedcat', {id: this.editId, title: this.title})
this.closeDialog()
}
}else{
if(this.onSubCat){
let response = await this.axiosGet(`category/insertsub/${this.newId}/${this.title}`)
if(this.resOk(response.status)){
// TODO: must get the id from response!!
console.log('insertsub')
const data = {id: this.newId*2 , title: this.title}
this.addCmsSubCat(data)
this.closeDialog()
}
}else{
let response = await this.axiosGet(`category/insert/${this.title}`)
if(this.resOk(response.status)){
// TODO: must get the id from response!!
const data = {id: 34 , title: this.title}
this.addCmsCat(data)
this.closeDialog()
}
}
}
}
}
},
async fetch(){
let response = await this.axiosGet(`categories/admin/0/1`)
if(this.notEmpty(response.data)){
this.setCmsCat(response.data.items)
}
},
computed:{
newBreadCrumb(){
let x = this.breadcrumbs
return x.splice(this.breadcrumbs.lenght-1,1)
}
},
created(){
this.$nuxt.$on('deletecat', (index)=>{
this.removeCmsCat(index)
})
this.$nuxt.$on('editcat', (category, index)=>{
this.title = category.title
this.editId = category.id
this.editIndex = index
this.dialog = true
})
this.$nuxt.$on('setparid', (id)=>{
this.parentId = id
})
this.$nuxt.$on('editsub', ()=>{
this.editSubCat = true
})
this.$nuxt.$on('showsub', (cat)=>{
this.newId = cat.id
this.showBtn = !this.showBtn
this.onSubCat = !this.onSubCat
})
this.$nuxt.$on('addbreadcrumb', (category)=>{
this.breadcrumbs.push(category) // category:{id: 1, title: "Cat1"}
console.log('cat: ')
console.log(category) // get the obj
console.log('bread: ')
console.log(this.breadcrumbs) // get empty array
})
}
}
</script>
In your computed, you are calling splice. That mutates the object. A big no-no in vue is to mutate your state from a computed property.
Try to create a new object and return it, by calling slice first:
const copy = x.slice()
copy.splice(this.breadcrumbs.length-1,1)
return copy
Also, you have a typo lenght instead of length

How to filter a product list using v-btn in vuetify?

I am currently trying to filter a list of objects by using buttons.
I know that both v-btn and v-card loops are not currently interdependent, but I am unable to figure out how to link both sections to properly show only the correct category when pressing its respective button.
Thanks for the help!
<v-row class="my-4">
<v-btn
v-for="category in categories"
text
class="underline"
route :to="category.route"
#click="category.function"
>{{ category.text }}</v-btn>
</v-row>
<v-row
justify="start">
<v-card
v-for="product in products"
:key="product.name"
class="ma-3 text-center">
<v-img
:src="product.src"
class="mc mt-2"
>
</v-img>
<v-card-title
class="bold justify-center">
<h4>{{ product.name }}</h4>
</v-card-title>
</v-card>
</v-row>
categories: [
{ text: 'All', function: "all()" },
{ text: 'Fruits & Veggies', function: "fruitsVeggies()" },
{ text: 'Baking', function: "baking" },
{ text: 'Gadgets', function: "gadgets" },
{ text: 'Cutting Boards', function: "cuttingBoards"}],
products: [{...}, {...}, etc]
computed: {
all() {
return this.products.filter(product => {
return product
})
},
fruitsVeggies() {
return this.products.filter(product => {
return product.category === 'Fruits & Veggies'
})
},
baking() {
return this.products.filter(product => {
return product.category === 'Baking'
})
},
cuttingBoards() {
return this.products.filter(product => {
return product.category === 'Cutting Boards'
})
}
Vue computed properties return a dynamic value
When you call baking as a function, it should throw an error in the console; it's not a function, but a property
Instead, you could define a computed property called filteredProducts that changes filter based on something stored in the data, and loop over that instead
data() {
productFilter: "all"
}
computed: {
// your computed values
filteredProducts() {
if (this.productFilter === "all") {
return this.all;
}
// etc. For all filters
}
}
Then in your template:
<v-row
justify="start"
>
<v-card
v-for="product in filteredProducts"