What I have
A custom DropDown with a filter text input above. The DropDown can be opened independently from the filter text input.
What I want
The intended behavior would be, that the dropdown closes when the filter input loses focus and also when I click with the mouse outside of the DropDown, so that the DropDown loses the focus.
What I tried
Bind to the blur event on my root div element in the control, which doesn't fire at all.
I also couldn't find anything about internal component methods which I could override.
Code
<div #blur="onRootLostFocus">
...
</div>
...
...
...
onRootLostFocus() {
console.log('LostFocus');
this.deactivateSearchPanel();
this.deactivateSelectionPanel();
}
Solution
I missed, that a div needs tabindex="0" to be focusable, this fixed my problem
Something like this?
Answer: You need to set tabindex="0" to make it focusable.
Here an custom dropdown how you could do it:
Vue.component("dropdown", {
props: ["value"],
data(){
return {
open: false,
options: ["BMW", "Fiat", "Citroen", "Audi", "Tesla"]
}
},
methods: {
toggleDropdown() {
this.open = !this.open;
},
closeDropdown(){
this.open = false;
},
selectOption(option) {
this.$emit("input", option);
}
},
template: `<div class="dropdown">
<div #blur="closeDropdown" tabindex="0" ref="dropdown" #click="toggleDropdown" class="select">
{{ value }}
</div>
<div class="options" :style="{'max-height': open ? '300px' : '0px'}">
<div v-for="option in options" #mousedown="selectOption(option)" class="option">
{{ option }}
</div>
</div>
</div>`
})
new Vue({
el: "#app",
data: {
selectedCar: "BMW"
}
})
.dropdown {
width: 200px;
position: relative;
}
.select {
height: 40px;
position: absolute;
left: 0;
width: 100%;
background: green;
display: flex;
justify-content: center;
align-items: center;
color: white;
cursor: pointer;
}
.option {
width: 100%;
height: 40px;
background: darkgreen;
color: white;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.option:hover {
background: green;
}
.options {
overflow: hidden;
transition: max-height 200ms;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"> <p>
{{ selectedCar }}</p>
<dropdown v-model="selectedCar" />
</div>
Note there is also tabindex="-1" to make it not reachable via sequential keyboard navigation.
Also consider using a <button> instead because of accessibility concerns.
See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex
Related
I have a vuejs project where when each of the green boxes is clicked it appends data to the screen. Right now, I get undefined, but ideally it needs display the designated description for the box that is
new Vue({
el: "#app",
data: {
chocs: [
{ text: "Learn JavaScript", description: "yello" },
{ text: "Learn Vue", description: "white" },
{ text: "Play around in JSFiddle", description: "blue"},
{ text: "Build something awesome", description: "orange" }
]
},
methods: {
handleTileClick: function(){
$("#fox").append(`<div id="popover-dialog">data here: ${this.choc.description}</div>`);
}
}
})
.main-carousel {
background: #464646;
padding-block: 1rem;
}
.carousel-cell-container {
width: 24%;
}
.carousel-cell {
width: 24%;
position: relative;
cursor: pointer;
overflow: visible;
}
.carousel-cell-card {
height: 200px;
position: relative;
background: #8c8;
border-radius: 5px;
}
.carousel-cell .caption {
text-align: center;
padding-block: 0.5rem;
box-sizing: border-box;
color:red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<div id="app">
<div v-for="(choc,index) in chocs" class="carousel-cell" :key="'chocs-' + index">
<div class="img-icon">
<div class="carousel-cell-card" v-on:click="handleTileClick()"></div>
</div>
<div class="caption">{{ choc.text }}</div>
</div>
</div>
<div id="fox">
</div>
found in the vue model depending on which box has been selected. any idea why i get undefined?
Inside your event handler you are using this.choc which would be undefined because it does not exist in the vue model. You need to pass the choc object to your event handler:
<div class="carousel-cell-card" v-on:click="handleTileClick(choc)"></div>
and then use it there
handleTileClick: function(choc) {
$("#fox").append(`<div id="popover-dialog">data here: ${choc.description}</div>`);
}
you need to send the specific item that was clicked as an argument of your function
like this :
v-on:click="handleTileClick(choc)"
then in your function
handleTileClick: function(choc){
$("#fox").append(`<div id="popover-dialog">data here: ${choc.description}
</div>`);
}
I have created a component that does two things.
Feature 1: toggling a button cycles between a blue square and a red square.
Feature 2: inputting 5 or more characters in an input area reveals a green square.
<template>
<div id="app">
<div id="container1">
<h2>Container 1</h2>
<h5>Toggle Button to Change Box</h5>
<div class="box1" v-if="isBlueBoxDisplayed()">
<span>1a</span>
</div>
<div class="box2" v-else>
<span>1b</span>
</div>
<button #click="toggleBlueBox">Toggle Blue Box</button>
</div>
<div id="container2">
<h2>Container 2</h2>
<h5>Type 5 or more characters for Green Box</h5>
<input
placeholder="Type 5 characters"
v-model="userInput"
#input="validateInputForGreenBox()"
/>
<div class="box3" v-if="displayGreen">
<span>2</span>
</div>
</div>
<div id="container3">
<p>
Why does <span class="bold">isBlueBoxDisplayed()</span> method trigger
when <span class="bold">validateInput()</span> is triggered?
</p>
<p>(see console for confirmation of this)</p>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
displayBlue: false,
displayGreen: false,
userInput: "",
};
},
methods: {
toggleBlueBox() {
this.displayBlue = !this.displayBlue;
},
isBlueBoxDisplayed() {
console.log("isBlueBoxDisplayed() method activated!");
if (this.displayBlue) {
return true;
}
return false;
},
validateInputForGreenBox() {
console.log("validateInputForGreenBox() method Hit!");
if (this.userInput.length > 4) {
return (this.displayGreen = true);
}
this.displayGreen = false;
},
},
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
display: flex;
justify-content: space-between;
width: 100%;
}
#container1,
#container2 {
border-right: 2px solid grey;
}
div > span {
color: white;
}
.bold {
font-weight: 600;
}
.box1 {
width: 100px;
height: 100px;
background-color: blue;
}
.box2 {
width: 100px;
height: 100px;
background-color: red;
}
.box3 {
width: 100px;
height: 100px;
background-color: green;
}
</style>
Sandbox here:
https://codesandbox.io/s/gifted-matsumoto-lq2mj?file=/src/App.vue
The issue:
The two features have nothing to do with one another. And yet, when a user types into the input area (which triggers the validateInputForGreenBox() method, as it should) it triggers the isBlueBoxDisplayed() method (which should not be triggered).
On a large scale application that I have, this is causing some performance issues whereby multiple unrelated methods are being triggered. So why is this happening? And is there something that can be done so that unrelated methods don't trigger?
Although a simple answer, I've seen the struggle many times.
The issue here is that the functions that you pass to your events are being called immediately, almost as acting as an IIFE. Because of the existence of v-if on the page, you're causing a reflow which is triggering the methods again.
Short answer:
Don't use () when passing functions to events:
Good:
#input="validateInputForGreenBox"
Bad:
#input="validateInputForGreenBox()"
and the fiddle for proof
How can I listen for events on a disabled element? I have an input box that I have disabled but that I want to enable if the user double clicks it. I know this is possible using a label and switching the label off using CSS. I want to know if there is a way to do this without a label - is there some way to disable the input and still handle events for it? or is there a way to make the input unfocusable unless double-clicked?
You can prevent the default action of the input with a timeout. If a user clicks before the ms has elapsed, you run the desired code:
new Vue({
el: '#app',
data: {
checked: false,
timeoutId: null, // You do not need to have this defined, vue will initialize it for you, but here for clarity
},
methods: {
dblClick(event) {
// Simple click
if (!this.timeoutId) {
event.preventDefault();
event.stopPropagation();
this.timeoutId = setTimeout(() => {
this.timeoutId = null;
}, 300);
return // Do not run below code if it is a single click
}
// double click
this.timeoutId = null;
console.log('dblClick')
}
}
});
#app {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100vh;
}
h1 {
font-size: 1.5em;
margin-bottom: 5px;
}
i {
font-size: .75em;
margin: 0;
color: #969696;
}
input {
transform: scale(2);
margin: 20px 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>Checkbox is {{ checked ? 'checked' : 'not checked' }}</h1>
<i>Double-click to change</i>
<input v-model="checked" type="checkbox" #click="dblClick">
</div>
I'm trying to make a overflow with tagging, which fades out in the beginning to give the user a hint that there's more. This is what it looks like:
I put the fading gradient as a :after inside the CSS and "activate" it by Vue's style binding, when scrollWidth > offsetWidth (overflow bigger than the box itself).
But the problem is that it sometimes (lags?) behind and does not calculate the scrollWidth right, especially when I enter a long word and then delete it. It doesn't "like" that and it says that the overflow is false, but there's no tag in the box. Basically this happens:
I tried to put the calculation inside this $nextTick(), but it didn't solve the issue. I also tried using Vue's keyDown, keyUp and keyPress listeners, but nothing solved this also.
This (also on CodePen) demonstrates the issue:
new Vue({
el: '#tagsinput',
data: {
input_value: "",
tags: []
},
methods: {
addTag: function() {
if (this.input_value > "") {
this.tags.push(this.input_value)
this.input_value = "";
// Refocus the text input, so it stays at the end
this.$refs.input.blur();
this.$nextTick(function() {
this.$refs.input.focus();
})
}
},
deleteTag: function() {
if (this.input_value == "") {
this.tags.pop()
}
}
}
})
.outter {
border: 1px solid red;
width: 250px;
overflow: hidden;
display: flex;
}
.inner {
border: 1px solid blue;
margin: 2px;
display: flex;
}
.tag {
border: 1px solid black;
margin: 2px;
}
input {
min-width: 80px;
width: 80px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.2/vue.min.js"></script>
<div id="tagsinput">
<div class="outter" ref="outter">
<div class="inner" ref="inner">
<div class="tag" v-for="tag in tags">{{tag}}</div><input type="text" v-model="input_value" #keydown.enter="addTag" #keydown.delete="deleteTag">
</div>
</div>
Outter div scrollwidth: {{ $refs.outter ? $refs.outter.scrollWidth : null }}<br> Outter div offsetWidth: {{ $refs.outter ? $refs.outter.offsetWidth : null }}<br>
<br> Is overflowing: {{ ($refs.outter ? $refs.outter.scrollWidth : null) > ($refs.outter ?$refs.outter.offsetWidth : null) }}
</div>
<br><br> Type a really long word in, add and then delete it. "Is overflowing" will be the inverse, until you press Backspace <b>again</b>.
Any help is greatly appreciated.
You should call the check for overflow after the moment you've added or deleted the tag, so you check the overflow at the right moment. Vue isn't databinding an inline condition like that. The following code should work for you. It calls a checkOverflow function within $nextTick, setting a data-binded variable isOverflowed that you then can use to bind some styles.
new Vue({
el: '#tagsinput',
data: {
input_value: null,
tags: [],
isOverflowed: false
},
methods: {
addTag: function() {
if(this.input_value) {
this.tags.push(this.input_value)
this.input_value = null;
// Refocus the text input, so it stays at the end
this.$refs.input.blur();
this.$nextTick(function() {
this.$refs.input.focus();
this.checkOverflow()
})
}
},
deleteTag: function() {
if(!this.input_value) {
this.tags.pop()
this.$nextTick(function() {
this.checkOverflow()
})
}
},
checkOverflow: function() {
this.isOverflowed = (this.$refs.outter ? this.$refs.outter.scrollWidth : null) >
(this.$refs.outter ? this.$refs.outter.offsetWidth : null)
}
}
})
.outter {
border: 1px solid red;
width: 250px;
overflow: hidden;
display: flex;
}
.inner {
border: 1px solid blue;
margin: 2px;
display: flex;
}
.tag {
border: 1px solid black;
margin: 2px;
}
input {
min-width: 80px;
width: 80px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="tagsinput">
<div class="outter" ref="outter">
<div class="inner" ref="inner">
<div class="tag" v-for="tag in tags">{{tag}}</div>
<input type="text" v-model="input_value" #keydown.enter="addTag" #keydown.delete="deleteTag" ref="input">
</div>
</div>
<br>
Is overflowing:
{{ isOverflowed }}
</div>
<br><br>
Type a really long word in, add and then delete it. "Is overflowing" will be the inverse, until you press Backspace <b>again</b>.
More of a CSS/HTML hack ...
Add <div id="spaceFade"></div> after <div id="tagsinput">, then add the following CSS :
#spaceFade {
background-image: linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,1), rgba(0,0,0,0));
position : absolute;
height : 2em;
width : 3em;
}
#tagsinput {
position : relative;
}
.outter {
justify-content: flex-end;
}
i have a vertical list of items, each of which can be removed. I put my items inside a transition-group and created simple opacity and transform transitions for them. The transitions on the removed elements work as expected, however if I remove an element which is not placed at the bottom, the ones beneath just jump up and take its place without any transition. I Can't find a way to target this behaviour.
All I want is just that the elements below slide up smoothly.
Is there any way to achieve this effect by using css transitipms and Vue's animation hooks?
Here is a demo: https://jsfiddle.net/gcp18nq0/
Template:
<div id="app">
<div class="form">
<label for="name">Name</label>
<input type="text" id="name" v-model="name">
<button #click="addPlayer">Add player</button>
</div>
<div class="players">
<transition-group name="player">
<div class="panel" v-for="player in players" :key="player.id">
<h2>
{{ player.name}}
<span class="remove" #click="removePlayer(player.id)">Remove</span>
</h2>
</div>
</transition-group>
</div>
</div>
Script:
data() {
return {
name: "",
players: [
{id: 1, name: 'Player1'},
{id: 2, name: 'Player2'},
{id: 3, name: 'Player3'},
]
}
},
methods: {
addPlayer: function () {
//,,,,
},
removePlayer: function (playerId) {
//...
}
}
});
CSS
.form {
margin:0 auto;
width:400px;
}
.panel {
width: 400px;
margin: 10px auto;
overflow: hidden;
border: 1px solid;
text-align: center;
}
.remove {
float: right;
cursor: pointer;
text-decoration: underline;
font-size: 12px;
vertical-align: bottom
}
.player-enter,
.player-leave-to
/* .fade-leave-active below version 2.1.8 */
{
opacity: 0;
}
.player-enter {
transform: translateY(30%);
}
.player-leave-to {
transform: translateX(30%);
}
.player-enter-active,
.player-leave-active {
transition: all 1.5s;
}
.player-move {
transition: all 1.5s;
}
The only working way I found was by adding position:absolute on "player-leave-active" state but since the element collapses it changes its vertical position, which is not the desired effect. I also tried changing the height but there the elements below still jump up a bit after the height is set to 0.
Im sure that this can be achieved easily with jQuery but i believe that there should be a way to do it without js.
Thank you in advance!
p.s. its my first post here, so i hope that it was explained clearly enough.
So I made some small tweaks to your fiddle: https://jsfiddle.net/gcp18nq0/1/ and hopefully that is what you looking for.
The most important change was setting display: inline-block on the .panel class, according to the Vue documentation:
One important note is that these FLIP transitions do not work with
elements set to display: inline. As an alternative, you can use
display: inline-block or place elements in a flex context.
new Vue({
el: "#app",
data() {
return {
name: "",
players: [{
id: 1,
name: 'Batman'
},
{
id: 2,
name: 'Robin'
},
{
id: 3,
name: 'Superman'
},
{
id: 4,
name: 'Spiderman'
},
]
}
},
methods: {
addPlayer: function() {
const newPlayer = {
id: this.players.length + 1,
name: this.name,
};
this.players.push(newPlayer);
},
deletePlayer: function(playerId) {
let playerToRemove = this.players.find((player) => {
return player.id === playerId;
});
let playerIndex = this.players.indexOf(playerToRemove);
this.players.splice(playerIndex, 1);
}
}
});
.form {
margin: 0 auto;
width: 400px;
}
.panel {
width: 400px;
margin: 6px auto;
overflow: hidden;
border: 1px solid;
text-align: center;
transition: all 1s;
display: inline-block;
}
.players {
position: relative;
text-align: center;
}
.remove {
float: right;
cursor: pointer;
text-decoration: underline;
font-size: 12px;
vertical-align: bottom
}
.player-enter,
.player-leave-to {
opacity: 0;
}
.player-enter {
transform: translateY(30%);
}
.player-leave-to {
transform: translateX(300%);
}
.player-leave-active {
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<div class="form">
<label for="name">Name</label>
<input type="text" id="name" v-model="name">
<button #click="addPlayer">Add player</button>
</div>
<div class="players">
<transition-group name="player" tag="div">
<div class="panel" v-for="player in players" :key="player.id">
<h2>
{{ player.name}}
<span class="remove" #click="deletePlayer(player.id)">Remove</span>
</h2>
</div>
</transition-group>
</div>
</div>