vuejs for triggering dropdown show - vue.js

I want the dropdown to be opened when ul is clicked on the menu I want, in the same way, when the 2nd dropdown is clicked, the open dropdown will be closed.
<template>
<li>
<MenuLink
:link="items"
:key="items.name"
#click.stop="clickShow()"
/>
<ul class="children" v-if="hasChild" :class="open ? 'show' : 'hidden'">
<MenuItems
v-for="subItem in items.children"
:key="subItem.name"
:items="subItem"
/>
</ul>
</li>
</template>
<script>
export default {
name: 'MenuItems',
components: {MenuLink},
props: {
items: {type: Object, required: true},
level: {
type: Number,
default: 0
}
},
data() {
return {
childrenCount: 0,
active: false,
show: null,
open: false,
};
},

This is a I found that worked for me.
Also, I used the vue composition api, which I highly recommend using.
<template>
<ul v-if="showDropdownX">
<li>This is X</li>
<li>This is X</li>
<li>This is X</li>
</ul>
<button #click="handleShowX">Toggle dropdown X</button>
<ul v-if="showDropdownY">
<li>This is Y</li>
<li>This is Y</li>
<li>This is Y</li>
</ul>
<button #click="handleShowY">Toggle Dropdown Y</button>
</template>
<script>
import { ref } from "#vue/reactivity";
export default {
setup() {
const showDropdownX = ref(false);
const showDropdownY = ref(false);
const handleShowX = () => {
showDropdownX.value = !showDropdownX.value;
showDropdownY.value = false;
};
const handleShowY = () => {
showDropdownY.value = !showDropdownY.value;
showDropdownX.value = false;
};
return {
showDropdownX,
showDropdownY,
handleShowX,
handleShowY,
};
},
};
</script>
I hope this works for you, I haven't used the exact same code which you used, but I think my example is great to understand the concept of v-if
Edit:
Now the other dropdown will also disappear when you click on one of them.

Related

Using vitest and testing-library is there a way to segregate component renders on a test by test basis?

I have a simple list component written in Vue3 that I am using to learn how to write automated test with Vitest and testing-library. However every test method seems to be rendered together, causing my getByText calls to throw the error TestingLibraryElementError: Found multiple elements with the text: foo.
This is the test I have written:
import { describe, it, expect, test } from 'vitest'
import { render, screen, fireEvent } from '#testing-library/vue'
import TmpList from '../ui/TmpList.vue'
const listItems = ['foo', 'bar']
describe('TmpList', () => {
// Test item-content slot rendering
test('renders item-content slot', () => {
const slotTemplate = `
<template v-slot:item-content="{ item }">
<div> {{ item }} </div>
</template>`;
render(TmpList, { props: { listItems }, slots: { 'item-content': slotTemplate } });
listItems.forEach(li => {
expect(screen.getByText(li)).toBeTruthy();
})
})
// Test list item interaction
test('should select item when clicked and is selectable', async () => {
const slotTemplate = `
<template v-slot:item-content="{ item }">
<div> {{ item }} </div>
</template>`;
render(TmpList, { props: { listItems, selectable: true }, slots: { 'item-content': slotTemplate } });
const firstItem = screen.getByText(listItems[0]);
await fireEvent.click(firstItem);
expect(firstItem.classList).toContain('selected-item')
})
})
The component:
<template>
<ul>
<li v-for="(item, index) in listItems" :key="`list-item-${index}`" #click="onItemClick(index)"
class="rounded mx-2" :class="{
'selected-item bg-secondary-600/20 text-secondary':
selectedIndex == index,
'hover:bg-zinc-200/30': selectable,
}">
<slot name="item-content" :item="item"></slot>
</li>
</ul>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
export interface Props {
listItems: any[];
selectable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
selectable: false,
});
const selectedIndex = ref<number>(-1);
const onItemClick = (index: number) => {
if (props.selectable) {
selectedIndex.value = index;
}
};
</script>
This is the full error I get in the terminal:
TestingLibraryElementError: Found multiple elements with the text: foo
Here are the matching elements:
Ignored nodes: comments, script, style
<div>
foo
</div>
Ignored nodes: comments, script, style
<div>
foo
</div>
(If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).
Ignored nodes: comments, script, style
<body>
<div>
<ul
data-v-96593be0=""
>
<li
class="rounded mx-2"
data-v-96593be0=""
>
<div>
foo
</div>
</li>
<li
class="rounded mx-2"
data-v-96593be0=""
>
<div>
bar
</div>
</li>
</ul>
</div>
<div>
<ul
data-v-96593be0=""
>
<li
class="rounded mx-2 hover:bg-zinc-200/30"
data-v-96593be0=""
>
<div>
foo
</div>
</li>
<li
class="rounded mx-2 hover:bg-zinc-200/30"
data-v-96593be0=""
>
<div>
bar
</div>
</li>
</ul>
</div>
</body>
❯ Object.getElementError node_modules/#testing-library/dom/dist/config.js:37:19
❯ getElementError node_modules/#testing-library/dom/dist/query-helpers.js:20:35
❯ getMultipleElementsFoundError node_modules/#testing-library/dom/dist/query-helpers.js:23:10
❯ node_modules/#testing-library/dom/dist/query-helpers.js:55:13
❯ node_modules/#testing-library/dom/dist/query-helpers.js:95:19
❯ src/components/__tests__/SUList.spec.ts:54:33
52|
53| render(TmpList, { props: { listItems, selectable: true }, slots: { 'item-content': slotTemplate } });
54| const firstItem = screen.getByText(listItems[0]);
| ^
55| await fireEvent.click(firstItem);
56| expect(firstItem.classList).toContain('selected-item')
I know I could use the getAllByText method to query multiple items, but in this test I am expecting only one element to be found. The duplication is related to the rendering in the test, not an issue with the actual component.
Am I doing something wrong when writing the tests? Is there a way to ensure that each render will be executend independetly of renders from other tests?
Every render() returns #testing-library's methods (query* /get* /find* ) scoped to the template being rendered.
In other words, they normally require a container parameter, but when returned by render, the container is already set to that particular render's DOM:
it('should select on click', async () => {
const { getByText } = render(TmpList, {
props: { listItems, selectable: true },
slots: { 'item-content': slotTemplate },
})
const firstItem = getByText(listItems[0])
expect(firstItem).not.toHaveClass('selected-item')
await fireEvent.click(firstItem)
expect(firstItem).toHaveClass('selected-item')
})
Notes:
fireEvent is no longer returning a promise in latest versions of #testing-library. If, in the version you're using, still returns a promise, keep the async - only true for #testing-library/react.
you want to get to a point where you no longer need to import screen in your test suite
If you find yourself writing the same selector or the same render parameters multiple times, it might make sense to write a renderComponent helper at the top of your test suite:
describe(`<ListItems />`, () => {
// define TmpList, listItems, slotTemplate
const defaults = {
props: { listItems, selectable: true },
slots: { 'item-content': slotTemplate },
}
const renderComponent = (overrides = {}) => {
// rendered test layout
const rtl = render(TmpList, {
...defaults,
...overrides
})
return {
...rtl,
getFirstItem: () => rtl.getByText(listItems[0]),
}
}
it('should select on click', async () => {
const { getFirstItem } = renderComponent()
expect(getFirstItem()).not.toHaveClass('selected-item')
await fireEvent.click(getFirstItem())
expect(getFirstItem()).toHaveClass('selected-item')
})
it('does something else with different props', () => {
const { getFirstItem } = renderComponent({
props: /* override defaults.props */
})
// expect(getFirstItem()).toBeOhSoSpecial('sigh...')
})
})
Note I'm spreading rtl in the returned value of renderComponent(), so all the get*/find*/query* methods are still available, for the one-off usage, not worth writing a getter for.

How do have unique variables for each dynamically created buttons/text fields?

I'm trying to create buttons and vue element inputs for each item on the page. I'm iterating through the items and rendering them with v-for and so I decided to expand on that and do it for both the rest as well. The problem i'm having is that I need to to bind textInput as well as displayTextbox to each one and i'm not sure how to achieve that.
currently all the input text in the el-inputs are bound to the same variable, and clicking to display the inputs will display them all at once.
<template>
<div class="container">
<div v-for="(item, index) in items" :key="index">
<icon #click="showTextbox"/>
<el-input v-if="displayTextbox" v-model="textInput" />
<el-button v-if="displayTextbox" type="primary" #click="confirm" />
<ItemDisplay :data-id="item.id" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
displayTextbox = false,
textInput = '',
},
methods: {
confirm() {
// todo send request here
this.displayTextbox = false;
},
showTextbox() {
this.displayTextbox = true;
}
}
}
</script>
EDIT: with the help of #kissu here's the updated and working version
<template>
<div class="container">
<div v-for="(item, index) in itemDataList" :key="itemDataList.id">
<icon #click="showTextbox(item.id)"/>
<El-Input v-if="item.displayTextbox" v-model="item.textInput" />
<El-Button v-if="item.displayTextbox" type="primary" #click="confirm(item.id)" />
<ItemDisplay :data-id="item.item.uuid" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
itemDataList = [],
},
methods: {
confirm(id) {
const selected = this.itemDataList.find(
(item) => item.id === id,
)
selected.displayTextbox = false;
console.log(selected.textInput);
// todo send request here
},
showTextbox(id) {
this.itemDataList.find(
(item) => item.id === id,
).displayTextbox = true;
},
populateItemData() {
this.items.forEach((item, index) => {
this.itemDataList.push({
id: item.uuid + index,
displayTextbox: false,
textInput: '',
item: item,
});
});
}
},
created() {
// items prop is obtained from parent component vuex
// generate itemDataList before DOM is rendered so we can render it correctly
this.populateItemData();
},
}
</script>
[assuming you're using Vue2]
If you want to interact with multiple displayTextbox + textInput state, you will need to have an array of objects with a specific key tied to each one of them like in this example.
As of right now, you do have only 1 state for them all, meaning that as you can see: you can toggle it for all or none only.
You'll need to refactor it with an object as in my above example to allow a case-per-case iteration on each state individually.
PS: :key="index" is not a valid solution, you should never use the index of a v-for as explained here.
PS2: please follow the conventions in terms of component naming in your template.
Also, I'm not sure how deep you were planning to go with your components since we don't know the internals of <ItemDisplay :data-id="item.id" />.
But if you also want to manage the labels for each of your inputs, you can do that with nanoid, that way you will be able to have unique UUIDs for each one of your inputs, quite useful.
Use an array to store the values, like this:
<template>
<div v-for="(item, index) in items" :key="index">
<el-input v-model="textInputs[index]" />
</div>
<template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
},
data() {
textInputs: []
}
}
</script>

how to send click button's value to a props

In my parent component I have a button and child component:
<div> #click="editMe(edit)" />
<edit-team :openModal="openModal"/>
</div>
export default {
data() {
return {
openModal: false,
};
},
method: {
editMe(edit) {
this.openModal = true;
},
}
}
So after I click the editMe button, I am expecting openModal becomes true and goes true to child component.
<template>
<el-dialog
:visible.sync="modal"
/>
</template>
<script>
export default {
props: {
openModal: {
type: Boolean,
required: true,
},
},
data() {
return {
modal: this.openModal,
};
},
</script>
But unfortunately, the modal is not opening because props comes as false always. I assigned openModal to new variable because it was giving me a warning about mutating props. So how do you think I can send the props in right value?
In child just try this
<template>
<el-dialog
:visible.sync="openModal"
/>
</template>
<script>
export default {
props: {
openModal: {
type: Boolean,
required: true,
},
},
},
</script>
If you have sibilngs components and you need to retrieve data, you can use the emit keyword and emit events
Then it will work like this :
The sibling emit the event to show the modal
The parent update the showModalData to true
This child is re-rendered
Vue.component('modal', {
template: '#modal',
props: ['show'],
});
Vue.component('buttonModal', {
template: '#buttonModal',
methods: {
showModal(){
this.$emit('show-modal-button', true)
}
}
});
new Vue({
el: '#app',
data: () => ({
showModalData: false
}),
methods: {
editMe() {
this.showModalData = true
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<p>this is the parent</p>
<hr>
<button-modal #show-modal-button="editMe">Show modal</button-modal>
<hr>
<modal :show="showModalData" />
</div>
<template id="modal">
<div>
props show : {{ show }}
<h2 v-if="show">This is the modal</h2>
</div>
</template>
<template id="buttonModal">
<div>
<p>This is the sibilng</p>
<button #click="showModal"> Show the modal through sibiling components </button>
</div>
</template>

How can I get a specifc selection in select vue.js?

How are you?
I'm studying Vue and I'm stuck on the current task not knowing where to go.
I have a select that when I click I need to show on screen only what corresponds to that selection. For example, when placing the "to do" option in the select, only the tasks with a concluded=false should appear on the screen. I've only gotten this far and I need help to continue. Can you help me? Thanks
This is my App.vue
<template>
<div id="app">
<h1>Lista de Tarefas</h1>
<List :data="list" #remove="handleRemove"/>
<Form #add="addNewTask" #onChange="handleN"/>
</div>
</template>
<script>
import List from "./components/List.vue";
import Form from "./components/Form.vue";
export default {
components: {
List,
Form,
},
data() {
return {
list: [],
};
},
methods: {
addNewTask(newTask) {
this.list.push(newTask);
},
handleRemove(item) {
const index = this.list.findIndex(i => i.id === item.id)
this.list[index].excluded = true
},
handleN(item) {
const index = this.list.findIndex(i => i.id === item.id)
this.list[index].concluded = true
}
},
};
</script>
This is my List.vue
<template>
<ul>
<select v-model="selected" #change="onChange($event)">
<option disabled value="">Escolha a visualização</option>
<option v-for="option in options" :key="option.text">
{{ option.text }}
</option>
</select>
<li v-for="item in itens" :key="item.id">
<input type="checkbox" id="checkbox" v-model="item.concluded" />
<label for="checkbox"> {{ item.description }} </label>
<button #click="() => $emit('remove', item)">Excluir</button>
</li>
</ul>
</template>
<script>
export default {
props: {
data: {
type: Array,
default: () => {},
},
},
data() {
return {
selected: "",
options: [
{ text: "Todos", value: "1" },
{ text: "A fazer", value: "2" },
{ text: "Concluído", value: "3" },
{ text: "Deletado", value: "4" },
],
};
},
computed: {
itens() {
return this.data.filter((item) => item.excluded === false);
},
},
methods: {
onChange(event) {
console.log(event.target.value);
return this.data.filter((item) => item.concluded === false);
},
},
};
</script>
This is my Form.vue
<template>
<form #submit.prevent="handleNewTask">
<input type="text" v-model="newTask" placeholder="Insira a tarefa"/>
<input type="submit" value="Adicionar"/>
</form>
</template>
<script>
import Task from '../types/Task.js'
export default {
data() {
return {
newTask: "",
};
},
methods: {
handleNewTask() {
this.$emit('add', new Task(this.newTask))
this.newTask = ''
}
},
};
</script>
And this is my Task.js
export default class {
constructor(description) {
this.description = description,
this.id = Math.random(),
this.concluded = false,
this.excluded = false
}
}
I watch some tutorials, read the documentation and some StackOverflow questions but I really can't get out of here
Thanks in advance for the help
Based on how you have structured your app, our only concern should be with the List.vue file.
Your goal is to filter the results based on the selection (selected property). However, your issue is that you are not even using that anywhere.
I know you are hard coding the filter on the onChange method but that is, first of all wrong because you aren't really changing anything (you are returning an array), and secondly it's inefficient.
A better way to do it is to update the computed itens function like so:
itens() {
return this.data.filter((item) => {
if (this.selected === '1'){
return item.concluded === false
} else if (this.selected === '2'){
// filter another way
} else if (... // so on and so forth
});
},
Also, I would filter out the excluded items before sending them to the component. If you aren't going to use it, don't send it.
Remove the onChange event on the <select> and the associated method since they are now unused.

How can i add a confirmation Pop up modal with Vue Draggable?

I have an vue component which uses Vue Draggable .
<template>
<div class="row my-5">
<div v-for="column in columns" :key="column.title" class="col">
<p class="font-weight-bold text-uppercase">{{column.title}}</p>
<!-- Draggable component comes from vuedraggable. It provides drag & drop functionality -->
<draggable :list="column.tasks" :animation="200" ghost-class="ghost-card" group="tasks" :move="checkMove">
<transition-group>
<task-card
v-for="(task) in column.tasks"
:key="task.id"
:task="task"
class="mt-3 cursor-move"
></task-card>
<!-- </transition-group> -->
</transition-group>
</draggable>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
import TaskCard from "../board/TaskCard";
export default {
name: "App",
components: {
TaskCard,
draggable,
},
data() {
return {
columns: [
.....
],
};
},
methods: {
checkMove: function(evt){
console.log('moved');
}
},
};
</script>
In TaskCard Component -
<template>
<div class="bg-white shadow rounded p-3 border border-white">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>{{task.id}}</h2>
<span>{{task.date}}</span>
</div>
<p class="font-weight-bold">{{task.title}}</p>
</div>
</template>
<script>
export default {
props: {
task: {
type: Object,
default: () => ({}),
},
},
};
</script>
When I move an item, I want a modal that confirms the change and only then move the item.
(ie. if I click on the cancel button inside the modal, the item should not be moved.)
How can this be achieved using the checkMove() function provided?
I don't think you can achieve this by using onMove event. The onEnd event it seems more suitable but unfortunately it doesn't have any cancel drop functionality.
So I think the only solution here is revert it back if the user decides to cancel.
You can listen on change event (See more in documentation)
<draggable
group="tasks"
v-model="column.tasks"
#change="handleChange($event, column.tasks)">
...
</draggable>
...
<button #click="revertChanges">Cancel</button>
<button #click="clearChanges">Yes</button>
And
...
handleChange(event, list) {
this.changes.push({ event, list })
this.modal = true
},
clearChanges() {
this.changes = []
this.modal = false
},
revertChanges() {
this.changes.forEach(({ event, list }) => {
if (event.added) {
let { newIndex } = event.added
list.splice(newIndex, 1)
}
if (event.removed) {
let { oldIndex, element } = event.removed
list.splice(oldIndex, 0, element)
}
if (event.moved) {
let { newIndex, oldIndex, element } = event.moved
list[newIndex] = list[oldIndex]
list[oldIndex] = element
}
})
this.changes = []
this.modal = false
}
...
JSFiddle example