Display after class based on amount of items in loop - vue.js

I am displaying data in html using in loop, inside it I got a class
div(v-for="item in items" :key="item.id")
.circle
p {{ item.name }}
in my css I got
.circle::after
content: ''
background: grey
height: 5px
width: 130px
position: absolute
left: 0
top: 16px
z-index: -1
Can I display this after class only if there is more than 1 item in my loop?

As I said in the comments, ::after pseudo element is CSS construct so you cannot control it using Vue.
I see two possible solutions:
Define two classes - one with ::after and one without it and conditionally apply only one of the classes based on the number of items
You can actually do it entirely with CSS using :not(:only-child) pseude-selector. See example below
new Vue({
el: "#app",
data: {
toggle: true,
items: [
{ text: "Learn JavaScript", id: 1 },
{ text: "Learn Vue", id: 2 },
{ text: "Play around in JSFiddle", id: 3 },
{ text: "Build something awesome", id: 4 }
]
},
computed: {
computedItems() {
return this.toggle ? this.items : this.items.slice(0,1)
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
.circle:not(:only-child)::after {
content: " ➥";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="toggle = !toggle">
Switch items
</button>
<div>
<div v-for="item in computedItems" :key="item.id" class="circle">
{{ item.text }}
</div>
</div>
</div>

Related

Inline style in vue

I try:
<a v-for='(item, index) in categories' :key='index'>
<div class='slider-categories__slide' :style='{ background: item.background}'>
</div>
</a>
Didn't work. Is it possible? If not, how i can add background for elements? (different background for items)
you can have an object named for example style in each object in the array, in each style object you can have specific style for that object and bind that to the style attribute on the element like :style="item.style".
also if you can't have a dedicated object for the styles in you array's objects you can use the data that you have to construct the appropriate object binding in the v-for, just pay attention to the correct formatting.
check the demo below: (here I used destructuring in v-for but its not necessary)
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: {
items: [{
id: 1,
style: {
background: 'blue'
}
},
{
id: 2,
style: {
background: "url('https://picsum.photos/id/1025/200')",
backgroundSize: 'contain'
}
},
{
id: 3,
style: {
background: "linear-gradient(#e66465, #9198e5)"
},
},
]
},
})
.items {
height: 100px;
width: 100px;
display: inline-block;
border: 2px solid red;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<div id="app">
<div class="items" v-for="{id, style} in items" :key="id" :style="style"></div>
</div>

Vue onclick display specific item

I have a question about Vue.
I want to add a class to a specific item:
<p v-on:click="display = !display">Rediger joke</p>
Display is False before and it change it to true.
And it works. But my problem is, that this onclick is inside an v-for loop, and i only want to put "display" on one "update-site" and not all of them. Can i do this or do I have to try a different setup?
Thanks a lot
I have this idea that might help you. The idea is you extend post object with for example visible property and when you click event triggered you change this property and add .display class. Please check this jsfiddle
template
<div id="app">
<article v-for="post in filteredPosts" :key="post.id">
{{post.name}}
<button #click="display(post)">show</button>
<div class="post-content" :class="{display: post.visible}">this is the part I want to display onclick</div>
<hr />
</article>
</div>
css
.post-content {
display: none;
}
.post-content.display {
display: block;
}
code
new Vue({
el: '#app',
data() {
return {
posts: []
};
},
created() {
//when you load posts. add visible property.
setTimeout(() => {
//posts from server
var postsFromServer = [{
id: 1,
name: 'Post One'
},
{
id: 2,
name: 'Post Two'
}
];
//add visible proprty.
this.posts = postsFromServer.map(post => {
return {
...post,
visible: false
};
});
}, 1000);
},
computed: {
filteredPosts() {
//do your filters
return this.posts;
}
},
methods: {
display(post) {
this.$set(post, 'visible', !post.visible);
}
}
});
I have an article, and i get the data from Firebase.
<article v-for="post in filteredPosts" :key="post.id">
{{post.name}}
<p v-on:click="display = !display"></p>
<div>this is the part I want to display onclick</div
</article>
updateInputs has display:none, but onclick I want it to be display as block:
.updateInputs.display {
display: block;
position: absolute;
top:0;
left:0;
bottom: 0;
background-color: white;
box-shadow: 4px 4px 10px black;
width: 100%;
height: auto;
overflow: hidden;
overflow-y: scroll;
padding-bottom: 10px;
}

How to animate todo moving from one list to another with Vue.js?

I am trying to do this svelte example todo moving animation with Vue.js.
Below you can find what I have done so far. Just click on the todo to see.
new Vue({
el: "#app",
data: {
items: [
{ id: 1, name: 'John', done: false },
{ id: 2, name: 'Jane', done: false },
{ id: 3, name: 'Jade', done: true },
{ id: 4, name: 'George', done: true },
]
},
computed: {
done () {
return this.items.filter(i => i.done)
},
undone () {
return this.items.filter(i => !i.done)
}
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
height: 500px;
transition: all 0.2s;
}
.todos {
display: grid;
grid-template-columns: 1fr 1fr;
}
.todo {
border: 1px solid #ccc;
}
.todo.undone {
grid-column: 2 /span 1;
}
.todo.done {
grid-column: 1 /span 1;
background: blue;
color: white;
}
.flip-list-move {
transition: all 1s ease-in-out;
}
.header-wrapper {
display: grid;
grid-auto-flow: column;
}
.header, .todo {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="header-wrapper">
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
</div>
<transition-group name="flip-list" tag="div" class="todos">
<div class="todo done" v-for="item of done" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>26</span>
<span>Male</span>
</div>
<div class="todo undone" v-for="item of undone" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
</transition-group>
</div>
In order to animate the todo move from one list to another, I used CSS grid but I can't find a way to distinguish todos (left and right) without having a grid cell which is empty.
I would appreciate if there is a better way to achieve the example in svelte docs or a way to omit the empty cells.
Even though it seemed easy in the beginning, it's a bit tricky.
You can target the first element by tracking the index in the v-for loop. Index 0 is always going to be the first element. And give it the following style:
grid-row-start: 1;
EDIT DEMO:
new Vue({
el: "#app",
data: {
items: [
{ id: 1, name: 'John', done: false },
{ id: 2, name: 'Jane', done: false },
{ id: 3, name: 'Jade', done: true },
{ id: 4, name: 'George', done: true },
]
},
computed: {
done () {
return this.items.filter(i => i.done)
},
undone () {
return this.items.filter(i => !i.done)
}
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
height: 500px;
transition: all 0.2s;
}
.todos {
display: grid;
grid-template-columns: 1fr 1fr;
}
.todo {
border: 1px solid #ccc;
}
.todo.undone {
grid-column: 2 /span 1;
}
.todo.done {
grid-column: 1 /span 1;
background: blue;
color: white;
}
.first-right {
grid-row-start: 1;
}
.flip-list-move {
transition: all 1s ease-in-out;
}
.header-wrapper {
display: grid;
grid-auto-flow: column;
}
.header, .todo {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="header-wrapper">
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
</div>
<transition-group name="flip-list" tag="div" class="todos">
<div class="todo done" v-for="item of done" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>26</span>
<span>Male</span>
</div>
<div :class="['todo', 'undone', { 'first-right': index === 0 }]" v-for="(item, index) of undone" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
</transition-group>
</div>
Adding grid-row-start to the first undone element doesn't works if there are more than 6 items in array.
As a solution, I used the index of v-for loop to add to every undone todo the corresponding grid-row-start.
index starts at 0 so we have to make index + 1
<div
class="todo undone"
v-for="(item, index) of undone"
:key="item.id"
:style="{'grid-row': index + 1}" // => HERE we guarantee no gaps are present in undone list`
#click="toggle(item)"
>
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
You can find the working example on this codesandbox

Unexpected behaviour removing a child component (row)

Description:
I have a table with some products, each row is a custom vue <row> component.
Each element has a closing (removing) button that triggers the custom "remove" event. The main app listens to this event and removes the children (by index)
The row a part from some static text it contains an input with a number.
The problem:
The parent (Vue app) removes the row, but the value of the input is then moved (and replaces its previous value) to the input in the next row.
Expected behaviour:
I want to simply remove the item I do not care about the value of the text input once it's removed. It should not move its value to the next sibling.
I attach an example.
let row = Vue.component('row', {
name: 'row',
props: ['number', 'name', 'sq'],
data: () => ({
quantity: 0
}),
template: '<tr>' +
'<td>{{number}}</td>' +
'<td>{{name}}</td>' +
'<td><button v-on:click="quantity--">-</button><input type="text" :value="quantity"><button v-on:click="quantity++">+</button></td>' +
'<td><button v-on:click="remove">×</button></td>' +
'</tr>',
methods: {
remove: function() {
this.$emit('remove', this.quantity)
}
},
beforeMount() {
this.quantity = this.sq
}
})
new Vue({
el: "#app",
data: {
out: [],
rows: [{
name: "Icecream",
sq: 0
},
{
name: "Sugar cube",
sq: 50
},
{
name: "Peanut butter",
sq: 0
},
{
name: "Heavy cream",
sq: 0
},
{
name: "Cramberry juice",
sq: 0
}
]
},
methods: {
removeRow: function(index, quantity) {
this.out.push(`Removing row ${index} (${this.rows[index].name} | ${quantity} units)`)
this.rows.splice(index, 1)
}
},
computed: {
log: function() {
return this.out.join("\r\n")
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
h2 {
font-weight: bold;
margin-bottom: 10px;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
td {
padding: 4px 5px;
}
input {
width: 40px;
text-align: center;
}
h4 {
margin-top: 20px;
margin-bottom: 5px;
}
#log {
padding: 10px;
background: #20262E;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<h2>Cart:</h2>
<table>
<row v-for="(row, index) in rows" :number="index" :name="row.name" :sq="row.sq" v-on:remove="removeRow(index, $event)"></row>
</table>
<h4>Log</h4>
<pre id="log" v-html="log"></pre>
</div>
As #Bert mentioned in the comments.
The problem was that I was missing a key.
https://v2.vuejs.org/v2/api/#key
Adding it solved the problem
Thanks

Vuejs - How to v-for loop on click

I am learning Vue and trying to complete a task myself.
I would like to find out how to run v-for loop on #click so that on initial state, only "city" template appears, and on each click, tour template renders with related tours.
let EventBus = new Vue();
Vue.component('cities', {
name: 'Listings',
props: ['city','tour'],
template: `
<div>
<div class='city-list box'>
<city v-for='city in cities' :id='city.id' :key='city.id' :city='city' :tour='tour' #click.native='select(city)'></city>
</div>
<div class='tour-list box'>
<tours v-for='tour in filter(tours)' :id='tour.id' :key='tour.id' :tour='tour'></tours>
</div>
</div>
`,
data() {
return {
cities: [
{id:1, name: 'Istanbul'},
{id:2, name: 'Paris'},
{id:3, name: 'Barça'},
{id:4, name: 'Rome'},
{id:5, name: 'Mars'}
],
tours: [
{id:1, cid:1, name: 'Bosphorus'},
{id:2, cid:2, name: 'Eiffel'},
{id:3, cid:3, name: 'La Sagrada Familia'},
{id:4, cid:4, name: 'Colosseum'},
{id:5, cid:5, name: 'Mars Canyon'},
{id:6, cid:1, name: 'Sultanahmet'},
{id:7, cid:2, name: 'Champs-Élysées'},
{id:8, cid:3, name: 'Casa Mila'},
{id:9, cid:4, name: 'Trevi Fountain'},
{id:10, cid:5, name: 'Mars Desert'},
]
};
},
methods: {
select(city) {
console.log('select');
EventBus.$emit('filter', city);
},
filter(tours) {
console.log('filter');
EventBus.$on('select', ()=>{
cid = this.city.id;
return tours.filter(function(tour) {
return tour.cid == cid;
});
});
},
},
components: {
'city': {
name: 'City',
props: ['city'],
template: `
<div :id="[city.name.toLowerCase()]" :class="[city.name.toLowerCase()]">
<h1>{{ city.name }}</h1>
</div>`
},
'tour': {
name: 'Tour',
props: ['city', 'tour'],
template: `
<div :class="['tour-' + tour.id]" :id="[city.name.toLowerCase() + '-tours']" :refs="city.id" :data-id="city.id">
{{ tour.name }}
</div>
`,
},
},
});
new Vue({
el: '#root'
});
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.box {
margin: 48px auto;
max-width: 1024px;
display: flex;
justify-content: center;
align-items: center;
}
.box h1 {
font-size: 1.1rem;
color: #41f;
}
.box > div {
padding: 24px;
margin: 12px;
width: 20%;
min-height: 100px;
border-radius: 2px;
font-size: 1.15rem;
line-height: 1;
}
.box > div:nth-child(1)
{background-color: #ffb3ba;}
.box > div:nth-child(2)
{background-color: #ffdfba;}
.box > div:nth-child(3)
{background-color: #ffffba;}
.box > div:nth-child(4)
{background-color: #baffc9;}
.box > div:nth-child(5)
{background-color: #bae1ff;}
<script src="https://unpkg.com/vue#2.4.4/dist/vue.js"></script>
<div id="root">
<cities></cities>
</div>
I am also interested in the state of art, if it is a good practice to have two templates together (which are related), and connect this model with a db and router (city/tour-list). Or how would you approach to a such case (I guess jsfiddle should be self explanatory).
As a side note I have tried adding tour as a child to parent component [jsfiddle] where I filter results by ID, I am not sure if this way is a better approach both for components and filtering results in the sense of architecture.
https://jsfiddle.net/oy5fdc0r/29/
https://jsfiddle.net/oy5fdc0r/30/
Use a data property to keep track of the selected city, instead of an Eventbus. Then you can use a computed property to show the correct tours, based on the selected city.
computed:{
selectedTours(){
return this.tours.filter(tour=>tour.cid == this.selectedCity.id)
}
},
methods: {
select(city) {
this.selectedCity = city;
},
},