How can I create a computed array in Vue and then render it using v-if all while using v-model on the computed array? - vue.js

I've been trying to create a computed array which I then render using v-if. But it also needs to work with v-model. The reason it needs v-model is because it's part of a draggable list using vuedraggable.
Currently I get the following error Computed property "list" was assigned to but it has no setter.
The following code is my drag.vue component:
<template>
<div>
<draggable
v-model="list"
v-bind="dragOptions"
class="bigger-area"
#start="isDragging=true"
#end="isDragging=false"
>
<transition-group name="flip-list" type="transition">
<li
v-for="text in list"
:key="text"
id="list1"
class="drag-item flex flex-justify-betweeen"
>{{ text }}</li>
</transition-group>
</draggable>
</div>
</template>
<script>
import draggable from "vuedraggable";
export default {
name: "Drag",
data() {
return {
test: [],
lists: [
{
title: "0-6 months",
correctlyOrderedList: [
"Lifting Head",
"Rolling",
"Sitting (with support)"
]
},
{
title: "6-12 months",
correctlyOrderedList: [
"Crawling on stomach",
"Sitting (without support)",
"Stands with support and walks holding on",
"Rolls a ball"
]
},
{
title: "12-18 months",
correctlyOrderedList: ["Crawling", "Walks alone"]
},
{
title: "18 months – 2 years",
correctlyOrderedList: [
"Walks smoothly and turns corners",
"Walks upstairs with support",
"Begins running"
]
},
{
title: "2-3 years",
correctlyOrderedList: [
"Walks upstairs without support",
"Runs safely",
"Catches using body and arms"
]
},
{
title: "3-4 years",
correctlyOrderedList: ["Kicks a ball forwards", "Can hop on one foot"]
},
{
title: "4-5 years",
correctlyOrderedList: [
"Catches using only their hands",
"Can skip following a demonstration"
]
}
]
};
},
components: {
draggable
},
methods: {
fullArrayMethod() {
//Puts all statements into single array
let i;
let v;
let fullArrayInOrder = [];
for (i = 0; i < this.lists.length; i++) {
for (v = 0; v < this.lists[i].correctlyOrderedList.length; v++) {
fullArrayInOrder.push(this.lists[i].correctlyOrderedList[v]);
}
}
return fullArrayInOrder;
},
disorderedArrayMethod() {
//Randomizes array
let fullArrayInOrder = this.fullArrayMethod();
var copy = [],
n = fullArrayInOrder.length,
i;
// While there remain elements to shuffle…
while (n) {
// Pick a remaining element…
i = Math.floor(Math.random() * fullArrayInOrder.length);
// If not already shuffled, move it to the new array.
if (i in fullArrayInOrder) {
copy.push(fullArrayInOrder[i]);
delete fullArrayInOrder[i];
n--;
}
}
return copy;
},
chunk(array, size) {
const chunked_arr = [];
let index = 0;
while (index < array.length) {
chunked_arr.push(array.slice(index, size + index));
index += size;
}
return chunked_arr;
},
splitArrayFinalProduct() {
let disorderedArray = this.disorderedArrayMethod();
let finalArray = this.chunk(disorderedArray, 3);
return finalArray;
}
},
computed: {
dragOptions() {
return {
animation: 0,
group: "shared",
disabled: false,
ghostClass: "ghost"
};
},
list() {
return this.disorderedArrayMethod();
}
}
};
</script>
Context: I'm trying to create an application which consolidates multiple arrays into one. Randomises the array. The user can then put it back in order and then see if they got it right.

For anyone who may find it useful this is what worked for me. I can't explain the in's and outs of why it works so hopefully somebody smarter than me can elaborate.
To get the computed array variable to work with v-model and v-for I used map() as shown below:
let completeListOfStatements = this.lists.map(
d => d.correctlyOrderedList
);
My understanding of map() is it returns an array.
Then in the v-model I set it to the array that is within the object. This is the same one that I used map() on. This can be seen below.
<draggable
v-model="lists.correctlyOrderedList"
v-bind="dragOptions"
class="list-group"
#start="isDragging=true"
#end="isDragging=false"
>
For comparisons to the code in my question here's all the code from the component:
<template>
<div class="draggable-list-container">
<div
class="draggable-list-inner-container"
v-for="(statement, index) in splitCompleteListOfStatements"
:key="index"
>
<h1>{{ lists[index].title }}</h1>
<draggable
v-model="lists.correctlyOrderedList"
v-bind="dragOptions"
class="list-group"
#start="isDragging=true"
#end="isDragging=false"
>
<transition-group name="flip-list" type="transition">
<li
v-for="(statement, index) in statement"
:key="index + 'index'"
class="drag-item flex flex-justify-betweeen"
>{{ statement }}</li>
</transition-group>
</draggable>
</div>
<div class="submit-button-container">
<button class="btn" #click="revealAnswers">Reveal answers</button>
</div>
</div>
</template>
<script>
import draggable from "vuedraggable";
export default {
name: "Drag",
data() {
return {
lists: [
{
title: "0-6 months",
correctlyOrderedList: [
"Lifting Head",
"Rolling",
"Sitting (with support)"
]
},
{
title: "6-12 months",
correctlyOrderedList: [
"Crawling on stomach",
"Sitting (without support)",
"Stands with support and walks holding on",
"Rolls a ball"
]
},
{
title: "12-18 months",
correctlyOrderedList: ["Crawling", "Walks alone"]
},
{
title: "18 months – 2 years",
correctlyOrderedList: [
"Walks smoothly and turns corners",
"Walks upstairs with support",
"Begins running"
]
},
{
title: "2-3 years",
correctlyOrderedList: [
"Walks upstairs without support",
"Runs safely",
"Catches using body and arms"
]
},
{
title: "3-4 years",
correctlyOrderedList: ["Kicks a ball forwards", "Can hop on one foot"]
},
{
title: "4-5 years",
correctlyOrderedList: [
"Catches using only their hands",
"Can skip following a demonstration"
]
}
]
};
},
components: {
draggable
},
methods: {
disorderedArrayMethod(value) {
//Randomizes array
let fullArrayInOrder = value;
var copy = [],
n = fullArrayInOrder.length,
i;
// While there remain elements to shuffle…
while (n) {
// Pick a remaining element…
i = Math.floor(Math.random() * fullArrayInOrder.length);
// If not already shuffled, move it to the new array.
if (i in fullArrayInOrder) {
copy.push(fullArrayInOrder[i]);
delete fullArrayInOrder[i];
n--;
}
}
return copy;
},
revealAnswers() {this.splitCompleteListOfStatements[0].push("Hello")}
},
computed: {
dragOptions() {
return {
animation: 0,
group: "shared",
disabled: false,
ghostClass: "ghost"
};
},
splitCompleteListOfStatements() {
let completeListOfStatements = this.lists.map(
//Maps out full array (Basically loops through gathers the arrays and creates an array from them)
d => d.correctlyOrderedList
);
completeListOfStatements = completeListOfStatements.reduce(function(
//The map returns an array as the following [[a,b], [], []] etc. So this turns it into [a,b,c,d]
a,
b
) {
return a.concat(b);
}, []);
completeListOfStatements = this.disorderedArrayMethod(
completeListOfStatements
); //This sends it to a method that jumbles the array
var temp = [];
var preVal = 0;
var nextVal = 3;
for (var i = 0; i < 7; i++) {
temp.push(completeListOfStatements.slice(preVal, nextVal));
preVal = nextVal;
nextVal = nextVal + 3;
}
return temp;
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.title {
margin-bottom: 0.5em;
}
.submit-button-container {
margin-top: 5px;
}
.btn {
width: 10em;
height: 5em;
}
.draggable-list-container {
display: inline-block;
justify-content: center;
min-height: 200px;
}
.list-group {
min-height: 80px;
}
.drag-item {
justify-content: center;
padding: 15px 10px;
background-color: whitesmoke;
border: 1px solid black;
width: 20em;
margin: 2px;
cursor: move;
}
.list-group-item {
position: relative;
display: block;
padding: 0.75rem 1.25rem;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.125);
}
.flip-list-move {
transition: transform 0.5s;
}
.no-move {
transition: transform 0s;
}
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
.list-group-item {
cursor: move;
}
</style>

Related

How to trigger function on viewport visible with Vue viewport plugin

I am using an counter to display some numbers, but they load up when the page loads, so it loads unless I do some button to trigger it.
Found this viewport plugin (https://github.com/BKWLD/vue-in-viewport-mixin) but I weren't able to use it. That's what I need to do, trigger a function when I reach some element (entirely), how to achieve it?
You don't necessarily need a package to do this. Add an event listener to listen to the scroll event, and check if the element is in the viewport every time there's a scroll event. Example code below - note that I've added an animation to emphasize the "appear if in viewport" effect.
Codepen here.
new Vue({
el: '#app',
created () {
window.addEventListener('scroll', this.onScroll);
},
destroyed () {
window.removeEventListener('scroll', this.onScroll);
},
data () {
return {
items: [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12
],
offsetTop: 0
}
},
watch: {
offsetTop (val) {
this.callbackFunc()
}
},
methods: {
onScroll (e) {
console.log('scrolling')
this.offsetTop = window.pageYOffset || document.documentElement.scrollTop
},
isElementInViewport(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
callbackFunc() {
let items = document.querySelectorAll(".card");
for (var i = 0; i < items.length; i++) {
if (this.isElementInViewport(items[i])) {
items[i].classList.add("in-view");
}
}
}
}
})
.card {
height: 100px;
border: 1px solid #000;
visibility: hidden;
opacity: 0
}
.in-view {
visibility: visible;
opacity: 1;
animation: bounce-appear .5s ease forwards;
}
#keyframes bounce-appear {
0% {
transform: translateY(-50%) translateX(-50%) scale(0);
}
90% {
transform: translateY(-50%) translateX(-50%) scale(1.1);
}
100% {
tranform: translateY(-50%) translateX(-50%) scale(1);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" onscroll="onScroll">
<div v-for="item in items" class="card">
{{item}}
</div>
</div>
Another option is to use an intersection observer - I haven't explored this yet but this tutorial seems good: alligator.io/vuejs/lazy-image. Note that you will need a polyfill for IE.

Computed styles not applied during leave transition

When an element has a computed style, the style changes are not applied if the element is going through a leave transition:
new Vue({
el: "#app",
data: {
selected: 1,
items: [{
color: 'red'
},
{
color: 'blue'
},
{
color: 'green'
},
],
tweened: {
height: 50,
},
},
computed: {
divStyles() {
return {
height: this.tweened.height + 'px',
background: this.displayed.color,
'margin-left': this.selected * 100 + 'px',
width: '100px',
}
},
displayed() {
return this.items[this.selected - 1]
}
},
watch: {
selected(newVal) {
function animate() {
if (TWEEN.update()) {
requestAnimationFrame(animate)
}
}
new TWEEN.Tween(this.tweened)
.to({
height: newVal * 50
}, 2000)
.easing(TWEEN.Easing.Quadratic.InOut)
.start()
animate()
}
},
methods: {
toggle: function(todo) {
todo.done = !todo.done
}
}
})
.colored-div {
opacity: 1;
position: absolute;
}
.switcher-leave-to,
.switcher-enter {
opacity: 0;
}
.switcher-enter-to,
.switcher-leave {
opacity: 1;
}
.switcher-leave-active,
.switcher-enter-active {
transition: opacity 5s linear;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.21/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/16.3.5/Tween.min.js"></script>
<div id="app">
<button #click="selected--" :disabled="selected <= 1">
Previous
</button>
<button #click="selected++" :disabled="selected >= 3">
Next
</button>
<span>Selected: {{selected}}</span>
<transition name="switcher">
<div v-for="(item, index) in items" v-if="index + 1 === selected" :key="index" :style="divStyles" class="colored-div" />
</transition>
</div>
https://jsfiddle.net/64syzru5/12/
I would expect the leaving element to continue resizing as it fades out, but it doesn't. What can be done to have the computed styles applied to the leaving element during the leave-active transition?
Since you're using CSS for the transitions, Javascript doesn't execute at each intermediate step. That's a good thing for performance, but it means that the computed properties aren't recomputed. As best as I can tell, though, you're just trying to animate the height. That's easily accomplished in pure CSS. Use a before-leave hook to set it to an initial value via an inline style or CSS variable, and then remove that property in the after-leave hook.
More to the point, though, it looks like your application might be more suitable for a transition-group instead of a simple transition.

Nuxt / Vuex / Vue Reactivity Issue Increment

Hi everyone I am I having some difficulty when working with Nuxt and Vuex.
I am trying to run through the example Vuex / Nuxt Classic Mode.
https://nuxtjs.org/guide/vuex-store/
After clicking my increment button I dont see the number go up. My page just stays at 0, I can see within the console that the state knows the number is no longer 0 but not on the screen, as if it doesnt know to be reactive.
My assumption is that I have miss configured something somewhere and my 0 is not the actual state, but I created some copy of it somehow.
Here is my button within my template.
<button #click="inc">{{ counter }}</button>
Here is my inc function within my methods.
inc () {
this.$store.commit('increment')
},
Here is my computed
computed: {
counter () {
return this.$store.getters.counter
}
}
Here is my Vuex/index.js file contained within the store folder.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const createStore = () => {
return new Vuex.Store({
state: () => ({
counter: 0
}),
getters: {
counter: state => state.counter
},
mutations: {
increment (state) {
state.counter++
}
}
})
}
export default createStore
Update: Included more code snippets, and replied to existing comments.
#Boussadjra Brahim #xaviert, thank you both for weighing in, I appreciate the assistance.
#Boussadjra Brahim - Yes, I had tried using an action that called the mutation, that didnt seem to get me there either. I also tried adjusting the state via the action alone, and wasnt able to make any changes, however that seems correct, as I am under the impression that actions call mutations to make state changes and do not themselves do so, please correct me if you know more. I am 100% open to the idea that I did not attempt it correctly. Below is that action that didnt do anything and the one that called the mutation
actions: {
increment (state) {
state.counter++
}
},
And here is the version with the action calling the mutation.
actions: {
incrementCounterUp () {
this.commit('increment')
}
},
#xaviert - I have tried starting the server over, and have also tried to see if an nuxt build followed by a firebase serve, to see if maybe that helped. It did not. My normal server start is 'npm run dev'. In hopes that you/anyone else may be able to find my mistake below is my full _id.vue component and also my nuxt.config.js file as maybe that's it. Its still pretty raw and could use a lot of refactoring so hope you can sort through it well enough.
_.id.vue
<template>
<div class="product">
<div class="product-image">
<div class="product-image-img">
<img v-bind:src="product.image_file" width="450px;"/>
</div>
</div>
<div class="product-details-wrapper">
<div class="product-details">
<h1>
<div v-if="this.$route.query.editPage">
<textarea #input="updateTextArea" ref="textarea2" v-model="product.item_name" type="text" />
</div>
<div v-else>{{product.item_name}}</div>
</h1>
<div class="product-description">
<div class="product-description-text" v-if="this.$route.query.editPage">
<textarea #input="updateTextArea" ref="textarea" v-model="product.description" type="text" />
</div>
<div class="product-description-text" v-else v-html="product.description"></div>
</div>
<p class="product-brand"><strong>Brand - </strong> {{product.brand_name}}</p>
<hr />
<div class="product-price">
<div v-if="this.$route.query.editPage">
<strong>Original Price - </strong>
<input v-model="product.msrp" type="text" />
</div>
<div v-else class="product-msrp">
<strong>Original Price - </strong>
<span class="strike">${{product.msrp}}</span>
</div>
<div v-if="this.$route.query.editPage">
<strong>Sale Price - </strong>
<input v-model="product.price" type="text" />
</div>
<div v-else class="product-sale-price">
<strong>Sale Price - </strong>
<span class="">${{product.price}}</span>
</div>
<div class="product-price">
Quantity x
<input #input="updateQuantity" v-model="quantity" min="1" class="" type="number" value="1" />
</div>
<button #click="inc">{{ counter }}</button>
</div>
</div>
</div>
<div v-if="this.$route.query.editPage" class="update-product"> <button #click="updateProduct(product)">Update</button></div>
<div class="footer">
<router-link to="/privacy-policy" target="_blank">Privacy</router-link> |
<router-link to="/terms" target="_blank">Terms</router-link>
</div>
</div>
</template>
<script>
// # is an alias to /src
import firebase from '#/services/fireinit'
import foo from '#/components/foo'
const db = firebase.firestore()
export default {
name: 'ProductPage',
head () {
return {
title: this.product.item_name
}
},
components: {
foo
},
data: function () {
return {
product: {},
image: '',
name: 'Checkout',
description: '',
currency: 'USD',
amount: '',
msrp: '',
quantity: 1
}
},
methods: {
inc () {
this.$store.dispatch('incrementCounterUp', true)
},
updateProduct: function (product) {
db.collection('products').doc(product.item_id).set(product)
.then(function () {
console.log('Document successfully written!')
})
.catch(function (error) {
console.error('Error writing document: ', error)
})
},
updateQuantity () {
this.product.msrp = (this.quantity * this.product.orgMsrp)
this.product.msrp = Math.round(100 * this.product.msrp) / 100
this.product.price = this.quantity * this.product.orgPrice
this.product.price = Math.round(100 * this.product.price) / 100
},
updateTextArea () {
this.$refs.textarea.style.minHeight = this.$refs.textarea.scrollHeight + 'px'
this.$refs.textarea2.style.minHeight = this.$refs.textarea2.scrollHeight + 'px'
}
},
async asyncData({app, params, error}) {
const ref = db.collection("products").doc(params.id)
let snap
let thisProduct = {}
try {
snap = await ref.get()
thisProduct = snap.data()
thisProduct.orgMsrp = thisProduct.msrp
thisProduct.orgPrice = thisProduct.price
} catch (e) {
// TODO: error handling
console.error(e)
}
return {
product: thisProduct
}
},
mounted () {
if(this.$refs.textarea) {
this.$refs.textarea.style.minHeight = this.$refs.textarea.scrollHeight + 'px'
this.$refs.textarea2.style.minHeight = this.$refs.textarea2.scrollHeight + 'px'
}
},
computed: {
counter () {
return this.$store.getters.counter
}
}
}
</script>
<style lang="less">
body {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin: 0
}
p{
margin-top: 1em;
margin-bottom: 1em;
}
html, body, #__nuxt, #__layout, .default, .product{
height: 100%;
}
.product {
justify-content: center;
display: flex;
max-width: 1150px;
margin: 0 auto;
flex-wrap: wrap;
align-items: center;
padding: 0 24px;
&-price{
input {
border: 1px solid;
padding: 0 .3em;
text-align: center;
width: 50px;
}
}
}
.product-details{
width: 100%;
textarea {
width: 100%;
font-size: inherit;
color: inherit;
font-family: inherit;
font-weight: inherit;
height: initial;
resize: none;
background-color: transparent;
border: none;
}
h1{
font-size: 1.9rem;
margin: 15px 0 20px;
}
hr{
width: 50%;
margin: .5rem 0px;
}
p{
}
}
.product-description-text{
margin: 10px 0;
}
.product-image, .product-details-wrapper{
align-items: center;
display: flex;
justify-content: center;
}
.product-details-wrapper{
flex: 0 1 535px;
}
.product-image{
flex: 0 1 535px;
img{
width: 100%;
}
}
.product-price{
.strike{
text-decoration: line-through;
}
button{
display: flex;
width: 150px;
height: 50px;
border-radius: 5px;
justify-content: center;
font-size: 24px;
margin-top: 20px;
&:hover{
cursor: pointer;
background-color: #f1f1f1;
box-shadow: 3px 3px 11px -1px rgba(0, 0, 0, 0.48);
}
}
}
.product-sale-price{
color: #f30000;
}
.footer {
flex: 1 1 100%;
text-align: center;
color: #ccc;
margin-top: 25px;
padding: 15px;
a {
color: #ccc;
text-decoration: none;
&:hover{
text-decoration: underline;
}
}
}
.update-product{
position: absolute;
top: 0;
text-align: center;
}
</style>
nuxt.confgs.js
const pkg = require('./package')
const { STRIPE_TOKEN } = process.env;
module.exports = {
vue: {
config: {
productionTip: false,
devtools: true
}
},
buildDir: './functions/nuxt',
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: pkg.name,
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: pkg.description }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
],
/*
** Plugins to load before mounting the App
*/
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://github.com/nuxt-community/axios-module#usage
'#nuxtjs/axios',
'nuxt-stripe-module'
],
stripe: {
version: 'v3',
publishableKey: 'pk_test_XXX',
},
/*
** Axios module configuration
*/
axios: {
// See https://github.com/nuxt-community/axios-module#options
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
publicPath: '/public/',
vendor: [],
extractCSS: true,
bable: {
presets: [
'es2015',
'stage-8'
],
plugins: [
['transform-runtime', {
'polyfill': true,
'regenerator': true
}]
]
},
extend (config, { isDev }) {
if (isDev && process.client) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/
})
}
},
router: {
middleware: 'router-auth'
}
},
plugins: [
{ src: '~/plugins/fireauth', ssr: true }
]
}
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const createStore = () => {
return new Vuex.Store({
state: () => ({
counter: 0
}),
actions: {
incrementCounterUp () {
this.commit('increment')
}
},
getters: {
counter: state => state.counter
},
mutations: {
increment (state) {
state.counter++
}
}
})
}
export default createStore
At the end of the day, I was not able to identify exactly what in my application was the error.
I assume that my during my initialization, configuration or development I touched something that should not have been touched, installed something that should not have be installed, messed with a package I should not have or was to bold in my nuxt.config.js changes.
I created a new nuxt app following the same install instructions. https://nuxtjs.org/guide/installation/
Moved the above _id.vue component exactly as it is and once I got dependencies updated it worked perfectly as seen in the image below.
Thank you very much #Boussadjra Brahim, #xaviert, #Andrew1325 for you assistance.

VueJS - onclick to make active on new Array entry not working

https://codepen.io/donnieberry97/pen/GGKQRN
var demo = new Vue({
el: '#main',
data: {
services: [
{
name: 'Item 1',
price: 200,
active: true
},
{
name: 'Item 2',
price: 500,
active: false
},
{
name: 'Item 3',
price: 700,
active: false
}
]
},
methods: {
addItem: function() {
var newItem= {
name:this.name,
price:this.price
};
this.services.push(newItem);
this.name="";
this.price="";
toggleActive();
},
toggleActive: function(f) {
f.active = !f.active;
},
total: function(){
var total=0;
this.services.forEach(function(f){
if(f.active){
total+=f.price;
}
});
return total;
}
}
});
When you use the input to add a new entry to the services array, upon clicking it afterwards, the active tag does not get applied to the new entry. It should turn blue and add to the total price but only the hover state works.
I've modified you code at method 'addItem' and use computed property total instead total method,have a look:
var demo = new Vue({
el: '#main',
data: {
services: [
{
name: 'Item 1',
price: 200,
active: true
},
{
name: 'Item 2',
price: 500,
active: false
},
{
name: 'Item 3',
price: 700,
active: false
}
]
},
computed: {
total () {
return this.services.reduce((last,item)=>last + parseInt(item.price) * item.active,0)
}
},
methods: {
addItem: function() {
var newItem= {
name:this.name,
price:this.price,
active: true
};
this.services.push(newItem);
this.name="";
this.price="";
},
toggleActive: function(f) {
f.active = !f.active;
}
}
});
* {
padding: 0;
margin: 0;
}
body{
font-family: 'Roboto', sans-serif !important;
}
h3 {
text-align:center;
padding: 2em 0em;
}
h5 {
padding: 1.5em 0.5em;;
box-sizing:border-box;
}
.container {
width:600px;
margin: 0 auto;
}
ul {
list-style:none;
}
li {
color:black;
border:1px solid #eeeeee;
padding:0.5em;
border-left: 5px solid #2196F3;
height:30px;
line-height:30px;
transition: 0.4s ease;
}
.active {
background-color:#2196F3;
color:white;
transition: 0.3s;
transition: 0.4s ease;
}
.active:hover {
background-color:#2196F3;
}
li:hover {
background-color:#82c4f8;
transition: 0.4s ease;
cursor:pointer;
}
span {
float:right;
}
#main {
box-shadow: 0 19px 38px rgba(0,0,0,0.0), 0 6px 12px rgba(0,0,0,0.22)
}
.text-center {
text-align:center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js"></script>
<div class="container">
<div id="main">
<div class="header"><h3>Click the services you wish to have:</div>
<ul>
<li class="group-item" v-for="service in services" v-on:click="toggleActive(service)" v-bind:class="{'active': service.active}">{{service.name}} <span>{{service.price | currency}}</span></li>
</ul>
<h5>Total is: {{total | currency}}</h5>
<input type="text" v-model="name" placeholder="name">
<input type="text" v-model="price" placeholder="price">
<button v-on:click="addItem()">Add Item</button>
</div>
</div>
The main issue that you're running into is you're calling an undefined function and not passing a parameter into your toggleActive function.
Since toggleActive is a Vue method, you'll need to use this to reference it correctly and use the function from your Vue instance; once that problem is fixed, you'll need to pass in the item that you're wanting to toggle, because the way that function is written it requires a parameter to update active status.
Here's how you could update your addItem function to get it working:
addItem: function() {
var newItem= {
name:this.name,
price:this.price,
active: false,
};
this.services.push(newItem);
this.name="";
this.price="";
this.toggleActive(this.services[this.services.length - 1]);
},
Also notice that I added the active property during item creation so that Vue treats this as a reactive property. Otherwise, your item will be stuck in the active state (after toggling it) and cannot become inactive on click. You could change this to just be active: true during creation (and remove the call to make it active completely) if all new items are supposed to be active on creation. I didn't do that, though, as I wanted to show how to fix the call to toggleActive.
You can view a forked and updated Codepen here if you'd like to see the code in a fully working state.

Vuejs - List transition via transition-group - parent container not animating smoothly

So I have a list that I am rendering with v-for, and I am also using transition-group to animate adding and removing items from this list. My problem is that while I can animate the adding/removing of list items, the container surrounding my entire list isn't smoothly transitioning its height. I'm wondering how I can fix this.
Here is an example with a 'Run code snippet' at the end:
var vm = new Vue({
el: '#vue-instance',
data: {
inventory: [
{name: 'Air', price: 1000, id:"name0"},
{name: 'Pro', price: 1800, id:"name1"},
{name: 'W530', price: 1400, id:"name2"},
{name: 'One', price: 300, id:"name3"}
]
},
methods: {
addItem() {
this.inventory.push({
name: 'Acer',
price: 700,
id: 'name4'
});
},
removeItem(index) {
this.inventory.splice(index, 1);
this.inventory.forEach((item, index) => {
item.id = `name${index}`;
});
}
}
});
.container {
background-color: green;
}
.list-enter {
opacity: 0;
}
.list-enter-active {
transition: all 2s;
height: 100%;
animation: slide-in 2s ease-out forwards;
}
.list-leave-to {
}
.list-leave-active{
transition: all 2s;
opacity: 0;
animation: slide-out 2s ease-out forwards;
}
.list-move {
transition: transform 2s;
}
#keyframes slide-in {
from {
transform: translateY(-100px);
}
to {
transform: translateY(0);
}
}
#keyframes slide-out {
from {
transform: translateY(0);
}
to {
transform: translateX(30px);
}
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="vue-instance">
<div class="container">
<ul>
<transition-group name="list">
<div v-for="(item, index) in inventory" :key="item.name">
<label :for="item.id">Hello</label>
<input :id="item.id">
<button #click="removeItem(index)">
Remove item
</button>
<button #click="addItem">
Add item
</button>
</div>
</transition-group>
</ul>
</div>
</div>
It can be done by adding a height value to the slide out animation
#keyframes slide-out {
from {
transform: translateY(0);
height: 10px;
}
to {
transform: translateY(-30px);
height: 0;
}
}
https://codepen.io/jacobgoh101/pen/EEvNzB?editors=0100