displaying highlighted list items in vuetify 2.x - vue.js

I'm writing my first app in vuetify 2.x. The app is displaying a list of items which are objects with a text (item.t) string field and a checked (item.c) boolean field.
I would like to display checked items in the current theme color and the unchecked items in the opposite theme color (highlighted). It thus depends on the value of the item.c field value.
I assume that changing the them of the list item will kind of reverse the colors of its content. Black <-> white.
How could I do that ?
This is my list component:
<template>
<v-list dense>
<template v-for="(item, index) in items">
<v-list-item :key="item.r">
<v-list-item-content class="font-weight-medium">
<v-layout>
<v-row align="center">
<v-col cols="2">
<v-row no-gutters justify="end">
{{ item.n }}
</v-row>
</v-col>
<v-col cols="10" class="px-0">
<v-row no-gutters>{{ item.t }}</v-row>
</v-col>
</v-row>
</v-layout>
</v-list-item-content>
</v-list-item>
<v-divider
v-if="index < items.length - 1"
:key="`divider-${index}`"
></v-divider>
</template>
</v-list>
</template>
<script>
export default {
name: "itemList",
computed: {
items() {
return this.$store.getters.currentListItems;
},
},
};
</script>
I tried many things without success and couldn't find an example how to do that.
Edit: since the items contains just text and no icons, maybe it's enough the change the background and text color. The nice thing of theme is that it also reverse icons.

You can do it by different ways by v-menu by v-select or v-combo-box
and here you can use option multiple
https://vuetifyjs.com/en/components/combobox/#dense
but i think you need to use combo-box
and here you have slots
freedom of thought

I finally solved it. It's a bit hacky but it does the job.
The solution is to use a style computed with a method receiving the item object as argument. The hacky part is the way I change the style.
The highlighted text and background colors swap with the dark or light setting. Switching the them switches all the colors.
<template>
<v-list dense>
<template v-for="(item, index) in items">
<v-list-item :key="item.r" v-bind:style="highlighted(item)">
<v-list-item-content class="font-weight-medium">
<v-layout>
<v-row align="center">
<v-col cols="2">
<v-row no-gutters justify="end">
{{ item.n }}
</v-row>
</v-col>
<v-col cols="9" class="px-0">
<v-row no-gutters>{{ item.t }}</v-row>
</v-col>
</v-row>
</v-layout>
</v-list-item-content>
</v-list-item>
<v-divider
v-if="index < items.length - 1"
:key="`divider-${index}`"
></v-divider>
</template>
</v-list>
</template>
<script>
export default {
name: "itemList",
computed: {
items() {
return this.$store.getters.currentListItems;
},
},
methods: {
highlighted(item) {
let backColor = "white";
let textColor = "#1E1E1E";
if (
(this.$vuetify.theme.dark && item.c) ||
(!this.$vuetify.theme.dark && !item.c)
) {
[textColor, backColor] = [backColor, textColor];
}
return {
"background-color": backColor,
color: textColor,
};
},
},
};
</script>

Related

Is it possible to create a grid layout in a list?

disclaimer: I'm just learning vue.js and vuetify and I'm not very familiar with javascript. I'm writing my first vue.js app.
I'm using vue 2.x and vuetify 2.x.
I need to create a scrolable list of items on which the user will be able to perform actions. For now, I'm just concerned about the layout of the list items.
The item is made of two visible information, a number and a text (like an inventory) that should be displayed in two columns. The numbers should be right aligned in their column and the text left aligned.
This is what I have so far.
<template>
<v-list dense>
<template v-for="(item, index) in items">
<v-list-item :key="item.r">
<v-list-item-content class="font-weight-bold">
<v-list-item-title>
{{ item.n }} {{ item.t }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-divider :key="index"></v-divider>
</template>
</v-list>
</template>
<script>
export default {
name: "itemList",
data() {
return {
items: this.$store.state.currentItems,
};
},
};
</script>
Is it possible to use a layout inside a list item ? If not, how could I create the desired column alignment ?
In case it could influence the implementation, the actions are
make the item a sliding button with multiple selectable actions
or make the number and the text individual clickable flat buttons with popup menus
manually reorder list items with drag and drop.
An alternative to the sliding button (if not possible) is to make the number and text flat buttons with a popup menu. I guess this could influence how to define the layout.
generally with the use of v-row and v-col you can implement a layout and this is true inside of a v-list-item-content, for example check the code below (I'm not sure if I understood the layout you've asked for correctly or not but it must give you a hint)
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: {
items: [{
t: 'title one',
n: 1
},
{
t: 'title two',
n: 2
},
{
t: 'title three',
n: 3
},
{
t: 'title four',
n: 4
},
],
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.js"></script>
<link href="https://cdn.jsdelivr.net/npm/vuetify#2.x/dist/vuetify.min.css" rel="stylesheet" />
<div id="app">
<v-app>
<v-main>
<v-container>
<v-list dense>
<template v-for="(item, index) in items">
<v-list-item :key="item.n">
<v-list-item-content class="font-weight-bold">
<v-row>
<v-col cols="6">
<v-row no-gutters justify="end">{{ item.n }}</v-row>
</v-col>
<v-col cols="6">
<v-row no-gutters>{{ item.t }}</v-row>
</v-col>
</v-row>
</v-list-item-content>
</v-list-item>
<v-divider :key="`divider-${item.n}`"></v-divider>
</template>
</v-list>
</v-container>
</v-main>
</v-app>
</div>

How do I lazy load item lists on Vuejs and Vuetify's lazyload?

Im trying to make an infinite scroll list but it's really not lazy loading, been stuck with this for hours, the whole list will come in.
.....
<v-col
v-for="(post, i) in posts"
:key="i"
cols="12"
>
<v-lazy
v-model="isActive"
:options="{
threshold: .5
}"
transition="fade-transition"
>
{{Content here}}
</....
API used for test : https://jsonplaceholder.typicode.com/posts
There is a new virtual-scroller component, but it doesn't work with responsive content like grid rows/cols. Instead use v-lazy...
I discovered that the columns need to have defined min-height (approx. to the expected height of the cards) in order for the v-lazy intersection observer to work. Use something like a v-sheet or v-responsive to set the min-height and contain the cards.
Also bind the v-model of the v-lazy to each post (ie: post.isActive), instead of a global isActive var...
<v-col lg="3" md="4" sm="6" cols="12" v-for="(post, index) in posts">
<v-sheet min-height="250" class="fill-height" color="transparent">
<v-lazy
v-model="post.isActive" :options="{
threshold: .5
}"
class="fill-height">
<v-card class="fill-height" hover>
<v-card-text>
<v-row :key="index" #click="">
<v-col sm="10" cols="12" class="text-sm-left text-center">
#{{ (index+1) }}
<h2 v-html="post.title"></h2>
<div v-html="post.body"></div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-lazy>
</v-sheet>
</v-col>
Demo: https://codeply.com/p/eOZKk873AJ
I can suggest another solution with v-intersect, which works perfect for me.
Sorry, the snippet may be not working as composed of my code, but the idea should be pretty clear
<template>
<v-list class="overflow-y-auto" max-height="500">
<v-list-item v-for="item in items">
{{ item.name }}
</v-list-item>
<v-skeleton-loader v-if="moreDataToAvailable" v-intersect="loadNextPage" type="list-item#5" />
</v-list>
</template>
<script lang="ts">
import Vue from 'vue'
const pageSize = 10
export default Vue.extend({
data(): any {
return {
pageLoaded: 0,
totalCount: 100,//fetch from API
items: []
}
},
computed: {
moreDataToAvailable (): boolean {
return Math.ceil(this.totalCount / pageSize) - 1 > this.pageLoaded
}
},
methods {
async loadNextPage (entries: IntersectionObserverEntry[]) {
if (entries[0].isIntersecting && this.moreDataToAvailable) {
const nextPage = this.pageLoaded + 1
const loaded = await this.loadPage(nextPage) //API call
loaded.data.forEach((item: any) => this.items.push(item))
this.totalCount = loaded.totalCount
this.pageLoaded = nextPage
}
},
}
})
</script>

How to avoid mutating a prop directly when all you have is a click?

How can I mutate a prop the correct way, so that I don't get the [Vue warn]: Avoid mutating a prop directly message?
I already got the v-model to work on this v-dialog. However, I also want to provide a close button in the dialog itself, which causes this mutation warning, as it's the dialog itself that's mutating the variable. How best to approach this case and solve it?
Dialog.vue:
<template>
<v-dialog
:value="value" #input="$emit('input', $event)"
scrollable
width="80vw"
:transition="false"
>
<template v-slot:activator="{ on }">
<div v-on="on" #click="$emit('open')">
<slot name="button">
<v-btn color="primary">{{ buttonText == null ? title : buttonText }}</v-btn>
</slot>
</div>
</template>
<v-card
elevation="10"
height="80vh"
>
<v-system-bar
color="light-blue darken-3"
window
>
<span>{{title}}</span>
<v-spacer></v-spacer>
<v-icon #click="value=false">mdi-close</v-icon>
</v-system-bar>
<v-card-text> <!-- required here to make the scrollable v-dialog work -->
<slot></slot>
</v-card-text>
<slot name="actions"></slot>
</v-card>
</v-dialog>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true,
},
buttonText: String,
value: Boolean,
},
}
</script>
I can use it quite nicely like:
<mydialog title="Select something" button-text="A button!" v-model="dialog" #open="loadData()">
Content goes here...
</mydialog>
The <v-icon #click="value=false">mdi-close</v-icon> is what's incorrectly mutating the "value"-variable.
Sidenote #1: The open event is there so I can populate data (loadData) from a database when the dialog is opened (vs. when its created on the DOM).
UPDATE; I can get it to work by doing:
<v-icon #click="$emit('close', $event)">mdi-close</v-icon>
and
<mydialog title="Select something" button-text="A button!" v-model="dialog" #open="loadData()" #close="dialog=false">
However, I feel this is far from being elegant. Aren't there any solutions in where I don't need to add on-Handlers to close this dialog? I almost feel that this is worse than living with the warning.. :|
Use a computed property with get and set. The dialog computed will behave exactly as a normal variable and it will eliminate the warning. Now you can use it to get the value and also to set the value.
Try this:
<template>
<v-dialog
:value="dialog"
#input="$emit('input', $event)"
scrollable
width="80vw"
:transition="false"
>
<template v-slot:activator="{ on }">
<div v-on="on" #click="dialogOpened()">
<slot name="button">
<v-btn color="primary">{{ buttonText == null ? title : buttonText }}</v-btn>
</slot>
</div>
</template>
<v-card elevation="10" height="80vh">
<v-system-bar color="light-blue darken-3" window>
<span>{{title}}</span>
<v-spacer></v-spacer>
<v-icon #click="dialog=false">mdi-close</v-icon>
</v-system-bar>
<v-card-text>
<!-- required here to make the scrollable v-dialog work -->
<slot></slot>
</v-card-text>
<slot name="actions"></slot>
</v-card>
</v-dialog>
</template>
export default {
props: ['title', 'buttonText', 'value'],
data: () => ({
dlg_close: false,
}),
computed: {
dialog: {
get() {
return this.value;
},
set(selection) {
this.$emit("input", selection);
}
}
},
methods: {
dialogOpened(newVal) {
this.dialog = newVal;
},
},
}

v-slot:badge and v-if does not work with a computed property

I'm working on a CMS project and I have an issue I can't figure out.
I Have a component where I'm showing IP's. On change I want a badge to appear, so the user knows "this field is changed".
But the thing is the badge won't show if I'm using "v-slot:badge".
In the v-if is a computed property, If I inspect the page with vue devtools ‘isStartIpValueChanged’ will be true on a change. So, it should work right?
Template
<v-list-item-content>
<v-form ref="form" v-model="valid">
<v-hover v-slot:default="{ hover }">
<v-row align-content="center" no-gutters>
<v-col>
<v-badge overlap color="red" right>
<template v-slot:badge v-if="isStartIpValueChanged">
<v-avatar color="red" size="6"></v-avatar>
</template>
<v-text-field
dense
:rules="apiIpRules"
v-model="apiIp.startIp"
#input="valueChanged()"
ref="startIp"
:class="hover ? 'hover-text-color' : ''"
placeholder="###.###.###.###">
</v-text-field>
</v-badge>
</v-col>
<v-col cols="1" class="text-center" align-self="center">
<p>-</p>
</v-col>
<v-col cols="1" class="text-center" align-self="center">
<v-btn v-show="hover" #click="deleteIp()" icon small color="red"><v-icon>mdi-minus-circle</v-icon></v-btn>
</v-col>
</v-row>
</v-hover>
</v-form>
Created and Computed (apiIp is a prop I get from the parent component)
created () {
this.apiIpsOriginalValueStartIp = this.apiIp.startIp
this.apiIpsOriginalValueEndIp = this.apiIp.endIp
this.apiIp.uuid = this.GenerateUUID()
},
computed: {
isStartIpValueChanged () {
return this.apiIp &&
(this.apiIp.startIp !== this.apiIpsOriginalValueStartIp ||
this.apiIp.uuid === null)
},
isEndIpValueChanged () {
return this.apiIp &&
(this.apiIp.endIp !== this.apiIpsOriginalValueEndIp ||
this.apiIp.uuid === null)
}
},
Anyone know what is going wrong here?
As according to Vuetify's own documentation, you should be using the v-model, directly on the v-badge, to show it only when you want it to.
<v-badge overlap color="red" right v-model="isStartIpValueChanged">
<template v-slot:badge>
<v-avatar color="red" size="6"></v-avatar>
</template>
<v-text-field
dense
:rules="apiIpRules"
v-model="apiIp.startIp"
#input="valueChanged()"
ref="startIp"
:class="hover ? 'hover-text-color' : ''"
placeholder="###.###.###.###">
</v-text-field>
</v-badge>
Doc: https://vuetifyjs.com/en/components/badges#show-on-hover

Extracting the information in a prop in a Vue child component

I'm passing a object as a prop to a child component but I can't reference one of its elements (user_id) to use in a method in that child component.
My code (slightly abbreviated) is:
<template>
<div class="back">
<v-app id="inspire">
<v-content>
<v-container fluid>
<v-card flat>
<v-card-title>
<div>
<div class="headline">
{{data.title}}
</div>
<span class="grey--text">{{data.user}} said {{data.created_at}}</span>
</div>
<v-spacer></v-spacer>
<v-badge color="deep-orange accent-3" left overlap>
<span slot="badge">7</span>
<v-icon color="grey lighten-1" large>
insert_comment
</v-icon>
</v-badge>
<!--<v-btn color="deep-orange accent-3">5 replies</v-btn>-->
</v-card-title>
<v-card-text v-html="data.body"></v-card-text>
<v-card-actions v-if="own">
<v-btn icon small>
<v-icon color="deep-orange accent-3">edit</v-icon>
</v-btn>
<v-btn icon small>
<v-icon color="red">delete</v-icon>
</v-btn>
</v-card-actions>
</v-card>
<return-button></return-button>
</v-container>
</v-content>
</v-app>
</div>
</template>
<script>
export default {
name: "ShowQuestion",
props: ['data'],
data() {
return {
own: this.Own(),
}
},
methods: {
Own: function () {
return this.UserID() == this.user_id <---HERE BE DRAGONS! (reported as 'undefined')
},
UserID: function () {
... returns the 'sub' from the JWT token
return sub;
}
},
}
</script>
While the correct information is being displayed in the template, I also need to be able to compare the user's ID from the token with that contained in the prop (i.e. data.user_id). My reading suggests the solution will involve converting the object to an array, but that's beyond my current skill level too. I'd appreciate some pointers to a solution.
Thanks,
Tom
If you can render data.user_id in your template you can use it anywhere, but I'd probably do something like this to solve your problem:
props: ['data']
data() {
return {
}
},
computed: {
UserId() {
return //however you get your id
}
},
Then your v-if could just be this:
<v-card-actions v-if="UserId === data.user_id">