Filter out duplicate data in Vue - vue.js

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>

Related

Vue: Update part of an html string from data v-for loop with v-model input

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).

Ant Design Vue3 Editable Table Value Does Not Change

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]

Vue Element UI - html inside <el-select>

I would like to implement a select element with different colored-labels for each entry:
My code looks like this:
var Main = {
data() {
return {
selectedState: null,
processStates: [
{
value: 0,
label: 'New',
color: 'ffffff'
},
{
value: 1,
label: 'Ready',
color: 'ff9933'
},
{
value: 2,
label: 'Running',
color: '008000'
},
{
value: 3,
label: 'Rejected',
color: 'cc0000'
},
{
value: 4,
label: 'Terminated',
color: '2E9AFE'
}
]
}
},
methods: {}
}
var Ctor = Vue.extend(Main);
new Ctor().$mount('#app');
#import url("//unpkg.com/element-ui#2.4.4/lib/theme-chalk/index.css");
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui#2.4.11/lib/index.js"></script>
<div id="app">
<el-select v-model="selectedState" style="width:200px">
<el-option
v-for="state in processStates"
:key="state.value"
:label="state.label"
:value="state.value"
>
<span>
<el-tag :style="'background-color:#' + state.color"> </el-tag> {{ state.label }}
</span>
</el-option>
</el-select>
</div>
AS you can see, I managed to inject html into the option tag and have the desired result.
However, I would like to have the same html when one option is selected.
Desired result:
Any idea how can I achieve it?
You have to use the prefix slot for this. As done below, also I changed the selectedState to an object, but you can also still use the string value, but then you have to do a lookup to get the color
var Main = {
data() {
return {
selectedState: { color: 'ffffff'},
processStates: [
{
value: 0,
label: 'New',
color: 'ffffff'
},
{
value: 1,
label: 'Ready',
color: 'ff9933'
},
{
value: 2,
label: 'Running',
color: '008000'
},
{
value: 3,
label: 'Rejected',
color: 'cc0000'
},
{
value: 4,
label: 'Terminated',
color: '2E9AFE'
}
]
}
},
methods: {}
}
var Ctor = Vue.extend(Main);
new Ctor().$mount('#app');
#import url("//unpkg.com/element-ui#2.4.4/lib/theme-chalk/index.css");
.el-input--prefix .el-input__inner {
padding-left: 40px;
}
<script src="//unpkg.com/vue/dist/vue.js"></script>
<script src="//unpkg.com/element-ui#2.4.11/lib/index.js"></script>
<div id="app">
<el-select v-model="selectedState" value-key="value" style="width:200px">
<template slot="prefix">
<el-tag class="prefix" :style="`background-color: #${selectedState.color}`"/>
</template>
<el-option
v-for="state in processStates"
:key="state.value"
:label="state.label"
:value="state"
>
<span>
<el-tag :style="'background-color:#' + state.color"> </el-tag> {{ state.label }}
</span>
</el-option>
</el-select>
</div>

Don't show selected option in vue-select

I have an array "option" with some element inside. And have vue-select feature. I want to not to show selected option in all options list.
So, I want to delete "RU" option form that list if "RU" is selected. Are there any decisions?
My component file:
v-select:
<v-select :options="options" label="title" class="select" v-model="selectedLang">
<template slot="option" slot-scope="option">
<img class="language-flag" :src="option.img" /> {{ option.title }}
</template>
<template slot="selected-option" slot-scope="option">
<img class="language-flag" :src="option.img" /> {{ option.title }}
</template>
</v-select>
script part:
export default {
data() {
return {
options: [{
title: 'RU',
img: require('../../assets/icons/flags/RU.svg'),
},
{
title: 'KZ',
img: require('../../assets/icons/flags/KZ.svg')
},
],
selectedLang: null,
}
},
mounted() {
this.selectedLang = this.options[0];
}
}
You can use computed:
computed: {
items () {
return this.options.filter(i => i.title !== this.selectedLang?.title)
}
}
and then use these "items" as options in select
<v-select :options="items" label="title" class="select" v-
model="selectedLang">
If you're looking multi-select, you can use the following,
<v-select multiple :options="getOptions" ... />
{{ selectedLang }} // Prints selected options
{
data: {
selectedLang: [],
options: [
{ title: 'RU', img: require(...) },
{ title: 'KZ', img: require(...) }
]
},
computed: {
getOptions() {
return this.options.filter(option => !this.selectedLang.find(o => o.title === option.title))
}
}
}

Image not showing in Vuetify data table slot

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