Hide popup box when clicked anywhere on the document - javascript

I am trying to make a component with a list of items and when I click on each of the items, it shows me an edit popup. When I click on it again, it hides the edit popup. But I would like to also be able to click anywhere on the document and hide all edit popups (by setting edit_item_visible = false).
I tried v-on-clickaway but since I have a list of items then it would trigger multiple times. And the #click event would trigger first and then the clickaway event would trigger multiple times and hide it right after showing it. I also tried to change the component's data from outside but with no luck.
Vue.component('item-list', {
template: `
<div>
<div v-for="(item, index) in items" #click="showEdit(index)">
<div>{{ item.id }}</div>
<div>{{ item.description }}</div>
<div v-if="edit_item_visible" class="edit-item">
Edit this item here...
</div>
</div>
</div>
`,
data()
{
return {
items: [],
edit_item_visible: false,
selected: null,
};
},
methods:
{
showEdit(index)
{
this.selected = index;
this.edit_item_visible = !this.edit_item_visible;
}
},
});
const App = new Vue ({
el: '#app',
})

If you want to be able to edit multiple items at the same time, you should store the list of edited items, not global edit_item_visible flag.
showEdit(item)
{
this.selected = item;
this.editing_items.push(item);
}
// v-on-clickaway="cancelEdit(item)"
cancelEdit(item)
{
let idx = this.editing_items.indexOf(item);
this.editing_items.splice(idx, 1);
}

Related

Vue: Input Manual Autocomplete component

I have a vue-cli project, that has a component named 'AutoCompleteList.vue' that manually handled for searching experience and this component has some buttons that will be fill out the input.
It listens an array as its item list. so when this array has some items, it will be automatically shown; and when I empty this array, it will be automatically hidden.
I defined an oninput event method for my input, that fetches data from server, and fill the array. so the autocomplete list, will not be shown while the user doesn't try to enter something into the input.
I also like to hide the autocomplete list when the user blurs the input (onblur).
but there is a really big problem! when the user chooses one of items (buttons) on the autocomplete list, JS-engine first blurs the input (onblur runs) and then, tries to run onclick method in autocomplete list. but its too late, because the autocomplete list has hidden and there is nothing to do. so the input will not fill out...
here is my code:
src/views/LoginView.vue:
<template>
<InputGroup
label="Your School Name"
inputId="schoolName"
:onInput="schoolNameOnInput"
autoComplete="off"
:onFocus="onFocus"
:onBlur="onBlur"
:vModel="schoolName"
#update:vModel="newValue => schoolName = newValue"
/>
<AutoCompleteList
:items="autoCompleteItems"
:choose="autoCompleteOnChoose"
v-show="autoCompleteItems.length > 0"
:positionY="autoCompletePositionY"
:positionX="autoCompletePositionX"
/>
</template>
<script>
import InputGroup from '../components/InputGroup'
import AutoCompleteList from '../components/AutoCompleteList'
export default {
name: 'LoginView',
components: {
InputGroup,
AutoCompleteList
},
props: [],
data: () => ({
autoCompleteItems: [],
autoCompletePositionY: 0,
autoCompletePositionX: 0,
schoolName: ""
}),
methods: {
async schoolNameOnInput(e) {
const data = await (await fetch(`http://[::1]:8888/schools/${e.target.value}`)).json();
this.autoCompleteItems = data;
},
autoCompleteOnChoose(value, name) {
OO("#schoolName").val(name);
this.schoolName = name;
},
onFocus(e) {
const position = e.target.getBoundingClientRect();
this.autoCompletePositionX = innerWidth - position.right;
this.autoCompletePositionY = position.top + e.target.offsetHeight + 20;
},
onBlur(e) {
// this.autoCompleteItems = [];
// PROBLEM! =================================================================
}
}
}
</script>
src/components/AutoCompleteList.vue:
<template>
<div class="autocomplete-list" :style="'top: ' + this.positionY + 'px; right: ' + this.positionX + 'px;'">
<ul>
<li v-for="(item, index) in items" :key="index">
<button #click="choose(item.value, item.name)" type="button">{{ item.name }}</button>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'AutoCompleteList',
props: {
items: Array,
positionX: Number,
positionY: Number,
choose: Function
},
data: () => ({
})
}
</script>
src/components/InputGroup.vue:
<template>
<div class="input-group mb-3">
<label class="input-group-text" :for="inputId ?? ''">{{ label }}</label>
<input
:type="type ?? 'text'"
:class="['form-control', ltr && 'ltr']"
:id="inputId ?? ''"
#input="$event => { $emit('update:vModel', $event.target.value); onInput($event); }"
:autocomplete="autoComplete ?? 'off'"
#focus="onFocus"
#blur="onBlur"
:value="vModel"
/>
</div>
</template>
<script>
export default {
name: 'input-group',
props: {
label: String,
ltr: Boolean,
type: String,
inputId: String,
groupingId: String,
onInput: Function,
autoComplete: String,
onFocus: Function,
onBlur: Function,
vModel: String
},
emits: [
'update:vModel'
],
data: () => ({
}),
methods: {
}
}
</script>
Notes on LoginView.vue:
autoCompletePositionX and autoCompletePositionY are used to find the best position to show the autocomplete list; will be changed in onFocus method of the input (inputGroup)
OO("#schoolName").val(name) is used to change the value of the input, works like jQuery (but not exactly)
the [::1]:8888 is my server that used to fetch the search results
If there was any unclear code, ask me in the comment
I need to fix this. any idea?
Thank you, #yoduh
I got the answer.
I knew there should be some differences between when the user focus out the input normally, and when he tries to click on buttons.
the key, was the FocusEvent.relatedTarget property. It should be defined in onblur method. here is its full tutorial.
I defined a property named isFocus and I change it in onBlur method, only when I sure that the focus is not on the dropdown menu, by checking the relatedTarget

How to hide an element from a list if clicked twice without toggling in a vue nuxt application

I have got a clickable list in a Vue/Nuxt application. When one item is selected, a little tick mark appears. I would like to be able to unselect an item (the tick mark to disappear) if the item is clicked again. If I click on another item, I would like this item to be selected and the previously selected item to unselect (only one item can be selected). So far, if I try to select another item, I need to click twice because the first click will only unselect the first selected item and the second click will select the new item. Any idea ??
<template>
<div
v-for="(item, itemIndex) in list"
:key="itemIndex"
#click="onClick(itemIndex)"
>
<div>
<div v-if="activeIndex == itemIndex && selected === true">
<TickMark />
</div>
<Item />
</div>
</div>
</template>
<script>
export default {
props: {
questionModules: {
required: true,
type: Array,
},
},
data() {
return {
activeIndex: null,
selected: false,
}
},
methods: {
onClick (index) {
this.activeIndex = index
this.selected = !this.selected
},
},
}
</script>
because you don't need to change positions or sort the list - keeping the selected index is just fine, do it like this:
<template>
<section
class="items-list">
<template v-for="(item, itemIndex) in list"
:key="itemIndex" >
<TickMark v-if="activeIndex === itemIndex
#click="selectItem(itemIndex)" /> // by clicking on the mark - it will toggle the selection
<Item />
</template>
</section>
</template>
<script>
export default {
props: {
questionModules: {
required: true,
type: Array,
},
},
data() {
return {
activeIndex: null
}
},
methods: {
selectItem (index) {
this.activeIndex = index
},
},
}
</script>
I've changed the architecture of the DOM so it will be without all the un-necessary elements

vue : can't navigate to another page

i'm new to vue and i have created a dynamic page that gets questions,user name, and categories from API ,every thing is showing fine but my problem is i want to make those dynamic (user name for example) is clickable so when ever i click on it it should take me to user's profile with same name, i have tried to do a clickable div but only the url route changes not the content of the page (it's not taking me to the profile page), also as u see in my code i created a function in methods that saved my clickable user in index and i'm trying to show this user saved in index in my profile page but i didn't figure out how to do it, any help please?
hom.vue
<p>Asked by: </p>
<div id="user" v-for="(user, index) in users"
:key="index">
<div #click="selectedUser(index)">{{ user }}</div>
</div>
export default {
props: {
question1: Object
},
data() {
return {
selectedIndex: null,
};
},
watch: {
question1: {
handler() {
this.selectedIndex = null;
},
},
},
computed: {
users() {
let users = [this.question1.user];
return answers;
},
},
methods: {//here i saved the user that i clicked on in index in order to show it to the profile page
selectedUser(index) {
this.selectedIndex = index;
this.$router.push('/Profile')
},
}
}
profile.vue
<template>
<div class="container" width=800px>
<b-row id="asked-info">
<p>Welcome to the profile of: </p>
<div id="user" v-for="(user, index) in users"
:key="index">
{{ user }}
</div>
</b-row>
</div>
</template>
<script>
export default {
props: {
question1: Object
},
computed: {
users() {
let users = [this.question1.user];
// here i'm not able to figure out how will the user i saved in index (the user i clicked on) will show here
return users;
},
},
}
</script>
</script>
You can pass the index via props to route components.
Your selectedUser function would be like:
selectedUser(index) {
this.$router.push({ path: `/Profile/${index}` })
},
In your Router instance:
routes: [
{
path: '/profile/:index',
component: Profile,
props: true //to specify that you are using props
},
//your other routes
]
Then in the profile.vue, you can access the index by this syntax: this.$route.params.index
But this is just how you pass the index to the profile.vue page, eventually you need to have access to the users array which you have in the hom.vue page, which means that you are sharing data between components (or pages). I strongly suggest you to use Vuex, the Vue state management, a centralized store for your app. You can check out their documentation here: https://vuex.vuejs.org/#what-is-a-state-management-pattern

show element in v-for list: VueJS [duplicate]

This question already has an answer here:
Vue.js - Add class to clicked button
(1 answer)
Closed 3 years ago.
I have a v-for which display all my items and I have a panel for each items (to modify and delete) but when I click on this button to display my panel, it appears on all of my items. How can I avoid that ? This is the same thing when I click on modify button, the input to modify my item appears on each element.
There is my code :
<div v-for="(comment, index) in comments" :list="index" :key="comment">
<div v-on:click="show = !show">
<div v-if="show">
<button #click="edit(comment), active = !active, inactive = !inactive">
Modify
</button>
<button #click="deleteComment(comment)">
Delete
</button>
</div>
</div>
<div>
<p :class="{ active: active }">
{{ comment.content }}
</p>
<input :class="{ inactive: inactive }" type="text" v-model="comment.content" #keyup.enter="doneEdit">
</div>
</div>
And the methods & data :
data() {
return {
show: false,
editing: null,
active: true,
inactive: true
}
},
methods: {
edit(comment) {
this.editing = comment
this.oldComment = comment.content
},
doneEdit() {
this.editing = null
this.active = true
this.inactive = true
}
}
You have the same show, editing, active, inactive state for all items. So if you change some data property for one item it changed for all.
There are a lot of ways to achieve what you want.
The easiest is to manage your data by index.
For example:
<div v-on:click="showIndex = index">
<div v-if="showIndex === index">
...
data () {
return {
showIndex: null
...
The main problem with this approach - you can show/edit only one item at the time.
If you need more complicated logic and whant to manage more then one item at the time I suggest to create a separate component for your items and each will have own state (show, editing etc.)
#NaN's approach works if you want to only have one open at a time. If you want to have the possibility of having multiple open at the same time you would need to keep track of each individual element. Right now you are only basing it on show. Which can only be true/false for all elements at the same time.
So this is what you need to do:
Change show from a boolean to an array
data() {
return {
show: [],
editing: null,
active: true,
inactive: true,
}
},
Then you can keep track of which element should have the panel or not:
<div v-on:click="toggleActive(index)">
And the method:
methods: {
toggleActive(index) {
if (this.show.includes(index)) {
this.show = this.show.filter(entry => entry !== index);
return;
}
this.show.push(index);
}
}
and finally your v-if becomes:
<div v-if="show.includes(index)">

How to prevent multiple renders in Vue v-for lists

I've captured the current code I have here:
https://jsfiddle.net/prsauer/Lnhu2avp/71
The basic issue is that for each click on a list item every item's computeStyle is called -- I'd prefer for each click to only produce a single recompute of the style
<div id="editor">
<div v-for="item in dungeons" :key="item.isOpened">
<div v-on:click="clickedChest(item)" v-bind:style="computeChestStyle(item)">
{{ item.name }} {{ item.isOpened }}
</div>
</div>
</div>
var dgnData = [
{ name: "Lobby", isOpened: false },
{ name: "Side", isOpened: false },
];
new Vue({
el: '#editor',
data: { dungeons: dgnData },
computed: { },
methods: {
clickedChest: chest => {
chest.isOpened = !chest.isOpened;
console.log("##### Clicked chest", chest);
},
computeChestStyle:
item => {
console.log("computeStyle", item);
return item.isOpened ? "color: red" : "color: blue";
}
}
});
Function calls get re-evaluated on every update of the view. If you want results to be cached to only re-render as needed, you need a computed. That means you need to create a component for your item, and create the computed in the component.

Categories

Resources