I have used the table component from the ant design, and right now I'm stuck on why this cannot work,
The reference link is:
https://2x.antdv.com/components/table-cn#components-table-demo-edit-cell
Here's the source code from the ant design about editable table:
<template>
<a-table :columns="columns" :data-source="dataSource" bordered>
<template v-for="col in ['name', 'age', 'address']" #[col]="{ text, record }" :key="col">
<div>
<a-input
v-if="editableData[record.key]"
v-model:value="editableData[record.key][col]"
style="margin: -5px 0"
/>
<template v-else>
{{ text }}
</template>
</div>
</template>
<template #operation="{ record }">
<div class="editable-row-operations">
<span v-if="editableData[record.key]">
<a #click="save(record.key)">Save</a>
<a-popconfirm title="Sure to cancel?" #confirm="cancel(record.key)">
<a>Cancel</a>
</a-popconfirm>
</span>
<span v-else>
<a #click="edit(record.key)">Edit</a>
</span>
</div>
</template>
</a-table>
</template>
<script lang="ts">
import { cloneDeep } from 'lodash-es';
import { defineComponent, reactive, ref, UnwrapRef } from 'vue';
const columns = [
{
title: 'name',
dataIndex: 'name',
width: '25%',
slots: { customRender: 'name' },
},
{
title: 'age',
dataIndex: 'age',
width: '15%',
slots: { customRender: 'age' },
},
{
title: 'address',
dataIndex: 'address',
width: '40%',
slots: { customRender: 'address' },
},
{
title: 'operation',
dataIndex: 'operation',
slots: { customRender: 'operation' },
},
];
interface DataItem {
key: string;
name: string;
age: number;
address: string;
}
const data: DataItem[] = [];
for (let i = 0; i < 100; i++) {
data.push({
key: i.toString(),
name: `Edrward ${i}`,
age: 32,
address: `London Park no. ${i}`,
});
}
export default defineComponent({
setup() {
const dataSource = ref(data);
const editableData: UnwrapRef<Record<string, DataItem>> = reactive({});
const edit = (key: string) => {
editableData[key] = cloneDeep(dataSource.value.filter(item => key === item.key)[0]);
};
const save = (key: string) => {
Object.assign(dataSource.value.filter(item => key === item.key)[0], editableData[key]);
delete editableData[key];
};
const cancel = (key: string) => {
delete editableData[key];
};
return {
dataSource,
columns,
editingKey: '',
editableData,
edit,
save,
cancel,
};
},
});
</script>
<style scoped>
.editable-row-operations a {
margin-right: 8px;
}
</style>
However, I have the functionality that my name will be a hyperlink, so I need to put the name column and its value outside the v-for. Here's what I got:
<template>
<a-table :columns="columns" :data-source="dataSource" bordered>
<template v-for="col in ['age', 'address']" #[col]="{ text, record }" :key="col">
<div>
<a-input
v-if="editableData[record.key]"
v-model:value="editableData[record.key][col]"
style="margin: -5px 0"
/>
<template v-else>
{{ text }}
</template>
</div>
<template #name="{ text, record }">
<div>
<a-input v-if="editableData[record.key]" v-model:value="editableData[record.key][record.name]" style="margin:
-5px 0"></a-input>
<router-link v-else="v-else" to="/tables/123">{{ text }}</router-link>
</div>
</template>
</template>
<template #operation="{ record }">
<div class="editable-row-operations">
<span v-if="editableData[record.key]">
<a #click="save(record.key)">Save</a>
<a-popconfirm title="Sure to cancel?" #confirm="cancel(record.key)">
<a>Cancel</a>
</a-popconfirm>
</span>
<span v-else>
<a #click="edit(record.key)">Edit</a>
</span>
</div>
</template>
</a-table>
</template>
<script lang="ts">
import { cloneDeep } from 'lodash-es';
import { defineComponent, reactive, ref, UnwrapRef } from 'vue';
const columns = [
{
title: 'name',
dataIndex: 'name',
width: '25%',
slots: { customRender: 'name' },
},
{
title: 'age',
dataIndex: 'age',
width: '15%',
slots: { customRender: 'age' },
},
{
title: 'address',
dataIndex: 'address',
width: '40%',
slots: { customRender: 'address' },
},
{
title: 'operation',
dataIndex: 'operation',
slots: { customRender: 'operation' },
},
];
interface DataItem {
key: string;
name: string;
age: number;
address: string;
}
const data: DataItem[] = [];
for (let i = 0; i < 100; i++) {
data.push({
key: i.toString(),
name: `Edrward ${i}`,
age: 32,
address: `London Park no. ${i}`,
});
}
export default defineComponent({
setup() {
const dataSource = ref(data);
const editableData: UnwrapRef<Record<string, DataItem>> = reactive({});
const edit = (key: string) => {
editableData[key] = cloneDeep(dataSource.value.filter(item => key === item.key)[0]);
};
const save = (key: string) => {
Object.assign(dataSource.value.filter(item => key === item.key)[0], editableData[key]);
delete editableData[key];
};
const cancel = (key: string) => {
delete editableData[key];
};
return {
dataSource,
columns,
editingKey: '',
editableData,
edit,
save,
cancel,
};
},
});
</script>
<style scoped>
.editable-row-operations a {
margin-right: 8px;
}
</style>
However, when I changed my name through the editing mode, it seems that the name will not change based on the edited name. Is there anything I'm wrong? Any help will be greatly appreciated!
Ok, I finally got the answer - should be using a editableData[record.key].name when we are using v-model instead of editableData[record.key][record.name]
Related
Using Vue2 I have an array of objects in data which have an html string rendered in a v-for loop. Part of each string is a prop, which renders correctly initially. However, when the prop value is updated with v-model the data in the v-for loop is not updated.
jsfiddle: When the input is changed from "Bob" to "Sally" all instances should change, but those in the for-loop do not.
html
<div id="app">
<h2>Testing</h2>
<ul>
<li v-for="statement in statements" v-html="statement.text"></li>
</ul>
<input v-model="name" placeholder="edit name">
<p>Name is: {{ name }}</p>
<p class="italic">Outside loop: <b>{{name}}</b> likes dogs.</p>
</div>
vue
new Vue({
el: "#app",
data: function() {
return {
statements: [
{
id: 'food',
text: '<b>'+ this.name + '</b> likes to eat ice cream.',
},
{
id: 'fun',
text: 'Running is the favorite activity of <b>'+ this.name + '</b>',
},
],
}
},
props: {
name: {
type: String,
default: 'Bob',
},
},
})
The code has been simplified - the actual HTML strings have ~3 variables each that need to update, and are at different locations in each string, so I can't think of another way to replace the values when they are updated, while preserving the html tags. This is intended to be a single-page vue application, but is using Laravel and blade for some general page formatting.
name should be in data, not in props (it's not coming from a parent, it's just reactive data, which needs to be tracked for changes internally - inside this component).
statements should be in computed, because you want Vue to update it whenever its reactive references (e.g: this.name) change. Besides, this is not what you think it is inside the data function.
See it working:
new Vue({
el: "#app",
data: () => ({
name: 'Bob'
}),
computed: {
statements() {
return [
{
id: 'food',
text: '<b>'+ this.name + '</b> likes to eat ice cream.',
},
{
id: 'fun',
text: 'Runing is the favorite activity of <b>'+ this.name + '</b>',
},
]
}
},
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
}
li {
margin: 8px 0;
}
h2 {
font-weight: bold;
margin-bottom: 15px;
}
p.italic {
font-style: italic;
}
<script src="https://v2.vuejs.org/js/vue.min.js"></script>
<div id="app">
<h2>Testing</h2>
<ul>
<li v-for="(statement, key) in statements" v-html="statement.text" :key="key"></li>
</ul>
<input v-model="name" placeholder="edit name">
<p>Name is: {{ name }}</p>
<p class="italic">Outside loop: <b>{{name}}</b> likes dogs.</p>
</div>
If you're trying to create a reusable component which takes in a person (with some values) and creates the statements based on those values and also allows editing the person's values, here's how to do it:
Vue.component('person-editor', {
template: '#person-editor-tpl',
props: {
person: {
type: Object,
default: () => ({})
}
},
data: () => ({
details: [
{ name: 'name', placeholder: 'Name' },
{ name: 'fun', placeholder: 'Running', label: 'Favorite activity' },
{ name: 'food', placeholder: 'pizza', label: 'Favorite food'}
]
}),
methods: {
update(payload) {
this.$emit('update:person', { ...this.person, ...payload })
},
getDetailLabel(d) {
return d.label || (d.name[0].toUpperCase() + d.name.slice(1))
}
}
});
Vue.component('person-details', {
template: '#person-details-tpl',
props: {
person: {
type: Object,
default: () => ({})
}
},
data: () => ({
statements: [
{ id: 'food', text: p => `<b>${p.name}</b> likes to eat ${p.food}.` },
{ id: 'fun', text: p => `${p.fun} is the favorite activity of <b>${p.name}</b>` }
]
})
})
new Vue({
el: "#app",
data: () => ({
persons: [
{ name: 'Jane', food: 'apples', fun: 'Hiking' },
{ name: 'Jack', food: 'pizza', fun: 'Sleeping' }
]
}),
methods: {
updatePerson(key, value) {
this.persons.splice(key, 1, value);
}
}
})
label {
display: flex;
}
label > span {
width: 150px;
}
<script src="https://v2.vuejs.org/js/vue.min.js"></script>
<div id="app">
<template v-for="(person, key) in persons">
<hr v-if="key" :key="`hr-${key}`">
<person-details :person="person"
:key="`details-${key}`"
></person-details>
<person-editor :person.sync="person"
:key="`editor-${key}`"
#update:person="updatePerson(key, person)"></person-editor>
</template>
</div>
<template id="person-editor-tpl">
<div>
<template v-for="detail in details">
<label :key="detail.name">
<span v-text="getDetailLabel(detail)"></span>
<input :value="person[detail.name]"
#input="e => update({ [detail.name]: e.target.value })">
</label>
</template>
</div>
</template>
<template id="person-details-tpl">
<ul>
<li v-for="(statement, key) in statements" v-html="statement.text(person)"></li>
</ul>
</template>
I separated the editor and the display in two separate components.
Because I had to define the components on the Vue instance it's a bit crammed in this example, but it looks a lot more elegant when using sfcs (and each component is a standalone .vue file).
I'm trying to test the following label using Jest:
<label class="no-content-label" v-if="products.length===0 && !isTableBusy">No Content To Show</label>
with the following test:
it('Test No Content To Show When table is not empty', async () => {
const wrapper = mount(productTable, {
propsData: {
products: items1,
fields: fields1
}
})
expect(wrapper.find(".no-content-label").exists()).toBeFalsy()
wrapper.destroy();
})
where items are just: [{ a: 1, b: 2, c: 3 }, { a: 4, b: 5, c: 6 }] nut I always getting True even that the components is not showing and should get false.
The full components code:
<template>
<div id="ProductTable" class="container mt-3 mb-5">
<h1 class="mainTitle">Product Table Home</h1>
<b-alert variant="danger" :show="showError" v-for="(error,index) in errorArray" :key="index" dismissible>
{{ error }}
</b-alert>
<b-row class="mb-3">
<b-col md="3">
<b-form-input v-model="filterInput" type="search" id="filterInput" placeholder="Type to Search"></b-form-input>
</b-col>
</b-row>
<b-table id="product-table" striped hover head-variant="light"
:busy="isTableBusy"
:items="products"
:filter="filterInput"
:fields="fields"
:bordered="bordered"
:borderless="bordered"
:total-rows="rows">
<template #table-busy>
<div class="text-center text-danger my-2">
<b-spinner class="align-middle"></b-spinner>
<strong>Loading...</strong>
</div>
</template>
<template #cell(cogs.unitManufacturingCost)="row">
{{
row.item.cogs.unitManufacturingCost ? Number(row.item.cogs.unitManufacturingCost).toLocaleString() + '$' : '0$'
}}
</template>
<template #cell(cogs.shipmentUnitCost)="row">
{{ row.item.cogs.shipmentUnitCost ? Number(row.item.cogs.shipmentUnitCost).toLocaleString() + '$' : '0$' }}
</template>
<template #cell(cogs.monthlyAdvertismentCost)="row">
{{
row.item.cogs.monthlyAdvertismentCost ? Number(row.item.cogs.monthlyAdvertismentCost).toLocaleString() + '$' : '0$'
}}
</template>
<template #cell(cogs.manufacturingCountry)="row">
{{ row.item.cogs.manufacturingCountry ? row.item.cogs.manufacturingCountry : '-' }}
</template>
<template #cell(actions)="row">
<b-button size="md" variant="primary" #click="openEditProductModal(row.item)">
Edit
</b-button>
<edit-product-modal ref="EditProductModal" :item="row.item" #fetchProducts="fetchProducts"></edit-product-modal>
</template>
</b-table>
<label class="no-content-label" v-if="products.length===0 && !isTableBusy">No Content To Show</label>
</div>
</template>
<script>
import EditProductModal from "#/components/EditProductModal";
import {BAlert} from "bootstrap-vue";
import {BRow} from "bootstrap-vue";
import {BCol} from "bootstrap-vue";
import {BFormInput} from "bootstrap-vue";
import {BTable} from "bootstrap-vue";
export default {
name: "BootstrapProductTable",
components: {
EditProductModal: EditProductModal,
'b-alert': BAlert,
'b-row': BRow,
'b-col': BCol,
'b-form-input': BFormInput,
'b-table': BTable,
},
data() {
return {
filterInput: "",
bordered: true,
isTableBusy: false,
products: [],
fields: [
{key: 'productName', label: 'Product Name', sortable: true, sortDirection: 'desc'},
{key: 'cogs.unitManufacturingCost', label: 'Unit manufacturing cost', sortable: true,},
{key: 'cogs.shipmentUnitCost', label: 'Shipment unit cost', sortable: true},
{key: 'cogs.monthlyAdvertismentCost', label: 'Monthly advertising cost', sortable: true,},
{key: 'cogs.manufacturingCountry', label: 'Manufacturing country', sortable: true,},
{key: 'actions', label: 'Actions'}
],
errorArray: [],
showError: false
}
},
computed: {
rows() {
return this.products.length;
}
},
created() {
this.fetchProducts();
},
methods: {
openEditProductModal(row) {
this.$refs.EditProductModal.show(row)
},
async fetchProducts() {
try {
this.isTableBusy = true;
//delay example
// const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
// await delay(1000)
let response;
response = await this.axios.get(
"http://localhost:3000/products"
);
this.products = response.data;
} catch (error) {
this.showError = true;
this.errorArray = error.response ? error.response.data.errors : [error]
}
this.isTableBusy = false;
}
}
}
</script>
You are trying to pass products, but your component is not expecting any props.
I see that products are fetched in fetchProducts method. All api calls must be mocked. fields are already set in the component. So all you have to do is to mock api call. That's why I suggest you to mock fetchProducts method so it will be always returning the same array of products:
const mockProducts = [{ a: 1, b: 2, c: 3 }, { a: 4, b: 5, c: 6 }];
it('Test No Content To Show When table is not empty', async () => {
const wrapper = mount(productTable, {
mocks: {
fetchProducts: () => mockProducts
}
})
expect(wrapper.find(".no-content-label").exists()).toBeFalsy()
wrapper.destroy();
})
I was finally been able to fix the test by adding an id attribute to the label and finding it by id:
the label:
<label id="no-content-label" v-if="products.length===0 && !isTableBusy">No Content To Show</label>
The working tests:
let wrapper = null
beforeEach(() => {
wrapper = mount(ProductTable)
})
afterEach(() => {
wrapper.destroy()
})
it('Test "No Content To" Show appear When Products are empty', async () => {
await wrapper.setData({products: []})
expect(wrapper.find('[id="no-content-label"]').exists()).toBe(true)
expect(wrapper.find('[id="no-content-label"]').text()).toBe("No Content To Show");
})
it('Test "No Content To" not Show do not appear When Products are not empty', async () => {
await wrapper.setData({products: mockProduct})
expect(wrapper.find('[id="no-content-label"]').exists()).toBe(false);
})
I am trying to display images in a Vuetify data table column, but the image is not displaying in the desired slot, and only shows the URL of the image, e.g. "9384053.jpg". How do I display an image using slots in Vuetify?
Segment is an array containing image URLs such as 93034.jpg, 9348903.jpg etc. I am trying to display the first image only e.g. Segment[0] with a sample output of 846454.jpg.
<v-card>
<v-card-title>
Animals
<v-spacer></v-spacer>
</v-card-title>
<v-data-table
:headers="headers"
:items="entries"
:items-per-page="12"
class="elevation-3"
:multi-sort="true"
mobile-breakpoint
:search="search"
>
<template v-slot:item.Image="{ item }">
<img :src="require(`${item.Image}`)" style="width: 50px; height: 50px" />
</template>
</v-data-table>
</v-card>
Here is the script file
<script>
import firebase from '../firebaseConfig';
const db = firebase.database().ref('/');
export default {
name: 'Animals',
data: () => ({
search: '',
entries: [],
headers: [
{ text: 'ID', value: 'ID' },
{ text: 'RFID', value: 'RFID' },
{ text: 'Image', value: 'Segment[0]' },
{ text: 'Age', value: 'Age Years' },
{ text: 'Weight', value: 'Weight' },
],
}),
methods: {
readAnimals() {
db.once('value', (snapshot) => {
snapshot.forEach((doc) => {
const dataRetrieve = doc.val();
this.$data.entries.push({
ID: dataRetrieve.ID,
RFID: dataRetrieve.RFID,
'Age Years': dataRetrieve['Age Years']
Weight: dataRetrieve.Weight,
Length_History: dataRetrieve['Length_History'],
Length: dataRetrieve.Length,
Tag: dataRetrieve.Tag,
Head: dataRetrieve.Head,
Segment: dataRetrieve.Segment,
});
});
return this.$data.entries;
}).catch((error) => {
console.log('error getting documents', error);
});
},
},
mounted() {
this.readAnimals();
},
};
</script>
<template v-slot:item.Image="{ item }">
<img :src="item.Image" style="width: 50px; height: 50px" />
</template>
You should bind it this way I think.
This seems to do the trick
<template v-slot:item.Segment[0]="{item}">
<img :src="item.Segment[0]" style="width: 50px; height: 50px" />
</template>
https://imgur.com/a/LX7JKoy
I'm having an issue with my small application. I currently have a job page, I'm able to post and get the jobs.
On the frontend I have a button to press Delete but when I do it keeps giving me this error - DELETE FROM 'jobs' WHERE 'id' = 'undefined'.
Currently, technologies being used ar MySQL, sequelize, node.js, express, and vue.js.
Console
<div>
<h2 class="mb-4 font-weight-light">Job postings</h2>
<div class="d-flex align-items-center justify-content-between">
<b-input-group class="w-30">
<b-form-input v-model="filter" placeholder="Type to Search" />
<b-input-group-append>
<b-btn :disabled="!filter" #click="filter = ''">Clear</b-btn>
</b-input-group-append>
</b-input-group>
<b-button variant="primary" class="d-flex align-items-center" v-b-modal.addJob><i class="material-icons mr-1"></i> Add job</b-button>
</div>
<b-table responsive hover :items="jobs" :fields="fields" :filter="filter" no-sort-reset sort-by="postedOn" :sort-desc="true" class="mt-3 f6">
<template slot="job_postingURL" slot-scope="data">
<a :href="`${data.value}`" target="_blank">{{ data.value }}</a>
</template>
<template slot="Remove" scope="jobs">
<b-btn variant="danger" #click="deleteJob(jobs.ID)"> Delete </b-btn>
</template>
</b-table>
<add-job></add-job>
</div>
</template>
<script>
import AddJob from '#/components/jobs/AddJob'
import JobService from '../../services/JobService'
import axios from 'axios'
export default {
components: {
AddJob
},
data () {
return {
fields: [
{ Key: 'ID', label: 'Job ID', sortable: false},
{ key: 'job_title', label: 'Job title', sortable: true },
{ key: 'job_name', label: 'Company name', sortable: true },
{ key: 'job_location', label: 'Location', sortable: true },
{ key: 'job_postingURL', label: 'Job posting link', sortable: false },
{ key: 'job_postingOn', label: 'Posted on', sortable: true, tdClass: 'text-right' },
{ key: 'job_postingBy', label: 'Posted by', sortable: true },
{ key: 'Remove', sortable: true }
],
filter: null,
jobs: [
{
ID: '',
job_title: '',
job_name: '',
job_location: '',
job_postingURL: '',
job_postingOn: '',
job_postingBy: ''
},
],
}
},
// this method is to get the data from database
async created () {
try {
this.jobs = await JobService.getJobs();
} catch(err) {
this.error = err.message;
}
},
methods: {
deleteJob (ID) {
axios.delete(`http://localhost:5000/api/jobs/${this.ID}`)
.then((res) => {
this.ID = ''
this.job_title = ''
this.job_name = ''
this.job_location = ''
this.job_postingURL =''
this.job_postingOn = ''
this.job_postingBy = ''
console.log(res)
})
.catch((err) => {
console.log(err)
})
}
}
}
</script>
Since you have jobs like a data object property you couldn't use jobs as slot-scope value, try something like row, in this case row object contains some properties like item which contain data about the current item shown in this row, so you should do :
<template slot="Remove" slot-scope="row">
<b-btn variant="danger" #click="deleteJob(row.item.ID)"> Delete </b-btn>
</template>
and in your method :
deleteJob (ID) {
axios.delete('http://localhost:5000/api/jobs/'+ID)
.then((res) => {...
How can I filter out duplicate tags in a list, so that only one of each tag is listed?
Here's my component code:
<template>
<v-layout row wrap>
<ul>
<li v-for="post in sortByName(posts)" :key="post.key">
<v-chip
v-for="i in sortByName(post.tags)"
:key="i.key"
:color="`${i.color} lighten-3`"
label
small
>
<span class="pr-2">
{{ i.text }}
</span>
</v-chip>
</li>
</ul>
</v-layout>
</template>
<script>
import { mapState } from 'vuex'
const fb = require('../firebaseConfig.js')
export default {
data: () => ({
}),
computed: {
...mapState(['posts'])
},
methods: {
// function to put the tags in the right order to a -> z
sortByName (list) {
return _.orderBy(list, 'text', 'asc');
}
}
}
</script>
For example, in the screenshot below, I want to filter out Beach, so that I only see Beach once in the list:
The data looks like this:
One solution is to use a computed property that returns a new list (e.g., filteredPosts) of posts with its tags array filtered. In the example below, a cache inside the computed property is used to track tags. The computed handler maps this.posts[] into a new Array and filters each entry's tags[], tracking new tags in the cache as "seen" and removing tags already "seen".
template:
<li v-for="post in filteredPosts" :key="post.key">
script:
computed: {
filteredPosts() {
const tagCache = {};
const newPosts = this.posts.map(post => {
return {
...post,
tags: post.tags.filter(tag => {
const seen = tagCache[tag.text];
tagCache[tag.text] = true;
return !seen;
})
};
});
return newPosts;
}
}
new Vue({
el: '#app',
data() {
return {
posts: [
{
key: 1,
tags: [
{ color: 'blue', text: 'Sky' },
{ color: 'green', text: 'Tree' },
{ color: 'yellow', text: 'Beach' },
],
},
{
key: 2,
tags: [
{ color: 'purple', text: 'Grape' },
{ color: 'red', text: 'Apple' },
{ color: 'orange', text: 'Orange' },
],
},
{
key: 3,
tags: [
{ color: 'blue', text: 'Blueberry' },
{ color: 'yellow', text: 'Beach' },
],
},
{
key: 4,
tags: [
{ color: 'pink', text: 'Flower' },
{ color: 'yellow', text: 'Beach' },
],
},
]
};
},
methods: {
// function to put the tags in the right order to a -> z
sortByName (list) {
return _.orderBy(list, 'text', 'asc');
},
},
computed: {
filteredPosts () {
const tagCache = {};
// map `posts` to a new array that filters
// out duplicate tags
const newPosts = this.posts.map(post => {
return {
...post,
tags: post.tags.filter(tag => {
const seen = tagCache[tag.text];
tagCache[tag.text] = true;
return !seen;
})
};
});
return newPosts;
}
}
})
#import 'https://unpkg.com/vuetify#1.1.9/dist/vuetify.min.css'
<script src="https://unpkg.com/vue#2.5.17"></script>
<script src="https://unpkg.com/lodash#4.17.10/lodash.min.js"></script>
<script src="https://unpkg.com/vuetify#1.1.9/dist/vuetify.min.js"></script>
<div id="app">
<v-app id="inspire">
<v-container fluid class="pa-0">
<v-layout row wrap>
<ul>
<li v-for="post in filteredPosts" :key="post.key">
<v-chip v-for="i in sortByName(post.tags)"
:key="i.key"
:color="`${i.color} lighten-3`"
label
small>
<span class="pr-2">
{{ i.text }}
</span>
</v-chip>
</li>
</ul>
</v-layout>
</v-container>
</v-app>
</div>