Vue.js 2 dynamically adding and removing classes in looped elements - javascript

I have a project here where I need to add and remove layers of images, using vue.js 2. I am building up a pizza where I need to add toppings. My current solution has a flaw - it removes all other elements/ pizza toppings when I add a new one.
The toppings are generated from an array which I loop through.
Can you please help, I am sure this is easy but me being a rookie in vue.js I have already struggled for hours... Thanks!
<div id="app" class="container-fluid">
<div class="row">
<div class="left-container">
<h2>add your ingredients:</h2>
<div v-for="(item, index) in pizzas" v-bind:key="index">
<button class="btn btn-primary" v-on:click="show == index ? show = -1 : show = index">{{ item.pizza }}</button>
</div>
<div class="submit-buttons">
<button class="btn btn-primary reset-pizza" v-on:click="show = -1">Reset pizza</button>
<button class="btn btn-primary submit-pizza">Share pizza</button>
</div>
</div>
<div class="right-container">
<ul class="pizza-layers">
<li v-for="(item, index) in pizzas" class="pizza-canvas" v-bind:class="item.class" v-if="show == index"></li>
<li class="pizza-canvas pizza-canvas--topping-base"></li>
</ul>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
pizzas: [
{ pizza: 'Salami', class: 'pizza-canvas--topping-salami' },
{ pizza: 'Rucolla', class: 'pizza-canvas--topping-rucolla' },
{ pizza: 'Cheese', class: 'pizza-canvas--topping-cheese' }
],
show: {},
},
})
</script>

To allow for multiple toppings, this.show should be an array, instead of an object.
Once you change that, you'd need to modify the click event handler to add/remove the topping based on whether or not the topping is already part of show.
Also, while displaying the topping, you'd need to check its existence using show.includes(index) instead of show == index - since this.show is an array.
In the snippet below, I've applied these changes, and added some background colors to visualize how the toppings are added or removed.
new Vue({
el: '#app',
data: {
pizzas: [{
pizza: 'Salami',
class: 'pizza-canvas--topping-salami'
},
{
pizza: 'Rucolla',
class: 'pizza-canvas--topping-rucolla'
},
{
pizza: 'Cheese',
class: 'pizza-canvas--topping-cheese'
}
],
show: [],
},
methods: {
addTopping(event, item, index) {
if(this.show.includes(index)) {
this.show.splice(this.show.indexOf(index),1);
} else {
this.show.push(index);
}
}
}
})
.pizza-canvas--topping-salami {
background-color: red;
}
.pizza-canvas--topping-rucolla {
background-color: yellow;
}
.pizza-canvas--topping-cheese {
background-color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="app" class="container-fluid">
<div class="row">
<div class="left-container">
<h2>add your ingredients:</h2>
<div v-for="(item, index) in pizzas" v-bind:key="index">
<button class="btn btn-primary" v-on:click="addTopping(event, item, index)">{{ item.pizza }}</button>
</div>
<div class="submit-buttons">
<button class="btn btn-primary reset-pizza" v-on:click="show = []">Reset pizza</button>
<button class="btn btn-primary submit-pizza">Share pizza</button>
</div>
</div>
<div class="right-container">
<ul class="pizza-layers">
<li v-for="(item, index) in pizzas" class="pizza-canvas" v-bind:class="item.class" v-if="show.includes(index)"></li>
<li class="pizza-canvas pizza-canvas--topping-base"></li>
</ul>
</div>
</div>
</div>

Related

How can I get 2 or more animations to work in a single div with UIKit?

I am working on a to-do web application and I wanted to make it look nicer with some animations from UIKit. Right now, I have an animation that toggles when the todo-items gets put into view:
<div class="todo-items uk-animation-toggle uk-animation-scale-up" tabindex="0" :id="style.id">
Where style.id changes based on the todo item.
I also have a checkmark that moves the todo item to the completed section of the webpage:
<input type="checkbox" uk-toggle :target="style.target" animation="uk-animation-scale-up uk-animation-reverse" mode="click" #click="completedTask">
However, the animation for that checkmark does not seem to work when there is already an animation for the block when it initially appears. The topmost toggle animation takes precedence and the checkmark animation does not work. If I remove the topmost animation, the checkmark works as intended, it is when there are two when it does not work.
This is a template from a Vue.js component:
<div class="todo-items uk-animation-toggle uk-animation-scale-up" tabindex="0" :id="style.id">
<div class="row">
<div class="col-1-10">
<div class="center">
<input type="checkbox" uk-toggle :target="style.target" animation="uk-animation-scale-up uk-animation-reverse" mode="click" #click="completedTask">
</div>
</div>
<div class="col-7-10">
<h3>Task {{ count + 1 }}</h3>
<p v-show="!edit" #click="editTask">{{ description }}</p>
<input v-show="edit" v-model="description" #keyup.enter="editTask">
</div>
<div class="col-2-10">
<div class="center">
<button #click="editTask">{{ edit ? "Done" : "Edit" }}</button>
<button #click="deleteTask" id="deleteButton">Delete</button>
</div>
</div>
</div>
</div>
Here is the whole component if needed:
Vue.component('todo-item', {
props: {
count: {
type: Number,
required: true
},
description: {
type: String,
required: true
},
index: {
type: Number,
required: true
}
},
template:
`
<div class="todo-items uk-animation-toggle uk-animation-scale-up" tabindex="0" :id="style.id">
<div class="row">
<div class="col-1-10">
<div class="center">
<input type="checkbox" uk-toggle :target="style.target" animation="uk-animation-scale-up uk-animation-reverse" mode="click" #click="completedTask">
</div>
</div>
<div class="col-7-10">
<h3>Task {{ count + 1 }}</h3>
<p v-show="!edit" #click="editTask">{{ description }}</p>
<input v-show="edit" v-model="description" #keyup.enter="editTask">
</div>
<div class="col-2-10">
<div class="center">
<button #click="editTask">{{ edit ? "Done" : "Edit" }}</button>
<button #click="deleteTask" id="deleteButton">Delete</button>
</div>
</div>
</div>
</div>
`,
data() {
return {
edit: false,
style: {
id: 'task' + this.index,
'target': '#task' + this.index,
'animation': 'uk-animation-scale-up uk-animation-reverse',
'slideRight': 'uk-animation-slide-right uk-animation-reverse',
'mode': 'click',
'duration': 400
}
}
},
methods: {
completedTask() {
setTimeout(() => this.$emit('complete-task', this.index), this.style.duration);
},
deleteTask() {
this.$emit('remove-task', this.index);
},
editTask() {
this.edit = !this.edit;
if (!this.edit) {
this.$emit('edit-task', this.description, this.index);
}
}
}
})
I think it's impossible. The alternative is remove the previous animation when it's finished.
<div
class="todo-items uk-animation-toggle"
:class="animationEnter ? 'uk-animation-scale-up' : ''"
:id="style.id"
tabindex="0">
...
</div>
...
mounted () {
setTimeout(() => {
this.animationEnter = false
}, 500)
}
...
Example
Or use Vue List-Transitions which is reliable and easier:
<transition-group
name='list'
tag='div'
appear
enter-active-class='uk-animation-scale-up'
leave-active-class='uk-animation-scale-up uk-animation-reverse'>
<todo-item
v-for='item in todoItems'
:key='item.id'
...>
</todo-item>
</transition-group>
Example

I want to show only one input clicked (on vue.js)

I would like to show an input on the click, but being in a for loop I would like to show only the clicked one
<div v-for="(todo, n) in todos">
<i class="fas fa-minus ml-2"></i>
<li class="mt-2 todo">
{{ todo }}
</li>
<li class="button-container">
<button class="ml-1 btn btn-primary rounded-circle btn-sm" v-if="isHidden" v-on:click="isHidden = false"><i
class="THIS-CLICK"></i></button>
<input class="ml-5 border border-primary rounded" v-if="!isHidden" v-model="todo">
<button class="ml-1 btn btn-success rounded-circle btn-sm" v-if="!isHidden" v-on:click="isHidden = true"
#click="modifyTodo(n, todo)"><i class="far fa-save"></i></button>
</li>
</div>
I would like that on clicking on THIS-CLICK, only one input (that of the button clicked) is visible, but being in a for loop I don't know if it can be done
I would suggest to change the structure a bit in your app. You can clean up your code a lot by using v-if inside the button instead of two different buttons.
Also, moving as much javascript out from the markup as possible is a good practice.
I have an example below where this is done.
Regarding your question, you would have to add the key to each todo item.
new Vue({
el: "#app",
data() {
return {
todos: [{
name: 'wash hands',
isHidden: true
},
{
name: 'Stay home',
isHidden: true
}
],
};
},
methods: {
toggleTodo(n, todo) {
const hidden = todo.isHidden;
if (hidden) {
this.modifyTodo(n, todo);
todo.isHidden = false;
} else {
todo.isHidden = true;
}
},
modifyTodo(n, todo) {
//Some logic...
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<div v-for="(todo, n) in todos">
<i class="fas fa-minus ml-2"></i>
<li class="mt-2 todo">
{{ todo.name }}
</li>
<li class="button-container">
<input class="ml-5 border border-primary rounded" v-if="!todo.isHidden" v-model="todo.name">
<button #click="toggleTodo(n, todo)">
<i v-if="todo.isHidden" class="THIS-CLICK">click-this</i>
<i v-else class="far fa-save">save</i>
</button>
</li>
</div>
</div>
If you cannot do this, you could go with adding a new key to data like: hiddenTodos that would be an array of ids/a unique identifier to the todo you selected to hide.
in the template, it would be something like this:
<button #click="hiddenTodos.push(todo)">
...
<div v-if="hiddenTodos.includes(todo)"

switching between v-if and v-else with a button

am having problem switching from the first section(v-if) to the second second(v-else) section using v-if. as default now section(v-if) is displaying and i created a button(startGame) to change to the second section(v-else) when clicked but i find it not easy to do please help me out
<section class="row controls" v-if="!gameIsrunning">
<div class="small-12 columns">
<button id="start-game" #click="startGame">START NEW GAME</button>
</div>
</section>
<section class="row controls" v-else>
<div class="small-12 columns">
<button id="attack">ATTACK</button>
<button id="special-attack">SPECIAL ATTACK</button>
<button id="heal">HEAL</button>
<button id="give-up">GIVE UP</button>
</div>
</section>
<script>
let app = new Vue({
el: "#app",
data: {
playerHealth: 100,
monsterHealth: 100,
gameIsRunning: false
},
methods: {
startGame: function() {
this.gameIsRunning = true;
}
}
});
</script>

I want to add counter in for loop in each item.(vue.js)

I'm making cart app with Vue. And trying to make quantity counter, but when I click - or + button, all of items quantity also increase or decrease.
So it seems like I need to give each key for buttons but I don't know how to do that.
new Vue({
el: '#app',
data(){
return {
foods: [{
id: 1,
imgUrl: 'https://image.shutterstock.com/image-photo/healthy-food-clean-eating-selection-260nw-761313058.jpg',
title: 'Food',
price: '5,00'
}],
num:0
}
},
methods:{
increase(index){
this.num++
},
decrease(index){
this.num --
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div
class="items" v-for="(food,index) in foods"
v-bind:food="food"
v-bind:key="food.id"
>
<img class="foodImg" v-bind:src="food.imgUrl" />
<h4>{{food.title}}</h4>
<p>{{food.price}}€</p>
<button :class="index" class="minus" #click="decrease">-</button>
{{num}}
<button :class="index" class="add" #click="increase">+</button>
<button type="submit">Add to cart</button>
</div>
</div>
Your num variable shouldn't be in your component and instead you should attach it to your food items. Otherwise the num variable is shared across all of them.
Please don't forget to give your food items the num variable before you pass the foods array to your component so its not initially empty.
try this:
<div class="items" v-for="(food,index) in foods" v-bind:food="food" v-bind:key="food.id">
<img class="foodImg" v-bind:src="food.imgUrl"/>
<h4>{{food.title}}</h4>
<p>{{food.price}}€</p>
<button :class="index" class="minus" #click="increase(food)">-</button>
{{food.num}}
<button :class="index" class="add" #click="decrease(food)">+</button>
<button type="submit">Add to cart</button>
</div>
Script
<script>
export default {
name:'Products',
props:['foods'],
methods:{
increase(food){
food.num++
},
decrease(index){
food.num--
}
}
}

inner nested ng repeat section not binding to scope variable and getting commented

I have a nested ng-repeat like this. I have a scope variable boards which is array and has further nesting with another array called tasks which is again an array. As you can see in the inner ng-repeat I am trying to bind to task.content.
<div class="col-md-3 boards topmargin leftmargin" ng-repeat="board in boards">
<div class="row">
<div class="col-md-12 centerText bordered"><b>{{board.title}}</b></div>
</div>
<div class="row topmargin tasksContainer">
<div class="col-md-12">
<p ng-init="tasks = board.tasks" ng-repeat="task in tasks" ng-init="taskIndex=$index">
<div>
<span>{{taskIndex}}</span>
<span>{{task.content}}</span>
</div>
</p>
</div>
<hr>
</div>
<div class="row topmargin addTask">
<div class="col-md-12"><textarea class="addTaskField" placeholder="enter task here....."
ng-model="newTask.content"></textarea></div>
<button class="btn btn-primary btn-block" ng-click="addNewTask(board)">Add Task</button>
</div>
</div>
This is the structure of boards array
// vars
$scope.boards = [];
$scope.board={
title: "",
tasks: []
};
$scope.newTask = {
content: "",
tags: [],
completed: null
};
I push the newTask object into board.tasks and board object in boards array which is working fine and on debugger boards array look like this
$scope.boards = [
{
title : "shopping",
tasks : [
{
content: "pen",
complete: false,
tags: []
},
{
content: "bread",
complete: true,
tags: ['groceries']
}
]
},
{
title : "tomorrow",
tasks : [
{
content: "go swimming",
complete: false,
tags: []
},
{
content: "complete to-do app",
complete: false,
tags: ['urgent']
}
]
}
];
The problem I am facing is that the binding {{task.content}} and {{taskIndex}} are not displaying. What am I doing wrong?
A few of things here:
The link that EProgrammerNotFound linked to in the comments (a <p> tag cannot contain a <div> tag)
Second, your ng-repeat is missing boards: ng-repeat="task in board.tasks". So, I think it's supposed to look like this:
<div class="col-md-3 boards topmargin leftmargin" ng-repeat="board in boards">
<div class="row">
<div class="col-md-12 centerText bordered"><b>{{board.title}}</b></div>
</div>
<div class="row topmargin tasksContainer">
<div class="col-md-12">
<div ng-repeat="task in board.tasks" ng-init="taskIndex=$index">
<div>
<span>{{taskIndex}}</span>
<span>{{task.content}}</span>
</div>
</div>
</div>
<hr>
</div>
<div class="row topmargin addTask">
<div class="col-md-12">
<textarea class="addTaskField" placeholder="enter task here....." ng-model="newTask.content"></textarea>
</div>
<button class="btn btn-primary btn-block" ng-click="addNewTask(board)">Add Task</button>
</div>
</div>
Another thing is your <p> tag with the ng-repeat has two ng-inits. Not sure what that results in :)
Example here https://plnkr.co/edit/yJ7u4YTu2TAfhFajAjUY

Categories