Vuetify v-data-table click on row - vue.js

I'm currently working on a vue project using vuetify as our main component-library. We display some information with the v-data-table component and are redirecting to another view if a row is clicked. This works totally fine.
In user testing an unexpected behaviour occured. If the user tries to highlight the value of a column e.g. to copy the value, he is redirected as if the whole row is selected.
<template>
<v-data-table
:headers="columns"
:items="filteredPlannings"
:item-class="setDeactivationClass"
:items-per-page="itemsPerPage"
:custom-filter="searchFunction"
multi-sort
:loading="isFindPending"
:search="search"
loading-text="Loading... Please wait"
hide-default-footer
:page.sync="page"
#page-count="pageCount = $event"
#click:row="handleRowClick"
#pagination="pagination = $event"
>
</v-data-table>
</template>
<script>
export default {
setup(props, context) {
const {$router} = context.root;
const handleRowClick = ({ id }) =>
$router.push({ name: "ProjectDetails", params: { id } });
return {
handleRowClick,
}
}
}
</script>

You can manipulate with native window.getSelection() method to avoid this. Just prevent your router.push event emit when there's something in selection:
<v-data-table
...
#click:row="handleRowClick"
></v-data-table>
...
data () {
return {
...
lastSelectedItem: null,
}
},
methods: {
handleRowClick(item) {
if (window.getSelection().toString().length === 0) {
this.lastSelectedItem = item.name; //Use router.push here
}
}
}
Test this at CodePen.
But personally I don't think it's a good UX to use #click:row in your case. Possibly you should use #dblclick:row event, or create a special "Actions" column with a "Link to..." button.

Related

How to append #click navigateTo() event to v-list items props

I am using Nuxt3 with Vuetify3, and I am having trouble getting this to work:
<template>
<v-navigation-drawer
v-model="menu"
class="pa-4"
temporary
>
<v-list
:items="items"
nav
density="compact"
/>
</v-navigation-drawer>
</template>
<script setup lang="ts">
const menu = ref(false);
const items = [
{
title: 'Dashboard',
props: {
'#click': () => {
navigateTo('./dashboard');
},
prependIcon: 'mdi-view-dashboard-variant-outline'
}
},
{
title: 'Orders',
props: {
to: './orders',
prependIcon: 'mdi-package-variant'
}
}
];
</script>
The Orders list-item works as expected, but it is causing other bugs in my app with Nuxt, so I would like to use Nuxt's navigateTo() function. The Dashboard link doesn't have the click event attached.
I have also tried '#click': navigateTo('./dashboard') but this creates an endless navigation loop in Nuxt. I also tried 'v-on:click' and click () {}, but I can't seem to get it to append the click event to the v-list-items.
Obviously I could rewrite this without the shorthand <v-list :items="items" />, but I'm sure there must be a way to do this shorthand.

Limiting the two way binding with vuejs / variable values change when modifying another variable

I have a 'client-display' component containing a list of clients that I get in my store via mapGetter. I use 'v-for' over the list to display all of them in vuetify 'v-expansion-panels', thus one client = one panel. In the header of those panels, I have a 'edit-delete' component with the client passed to as a prop. This 'edit-delete' basically just emits 'edit' or 'delete' events when clicked on the corresponding icon with the client for payload. When I click on the edit icon, the edit event is then catched in my 'client-display' so I can assign the client to a variable called 'client' (sorry I know it's confusing a bit). I pass this variable to my dialog as a prop and I use this dialog to edit the client.
So the probleme is : When I edit a client, it does edit properly, but if I click on 'cancel', I find no way to revert what happened in the UI. I tried keeping an object with the old values and reset it on a cancel event, but no matter what happens, even the reference values that I try to keep in the object change, and this is what is the most surprising to me. I tried many things for this, such as initiating a new object and assigning the values manually or using Object.assign(). I tried a lot of different ways to 'unbind' all of this, nothing worked out. I'd like to be able to wait for the changes to be commited in the store before it's visible in the UI, or to be able to have a reference object to reset the values on a 'cancel' event.
Here are the relevant parts of the code (I stripped a lot of stuff to try and make it easier to read, but I think everything needed is there):
Client module for my store
I think this part works fine because I get the clients properly, though maybe something is binded and it should not
const state = {
clients: null,
};
const getters = {
[types.CLIENTS] : state => {
return state.clients;
},
};
const mutations = {
[types.MUTATE_LOAD]: (state, clients) => {
state.clients = clients;
},
};
const actions = {
[types.FETCH]: ({commit}) => {
clientsCollection.get()
.then((querySnapshot) => {
let clients = querySnapshot.docs.map(doc => doc.data());
commit(types.MUTATE_LOAD, clients)
}).catch((e) => {
//...
});
},
}
export default {
state,
getters,
mutations,
...
}
ClientsDisplay component
<template>
<div>
<div>
<v-expansion-panels>
<v-expansion-panel
v-for="c in clientsDisplayed"
:key="c.name"
>
<v-expansion-panel-header>
<div>
<h2>{{ c.name }}</h2>
<edit-delete
:element="c"
#edit="handleEdit"
#delete="handleDelete"
/>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
//the client holder displays the client's info
<client-holder
:client="c"
/>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
<client-add-dialog
v-model="clientPopup"
:client="client"
#cancelEdit="handleCancel"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import * as clientsTypes from '../../../../store/modules/clients/types';
import ClientDialog from './ClientDialog';
import EditDelete from '../../EditDelete';
import ClientHolder from './ClientHolder';
import icons from '../../../../constants/icons';
export default {
name: 'ClientsDisplay',
components: {
ClientHolder,
ClientAddDialog,
EditDelete,
},
data() {
return {
icons,
clientPopup: false,
selectedClient: null,
client: null,
vueInstance: this,
}
},
created() {
this.fetchClients();
},
methods: {
...mapGetters({
'stateClients': clientsTypes.CLIENTS,
}),
...mapActions({
//this loads my clients in my state for the first time if needed
'fetchClients': clientsTypes.FETCH,
}),
handleEdit(client) {
this.client = client;
this.clientPopup = true;
},
handleCancel(payload) {
//payload.uneditedClient, as defined in the dialog, has been applied the changes
},
},
computed: {
isMobile,
clientsDisplayed() {
return this.stateClients();
},
}
}
</script>
EditDelete component
<template>
<div>
<v-icon
#click.stop="$emit('edit', element)"
>edit</v-icon>
<v-icon
#click.stop="$emit('delete', element)"
>delete</v-icon>
</div>
</template>
<script>
export default {
name: 'EditDelete',
props: ['element']
}
</script>
ClientDialog component
Something to note here : the headerTitle stays the same, even though the client name changes.
<template>
<v-dialog
v-model="value"
>
<v-card>
<v-card-title
primary-title
>
{{ headerTitle }}
</v-card-title>
<v-form
ref="form"
>
<v-text-field
label="Client name"
v-model="clientName"
/>
<address-fields
v-model="clientAddress"
/>
</v-form>
<v-card-actions>
<v-btn
#click="handleCancel"
text
>Annuler</v-btn>
<v-btn
text
#click="submit"
>Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import AddressFields from '../../AddressFields';
export default {
name: 'ClientDialog',
props: ['value', 'client'],
components: {
AddressFields,
},
data() {
return {
colors,
clientName: '',
clientAddress: { province: 'QC', country: 'Canada' },
clientNote: '',
uneditedClient: {},
}
},
methods: {
closeDialog() {
this.$emit('input', false);
},
handleCancel() {
this.$emit('cancelEdit', { uneditedClient: this.uneditedClient, editedClient: this.client})
this.closeDialog();
},
},
computed: {
headerTitle() {
return this.client.name
}
},
watch: {
value: function(val) {
// I watch there so I can reset the client whenever I open de dialog
if(val) {
// Here I try to keep an object with the value of this.client before I edit it
// but it doesn't seem to work as I intend
Object.assign(this.uneditedClient, this.client);
this.clientName = this.client.name;
this.clientContacts = this.client.contacts;
this.clientAddress = this.client.address;
this.clientNote = '';
}
}
}
}
</script>
To keep an independent copy of the data, you'll want to perform a deep copy of the object using something like klona. Using Object.assign is a shallow copy and doesn't protect against reference value changes.

how do I pass the value from child to parent with this.$emit

What I trying to achieve here is to pass the const randomNumber inside the child component [src/components/VueForm/FormQuestion.vue] that need to be passed to parent component [src/App.vue]. Therefore I use $emit to pass the date, but since this is my first time working with $emit, I am not really sure how to do that. Could someone help me with this.
In order to run this app, I would add a working code snippet. Click on the start button and fill in the input fields. When the input field validates correctly it will pop up the button and if the user clicks on that is should pass the data to the parent. At the end it should be stored inside the App.vue in localStorage, so therefore I want to receive the randomNumber from that child component.
working code snippet here
// child component
<template>
<div class="vue-form__question">
<span class="question" :class="{ big: !shouldShowNumber }"> {{ getRandomNumber() }} </span>
</div>
</template>
<script>
export default {
methods: {
getRandomNumber() {
const randomNumber = Math.floor((Math.random() * 3) + 1);
const question = this.question.question;
this.$emit('get-random-number', question[randomNumber]);
return question[randomNumber];
}
}
};
// parent component
<template>
<div id="app">
<vue-form
:data="formData"
#complete="complete"
#getRandomNumber="newRandomNumber"
></vue-form>
</div>
</template>
<script>
import VueForm from "#/components/VueForm";
import data from "#/data/demo";
export default {
data() {
return {
formData: data
}
},
components: {
VueForm
},
created() {
this.complete()
},
methods: {
complete(data) {
// Send to database here
// localStorage.setItem('questions', data.map(d => d.question[this.randomNumber] + ': ' + d.answer));
},
}
};
</script>
v-on:get-random-number (or the superior short-hand syntax: #get-random-number). Just like you'd listen to any other event, such as #click or #mouseenter.
Though I don't know off the top of my head if dashes are valid in event names. Might have to camelcase it.

Caret position in editable div after prop change using Vue.js

I'm using Vue.js and have the following code.
When I type in div and this.content is updated, the caret is always reset to the beginning.
<template>
<div>
<div contenteditable="true"
v-html="content"
#input="onContentChange($event)">
</div>
</div>
</template>
<script>
export default {
props: ['content'],
methods: {
onContentChange: function(e) {
this.content = e.target.innerHTML;
},
},
}
</script>
<style>
</style>
How can I preserve the caret's position and update the content?
I've seen some other similar posts, but the solutions there either are not for Vue.js, or don't work in my case, or I might have failed to apply them correctly.
I've tested a few scenarios and I think what you actually need is plainly the Create a reusable editable component in this post.
However, if you want to have everything in one component, in chrome the following code works:
<template>
<div
ref="editable"
contenteditable
#input="onInput"
>
</div>
</template>
<script>
export default {
data () {
return {
content: 'hello world'
}
},
mounted () {
this.$refs.editable.innerText = this.content
},
methods: {
onInput (e) {
this.content = e.target.innerText
}
}
}
</script>
Note that the Vue plugin in Chrome doesn't seem to update correctly the value of content in this scenario, therefore you have to click on refresh on the top right of the vue plugin.
First we preserve current click on the contenteditable, then change HTML content, and set new selection.
const range = document.getSelection().getRangeAt(0)
const pos = range.endOffset
this.$el.innerHTML = this.content
const newRange = document.createRange()
const selection = window.getSelection()
const node = this.$el.childNodes[0]
newRange.setStart(node, node && pos > node.length ? 0 : pos)
newRange.collapse(true)
selection.removeAllRanges()
selection.addRange(newRange)

vuetify: programmatically showing dialog

vuetify says: If you want to programmatically open or close the dialog, you can do so by using v-model with a boolean value.
However I am quite unclear on what this means. Saying "using v-model" is vague at best. The parent component knows on setup if it should open but I am unclear on how to dynamically change this in the child. Am i supposed to pass it using v-bind?
<login v-bind:showDialog></login>
If so how does the child component deal with this?
Vuetify Dialog info here: https://vuetifyjs.com/components/dialogs
As I understand you have a child component which have a dialog within it. Not sure that this is 100% right, but this is how I implement it. Child component with dialog:
<template>
<v-dialog v-model="intDialogVisible">
...
</template>
<script>
...
export default {
props: {
dialogVisible: Boolean,
...
},
computed: {
intDialogVisible: {
get: function () {
if (this.dialogVisible) {
// Some dialog initialization code could be placed here
// because it is called only when this.dialogVisible changes
}
return this.dialogVisible
},
set: function (value) {
if (!value) {
this.$emit('close', some_payload)
}
}
}
in parent component we use it:
<my-dilaog :dialogVisible="myDialogVisible"
#close="myDialogClose">
</my-dialog>
data () {
return {
myDialogVisible: false
}
},
methods: {
myDialogClose () {
this.myDialogVisible = false
// other code
}
}
Дмитрий Алферьев answer's is correct but get "Avoid mutating a prop directly" warning, because when close dialog, v-dialog try change v-model to false, while we passed props to v-model and props value won't change. to prevent the warning we should use :value , #input
<template>
<v-dialog :value="dialog" #input="$emit('update:dialog',false)" #keydown.esc="closeDialog()" >
...
</v-dialog>
</template>
<script>
export default {
props: {
dialog: Boolean
},
methods: {
closeDialog(){
this.$emit('closeDialog');
}
}
In parent
<template>
<v-btn color="primary" #click="showDialog=true"></v-btn>
<keep-alive>
<my-dialog
:dialog.sync="showEdit"
#closeDialog="closeDialog"
>
</my-dialog>
</keep-alive>
</template>
<script>
data(){
return {
showEdit:false,
},
},
methods: {
closeDialog(){
this.showEdit = false;
},
}
v-model is a directive. You would use v-model, not v-bind.
The page you link has several examples. If you click on the <> button on the first one, it shows HTML source of
<v-dialog v-model="dialog">
v-model makes a two-way binding on a prop that is named value inside the component. When you set the bound variable's value to true, the dialog will display; when false, it will hide. Also, if the dialog is dismissed, it will set the variable's value to false.