Vue 2 event listener on component root - javascript

I'm trying to capture an event on the component root node, but the following does not work. I don't want to just listen on a node in the component. I want to be able to click on any element and then hit backspace to remove it. The code below is a basic example of how I setup my code.
<template>
<div v-on:keydown.delete="delete()">
<img id="image" src="..." v-on:click="set_active()">
</div>
</template>
<script>
export default {
return {
data() {
active: ''
},
methods: {
delete(){
delete this.$refs[this.active][0];
},
set_active() {
this.active = event.target.getAttribute('id');
}
}
}
}
</script>

After doing some tests, here is what I discovered:
Having a method called delete won't work. I don't know why, the question remains unanswered here. Rename it to remove, for example.
When trying to catch keyboard events on a div, you may need to add a tabindex attribute for it to work. (See here)
Interactive demo
Vue.component('my-component', {
template: '#my-component',
data() {
return {
images: [
"https://media.giphy.com/media/3ohs7KtxtOEsDwO3GU/giphy.gif",
"https://media.giphy.com/media/3ohhwoWSCtJzznXbuo/giphy.gif",
"https://media.giphy.com/media/8L0xFP1XEEgwfzByQk/giphy.gif"
],
active: null
};
},
methods: {
set_active(i) {
this.active = i;
},
remove() {
if (this.active !== null) {
this.images = this.images.filter((_, i) => i !== this.active);
this.active = null;
}
}
}
});
var vm = new Vue({
el: '#app'
});
div {
outline: none; /* Avoid the outline caused by tabindex */
border: 1px solid #eee;
}
img {
height: 80px;
border: 4px solid #eee;
margin: .5em;
}
img:hover {
border: 4px solid #ffcda9;
}
img.active {
border: 4px solid #ff7c1f;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.21/vue.min.js"></script>
<div id="app">
<my-component></my-component>
</div>
<template id="my-component">
<div #keydown.delete="remove" tabindex="0">
<img
v-for="(img, i) in images"
:key="i"
:src="img"
:class="{ active: active === i }"
#click="set_active(i)"
/>
</div>
</template>

Related

How to pass a style property to a child component as a computed property in Vue.js?

I have the following problem:
I have too much logic in my inline style and would to place it inside a computed property. I know, that this is the way, that I should go, but do not know, how to achieve it.
Below I a simple example that I made for better understanding. In it, on button press, the child's component background-color is changing.
My code can be found here: Codesandbox
My parent component:
<template> <div id="app">
<MyChild :colorChange="active ? 'blue' : 'grey'" />
<p>Parent:</p>
<button #click="active = !active">Click Me!</button> </div> </template>
<script> import MyChild from "./components/MyChild";
export default { name: "App", components: {
MyChild, }, data() {
return {
active: false,
}; }, }; </script>
and my child component:
<template> <div class="hello">
<p>Hello from the child component</p>
<div class="myDiv" :style="{ background: colorChange }">
here is my color, that I change
</div> </div> </template>
<script> export default { name: "HelloWorld", props: {
colorChange: {
type: String,
default: "green",
}, }, }; </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> .myDiv { color: white; padding: 1rem 0; } </style>
I also have a second question. Let's say, that I have more than one child component and also would like to change to colors on button click, but those colors differ. How can I achieve it without repeating myself (within the computed properties?)
Code example for my parent component:
<MyChild :colorChange="active ? 'blue' : 'grey'" />
<MyChild :colorChange="active ? 'grey' : 'blue'" />
<MyChild :colorChange="active ? 'blue' : 'red'" />
<MyChild :colorChange="active ? 'yellow' : 'blue'" />
Thanks in advance!
Maybe You can bind class and use different css classes:
Vue.component('MyChild',{
template: `
<div class="hello">
<p>Hello from the child component</p>
<div class="myDiv" :class="collorCurrent">
here is my color, that I change
</div>
</div>
`,
props: {
colorChange: {
type: String,
default: "green",
},
colorDef: {
type: String,
default: "green",
},
isActive: {
type: Boolean,
default: false,
},
},
computed: {
collorCurrent() {
return this.isActive ? this.colorChange : this.colorDef
}
}
})
new Vue({
el: "#demo",
data() {
return {
active: false,
}
},
})
.myDiv { color: white; padding: 1rem; }
.blue {
background: blue;
font-size: 22px;
}
.red {
background: red;
font-variant: small-caps;
}
.yellow {
background: yellow;
color: black;
}
.grey {
background: grey;
text-decoration: underline;
}
.green {
background: green;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<p>Parent:</p>
<button #click="active = !active">Click Me!</button>
<my-child :is-active="active" :color-def="'grey'" :color-change="'blue'"></my-child>
<my-child :is-active="active" :color-def="'blue'" :color-change="'grey'"></my-child>
<my-child :is-active="active" :color-def="'red'" :color-change="'blue'"></my-child>
<my-child :is-active="active" :color-def="'blue'" :color-change="'yellow'"></my-child>
</div>

Set up a v-on:click directive inside v-for

I have displayed a list of images with some information. I want those images to be clickable. And when clicked it should show a div with saying "HI!!". I have been trying to add a variable as show:true in Vue data and tried to build some logic that show becomes false when clicked. But I have not been able to achieve it.
Below is the sample code:
template>
<div>
<h1>SpaceX</h1>
<div v-for="launch in launches" :key="launch.id" class="list" #click="iclickthis(launch)">
<div ><img :src="launch.links.patch.small" alt="No Image" title={{launch.name}} /></div>
<div>ROCKET NAME: {{launch.name}} </div>
<div>DATE: {{ launch.date_utc}} </div>
<div>SUCCESS: {{ launch.success}} </div>
<div>COMPLETENESS: {{ launch.landing_success}} </div>
</div>
<!-- <v-model :start="openModel" #close="closeModel" /> -->
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'SpaceXTimeline',
components: {
},
data: () => ({
launches : [],
openModel : false,
show : true,
}),
methods: {
iclickthis(launch) {
// this should load a model search v-model / bootstrap on vue npm install v-model
console.log(launch.name + " is launched");
console.log("DETAILS: "+ launch.details);
console.log("ROCKET INFO: "+ launch.links.wikipedia);
console.log("CREW DETAILS: "+ launch.crew);
console.log("Launchpad Name: "+ launch.launchpad);
this.openModel = true;
},
closeModel () {
this.openModel = false;
}
},
async created() {
const {data} = await axios.get('https://api.spacexdata.com/v4/launches');
data.forEach(launch => {
this.launches.push(launch);
});
}
};
</script>
<style scoped>
.list {
border: 1px solid black;
}
</style>
Thanks, and appreciate a lot.
v-model is a binding and not an element, unless you've named a component that? Is it a misspelling of "modal"?
Either way, sounds like you want a v-if:
<v-model v-if="openModel" #close="closeModel" />
Example:
new Vue({
el: '#app',
components: {},
data: () => ({
launches: [],
openModel: false,
show: true,
}),
methods: {
iclickthis(launch) {
// this should load a model search v-model / bootstrap on vue npm install v-model
console.log(launch.name + ' is launched');
console.log('DETAILS: ' + launch.details);
console.log('ROCKET INFO: ' + launch.links.wikipedia);
console.log('CREW DETAILS: ' + launch.crew);
console.log('Launchpad Name: ' + launch.launchpad);
this.openModel = true;
},
closeModel() {
this.openModel = false;
},
},
async created() {
const {
data
} = await axios.get('https://api.spacexdata.com/v4/launches');
data.forEach(launch => {
this.launches.push(launch);
});
},
})
Vue.config.productionTip = false;
Vue.config.devtools = false;
.modal {
cursor: pointer;
display: flex;
justify-content: center;
position: fixed;
top: 0;
width: 100%;
height: 100vh;
padding: 20px 0;
background: rgba(255, 255, 255, 0.5);
}
img {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<div id="app">
<h1>SpaceX</h1>
<div v-for="launch in launches" :key="launch.id" class="list" #click="iclickthis(launch)">
<div>
<img :src="launch.links.patch.small" alt="No Image" :title="launch.name" />
</div>
<div>ROCKET NAME: {{ launch.name }}</div>
<div>DATE: {{ launch.date_utc }}</div>
<div>SUCCESS: {{ launch.success }}</div>
<div>COMPLETENESS: {{ launch.landing_success }}</div>
</div>
<div v-if="openModel" #click="closeModel" class="modal">
MODAL
</div>
</div>

How does :class="`level ${file.invalidMessage && 'has-text-danger'}`" break down?

I am following this tutorial to upload files using VueJS and Express server and I have encountered a line of code that I do not understand.
At 8:19 we are introduced to :class="`level ${file.invalidMessage && 'has-text-danger'}`". I couldn't find anything similar in the Vue documentation.
Video Example:
<div v-for="(file, index) in files" :key="index"
:class="`level ${file.invalidMessage && 'has-text-danger'}`"
>
</div>
Vue Docs:
<div v-bind:class="{ active: isActive }"></div>
<div
class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }"
></div>
<div v-bind:class="classObject"></div>
<div v-bind:class="[activeClass, errorClass]"></div>
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
<div v-bind:class="[{ active: isActive }, errorClass]"></div>
There is nothing similar to that weird syntax in the Vue Documentation.
I understand what ${} inside back quotes marks is used for(template literals).
I understand what the code purpose is, I have tested it and works, but I do not understand how it works and what shortcuts were used.
Can somebody "translate" that line of code ?
The :class binding in Vue accepts strings:
new Vue({
el: "#app",
computed: {
classlist() {
return this.classes.join(' ')
}
},
data() {
return {
classes: [
'first-class',
'second-class'
],
domClassList: null
}
},
mounted() {
// getting and setting the classlist of the DOM element
this.domClassList = document.getElementById('classList').getAttribute('class')
}
})
.level {
background: green;
padding: 15px;
}
.first-class {
color: white;
}
.second-class {
font-weight: 700;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div id="classList" :class="`level ${ classlist }`">
CLASSES ADDED AS STRINGS
</div>
<div>Classes: {{ domClassList }}</div>
</div>
As you can see in the snippet above, the classes are made from one level and the join of a Vue data variable (the classes array) that is returned in a computed property (classlist).
The other part of the question is the conditional format:
new Vue({
el: "#app",
computed: {
classlist() {
return this.classes.join(' ')
}
},
data() {
return {
firstOperand: true,
classes: [
'first-class',
'second-class'
],
domClassList: null
}
},
methods: {
setDomClassList() {
// getting and setting the classlist of the DOM element
this.domClassList = document.getElementById('classList').getAttribute('class')
},
toggleOperand() {
this.firstOperand = !this.firstOperand
// this.$nextTick is required so Vue can set the data
// values before we read it from the DOM
const self = this
this.$nextTick(function() {
self.setDomClassList()
})
}
},
mounted() {
this.setDomClassList()
}
})
.level {
background: green;
padding: 15px;
}
.first-class {
color: white;
}
.second-class {
font-weight: 700;
}
/* this class is set so you can see the difference on
toggling the firstOperand variable
*/
.false {
font-style: italic
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div id="classList" :class="`level ${ firstOperand && classlist }`">
CLASSES ADDED AS STRINGS
</div>
<div>Classes: {{ domClassList }}</div>
<hr />
<div>
FIRSTOPERAND IS {{ firstOperand }}<br />
<button #click="toggleOperand">TOGGLE FIRSTOPERAND TO {{ !firstOperand }}</button>
</div>
</div>
The second snippet is almost the same as the first, except the && is introduced. I also created a .false CSS value so you can see the returned false.

Vue.js: Collapse/expand all elements from parent

I need to add "expand/collapse all" functionality for my Vue component(some collapsible panel).
If user clicks collapse button then clicks on some panel and expand it then clicking on collapse button will do nothing because watched parameter will not change.
So how to implement this functionality properly (buttons must collapse and expand components always)?
I prepared simple example(sorry for bad formatting, it looks nice in editor :( ):
var collapsible = {
template: "#collapsible",
props: ["collapseAll"],
data: function () {
return {
collapsed: true
}
},
watch: {
collapseAll: function(value) {
this.collapsed = value
}
}
}
var app = new Vue({
template: "#app",
el: "#foo",
data: {
collapseAll: true
},
components: {
collapsible: collapsible
}
});
.wrapper {
width: 100%;
}
.wrapper + .wrapper {
margin-top: 10px;
}
.header {
height: 20px;
width: 100%;
background: #ccc;
}
.collapsible {
height: 100px;
width: 100%;
background: #aaa;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="foo"></div>
<script type="text/x-template" id="collapsible">
<div class="wrapper">
<div class="header" v-on:click="collapsed = !collapsed"></div>
<div class="collapsible" v-show="!collapsed"></div>
</div>
</script>
<script type="text/x-template" id="app">
<div>
<button v-on:click="collapseAll = true">Collapse All</button>
<button v-on:click="collapseAll = false">Expand All</button>
<collapsible v-for="a in 10" v-bind:collapseAll="collapseAll" v-bind:key="a"></collapsible>
</div>
</script>
Thanks!
This is a case where I might use a ref.
<button v-on:click="collapseAll">Collapse All</button>
<button v-on:click="expandAll">Expand All</button>
<collapsible ref="collapsible" v-for="a in 10" v-bind:key="a"></collapsible>
And add methods to your Vue.
var app = new Vue({
template: "#app",
el: "#foo",
methods:{
collapseAll(){
this.$refs.collapsible.map(c => c.collapsed = true)
},
expandAll(){
this.$refs.collapsible.map(c => c.collapsed = false)
}
},
components: {
collapsible: collapsible
}
});
Example.

Parent onClick is not triggered when the child element is clicked inside button

I have a dropdown-button component which has a click lister. The button has some text and icon. The click event is triggered if we click the button carefully on the outline and not on the text or icon. Here is my component:
<template lang="html">
<div>
<button class="button dropbtn" #click="toggleDropdown">
<span>{{ name }}</span>
<span class="icon"><i class="fa fa-caret-down"></i></span>
</button>
<div v-show="visible" class="dropdown-content is-primary-important">
<slot>
</slot>
</div>
</div>
</template>
<script>
export default {
props: ['name'],
data: function() {
return {
visible: false
}
},
mounted: function() {
this.hideDropdownOnClick();
},
methods: {
toggleDropdown: function() {
// trigged on click of the button
// make the dropdown visible
console.log("toggling dropdown");
this.visible = !this.visible;
},
hideDropdownOnClick: function() {
window.onclick = (event) => {
console.log(event.target);
if (!event.target.matches('.dropbtn')) {
console.log("here", this.visible);
this.visible = false;
}
}
}
}
}
</script>
<style lang="css">
/* Dropdown Content (Hidden by Default) */
.dropdown-content {
position: absolute;
background-color: #fff;
min-width: 140px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
z-index: 1;
}
/* Links inside the dropdown */
.dropdown-content a {
padding: 12px 16px;
text-decoration: none;
display: block;
}
</style>
I feel that I am missing something very basic here, can someone help me with this? Thanks.
Edit
It seems like a bug with the browser as the answer to this question says:
Button element not firing click event when clicking on button text and releasing off it (but still inside the button)?
Adding this CSS rule fixes the issue for me:
span {
pointer-events: none;
}
When You click in span its toggled twice:) once by toggleDropdown method,
second by windows onclick handler.
here is working example: jsfiddle
<template id="tmp">
<div>
<button class="button dropbtn" #click="toggleDropdown">
<span>{{ name }}</span>
<span class="icon"><i class="fa fa-caret-down"></i></span>
</button>
<div v-show="visible" class="dropdown-content is-primary-important">
<slot>
</slot>
</div>
</div>
</template>
<div id="x">
<tmp name="my name">
<span>toggle me!</span>
</tmp>
</div>
Vue.component('tmp', {
template: '#tmp',
props: ['name'],
data: function() {
return {
visible: false
}
},
mounted: function() {
this.hideDropdownOnClick();
},
methods: {
toggleDropdown: function() {
// trigged on click of the button
// make the dropdown visible
console.log("toggling dropdown");
this.visible = !this.visible;
}
}
});
new Vue({
el: '#x',
data: function(){
return {
name: 'myName'
}
}
});
edit:
if you don't want to use clickaway here is small directive to detect clicks outside of element:
var VueClickOutSide = {
bind: function(el, binding, vNode) {
var bubble = binding.modifiers.bubble;
var handler = function(e) {
if (bubble || (!el.contains(e.target) && el !== e.target)) {
binding.value(e);
}
}
el.__vueClickOutside__ = handler;
document.addEventListener('click', handler);
},
unbind: function (el, binding, vNode) {
document.removeEventListener('click', el.__vueClickOutside__);
el.__vueClickOutside__ = null;
}
};
You just have to register that directive:
Vue.directive('click-outside', VueClickOutSide)
And use it in template:
v-click-outside:delay="hideDropdownOnClick"
You can use vue-clickaway to hide dropdown when click outside :
$ npm install vue-clickaway --save
import { mixin as clickaway } from 'vue-clickaway';
export default {
mixins: [ clickaway ],
... the rest of your code ...
You put v-clickaway="visible = false" in your root div of your dropdown-button component.
On click you show and immediately hide list.
You click text or icon (they are "span"),
this.visible = !this.visible;
then code goes to window.onclick where
if (!event.target.matches('.dropbtn'))
and your spans dont have this class. So you set
this.visible = false;
Change check to
if (!event.target.closest('.dropbtn'))

Categories