Vue: Getting a default value for v-select - vue.js

I have a list of cartItems and I'm generating a dropdown for each one. Each cartItem has a field called orig_quantity that I'm looking to set the default value of the drop down to. I tried doing :value="item.orig_quantity" but that doesn't seem to be doing it.
computed: {
quantityOptions: function() {
return [1,2,3]
}
}
<div v-for="(item, index) in cartItems"
<div>{item.product_name}</div>
<v-select :options="quantityOptions"
v-on:change="updateQuantity($event,item)">
</v-select>
</div>

Sorry about that - I misunderstood your question at first.. I have updated my answer below.. This should be sufficient to get the idea across (the code stands to be cleaned - its 'pseudo' enough to get the idea across, though)..
In CodePen form, which I find easier to read:
https://codepen.io/oze4/pen/vMLggE
Vue.component("v-select", VueSelect.VueSelect);
new Vue({
el: "#app",
data: {
cartItems: [{
product_name: "Chair",
original_quantity: 7,
total_quantity: 9,
pending_quantity: null,
price: "$19.99"
},
{
product_name: "Couch",
original_quantity: 3,
total_quantity: 6,
pending_quantity: null,
price: "$29.99"
}
],
},
methods: {
getStock(cartItem) {
let ci = this.cartItems.find(i => {
return i.product_name === cartItem.product_name;
});
return [...Array(ci.total_quantity + 1).keys()].slice(1);
},
updateQty(cartItem) {
alert("this is where you would post");
let ci = this.cartItems.find(i => {
return i.product_name === cartItem.product_name;
});
ci.original_quantity = ci.pending_quantity;
}
}
});
h5,
h3 {
margin: 0px 0px 0px 0px;
}
.reminder {
color: red;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.10/dist/vue.js"></script>
<script src="https://unpkg.com/vue-select#2.6.4/dist/vue-select.js"></script>
<div id="app">
<h4>Cart Items:</h4>
<div v-for="(item, index) in cartItems">
<h3>{{ item.product_name }}</h3>
<h5>Price: {{ item.price }} | InBasket: {{ item.original_quantity }}</h5>
<small>Change Quantity: </small>
<v-select :value="item.original_quantity" :options="getStock(item)" #change="item.pending_quantity = $event"></v-select>
<button #click="updateQty(item)" type="button">Update {{ item.product_name }} Qty</button><small class="reminder">*update after changing quantity</small>
<br/>
<hr/>
</div>
</div>

You should be able to add a v-model attribute to the select.
<div v-for="(item, index) in cartItems"
<v-select v-model="item.orig_quantity" :options="quantityOptions"
v-on:change="updateQuantity($event,item)">
</v-select>
</div>

Related

Cannot get computed property (array)

Trying to get a 'displayImages' array as a computed property. Using a default 'selected' property = 0.
this.selected changes accordingly on mouseover and click events.
When trying to get the computed 'displayImages' it says:
"this.variations[this.selected] is undefined."
I'm using an api to get my product data and images.
<template>
<div id="product-page">
<v-card width="100%" class="product-card">
<div class="image-carousel">
<v-carousel height="100%" continuos hide-delimiters>
<v-carousel-item
v-for="(image, i) in displayImages"
:key="i"
:src="image"
>
</v-carousel-item>
</v-carousel>
</div>
<div class="details">
<h2>{{ this.title }}<br />Price: ${{ this.price }}</h2>
<p>{{ this.details }}</p>
<ul style="list-style: none; padding: 0">
<li
style="border: 1px solid red; width: auto"
v-for="(color, index) in variations"
:key="index"
#mouseover="updateProduct(index)"
#click="updateProduct(index)"
>
{{ color.color }}
</li>
</ul>
<div class="buttons">
<v-btn outlined rounded
>ADD TO CART<v-icon right>mdi-cart-plus</v-icon></v-btn
>
<router-link to="/shop">
<v-btn text outlined rounded> BACK TO SHOP</v-btn>
</router-link>
</div>
</div>
</v-card>
</div>
</template>
<script>
export default {
name: "Product",
props: ["APIurl"],
data: () => ({
title: "",
details: "",
price: "",
variations: [],
selected: 0,
}),
created() {
fetch(this.APIurl + "/products/" + this.$route.params.id)
.then((response) => response.json())
.then((data) => {
//console.log(data);
this.title = data.title;
this.details = data.details.toLowerCase();
this.price = data.price;
data.variations.forEach((element) => {
let imagesArray = element.photos.map(
(image) => this.APIurl + image.url
);
this.variations.push({
color: element.title,
images: imagesArray,
qty: element.qty,
productId: element.productId,
});
});
});
},
computed: {
displayImages() {
return this.variations[this.selected].images;
},
},
methods: {
updateProduct: function (index) {
this.selected = index;
console.log(index);
}
},
};
</script>
To properly expand on my comment, the reason why you are running into an error is because when the computed is being accessed in the template, this.variations is an empty array. It is only being populated asynchronously, so chances are, it is empty when VueJS attempts to use it when rendering the virtual DOM.
For that reason, accessing an item within it by index (given as this.selected) will return undefined. Therefore, attempting to access a property called images in the undefined object will return an error.
To fix this problem, all you need is to introduce a guard clause in your computed as such:
computed: {
displayImages() {
const variation = this.variations[this.selected];
// GUARD: If variation is falsy, return empty array
if (!variation) {
return [];
}
return variation.images;
},
}
Bonus tip: if you one day would consider using TypeScript, you can even simplify it as such... but that's a discussion for another day ;) for now, optional chaining and the nullish coalescing operator is only supported by bleeding edge versions of evergreen browsers.
computed: {
displayImages() {
return this.variations[this.selected]?.images ?? [];
},
}
For avoid this kind of error, you must to use the safe navigation property.
Remember, it's useful just when the app is loading.
Try something like that:
<script>
export default {
name: 'Product',
computed: {
displayImages() {
if (this.variations[this.selected]) {
return this.variations[this.selected].images;
}
return [];
},
},
};
</script>

How to create a numeric input component in Vue with limits that doesn't allow to type outside limits

I'm trying to create a numeric input component in Vue with min and max values that doesn't allow to type outside outside limits without success:
<template id="custom-input">
<div>
<input :value="value" type="number" #input="onInput">
</div>
</template>
<div id="app">
<div>
<span>Value: {{ value }}</span>
<custom-input v-model="value" :max-value="50"/>
</div>
</div>
Vue.component('custom-input', {
template: '#custom-input',
props: {
value: Number,
maxValue: Number
},
methods: {
onInput(event) {
const newValue = parseInt(event.target.value)
const clampedValue = Math.min(newValue, this.maxValue)
this.$emit('input', clampedValue)
}
}
})
new Vue({
el: "#app",
data: {
value: 5
}
})
Fiddle here: https://jsfiddle.net/8dzhy5bk/6/
In the previous example, the max value is set in 50. If I type 60 it's converted automatically to 50 inside the input, but if I type a third digit it allow to continue typing. The value passed to the parent is clamped, but I also need to limit the input so no more digits can be entered.
When the value of input is great than 10, it will always emit 10 to parent component, but the value keeps same (always=10) so it will not trigger reactvity.
One solution, always emit actual value (=parseInt(event.target.value)) first, then emit the max value (=Math.min(newValue, this.maxValue)) in vm.$nextTick()
Another solution is use this.$forceUpdate().
Below is the demo for $nextTick.
Vue.component('custom-input', {
template: '#custom-input',
props: {
value: Number,
maxValue: Number
},
methods: {
onInput(event) {
const newValue = parseInt(event.target.value)
const clampedValue = Math.min(newValue, this.maxValue)
this.$emit('input', newValue)
this.$nextTick(()=>{
this.$emit('input', clampedValue)
})
}
}
})
new Vue({
el: "#app",
data: {
value: 5
},
methods: {
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<template id="custom-input">
<div>
<input
:value="value"
type="number"
#input="onInput"
>
</div>
</template>
<div id="app">
<div>
<span>Value: {{ value }}</span>
<custom-input v-model="value" :max-value="10"/>
</div>
</div>
Below is the demo for vm.$forceUpdate.
Vue.component('custom-input', {
template: '#custom-input',
props: {
value: Number,
maxValue: Number
},
methods: {
onInput(event) {
const newValue = parseInt(event.target.value)
const clampedValue = Math.min(newValue, this.maxValue)
this.$emit('input', clampedValue)
this.$forceUpdate()
}
}
})
new Vue({
el: "#app",
data: {
value: 5
},
methods: {
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<template id="custom-input">
<div>
<input
:value="value"
type="number"
#input="onInput"
>
</div>
</template>
<div id="app">
<div>
<span>Value: {{ value }}</span>
<custom-input v-model="value" :max-value="10"/>
</div>
</div>

vue.js - Dynamically render items on scroll when visible

I have list of 100+ items and rendering takes too much time. I want to show just the once that are visible, and rest on scroll.
What's the best approach?
I have this snippet below, but the vue.set() isn't working.
var dbItems = [{name: 'New item'}, {name:'Another'}, {name:'Third'}];
var app = new Vue({
el: '#app',
data: {
// if I put items : dbItems, then for some reason the Vue.set() doesn't work!!
items : [],
},
methods: {
init: function () {
this.items = dbItems; // we add all items
},
makeItemVisible : function(id) {
console.log("Making visible #"+id);
this.items[id].show = 1;
Vue.set(this.items, id, this.items[id]);
}
}
});
app.init();
app.makeItemVisible(1); // this works
$(document).on('scroll', function(){
// function to show elements when visible
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div id="app" v-cloak>
<button v-on:click="makeItemVisible(0)">MAKE VISIBLE - This button doesn't work</button>
<div class="items" v-show="items.length">
<!-- I dont know why, but (key, item) had to be switched compared to VUE documentation! -->
<div v-for="(key, item) in items">
<div v-if="item.show" style="border:2px solid green;height:700px">
You can see me: {{ item.name }} | ID: {{ key }}
</div>
<div class="item-blank" data-id="{{ key }}" v-else style="border:2px solid red;height:700px">
{{ item.name }} invisible {{ key }}
</div>
</div>
</div>
</div>
Solved.
Edit: This Vue.js is only useable in Chrome... otherwise it is incredibly slow (Firefox is slowest), it works better when loading the whole document in HTML at once.
var dbItems = [{name: 'New item'}, {name:'Another'}, {name:'Third'}];
var app = new Vue({
el: '#app',
data: {
items : dbItems
},
methods: {
makeItemVisible : function(id) {
console.log("Making visible #"+id);
Vue.set(this.items[id], 'show', 1);
}
}
});
function isScrolledIntoView(elem)
{
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
var elemTop = $(elem).offset().top;
var elemBottom = elemTop + $(elem).height();
return (elemTop <= docViewBottom && elemTop >= docViewTop) || (elemBottom >= docViewTop && elemBottom <= docViewBottom);
}
var fn = function(){
$('.item-blank').each(function(){
if(isScrolledIntoView(this)) {
app.makeItemVisible($(this).attr('data-id'));
}
});
};
$(window).scroll(fn);
fn(); // because trigger() doesn't work
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app" v-cloak>
<div class="items" v-show="items.length">
<div v-for="(item, index) in items">
<div v-if="item.show" style="border:2px solid green;height:700px">
You can see me: {{ item.name }} | ID: {{ index }}
</div>
<div class="item-blank" :data-id="index" v-else style="border:2px solid red;height:700px;position:relative;">
{{ item.name }} invisible {{ index }}
</div>
</div>
</div>
</div>

deleting child component removes that component and all child components created after it

I've run into a problem when I delete a component any component created after it deleted and then recreated. Component in question gets deleted and child component created after it get deleted and then recreated.
Is there a reason why this is happening?
here is a video of it
Here is fiddle
fiddle code:
Vue.component('column-component', {
props: ["columnData", "uniqueId"],
mounted: function() {
console.log('mounting column: ' + this.uniqueId)
},
beforeDestroy: function() {
console.log('removing: ' + this.uniqueId)
},
template: `
<div style="float: left; padding: 10px; margin-right: 10px; border: 1px solid black;">aaa</div>`
})
Vue.component('row-component', {
props: ["rowData", "uniqueId"],
data: function data() {
return {
columns: [],
columnCount: 0
}
},
mounted: function() {
console.log('mounting row: ' + this.uniqueId)
},
methods: {
addColumn() {
console.log
var column = new Object()
column.uniqueId = this.uniqueId +'.col.'+ this.columnCount
this.columns.push(column)
this.columnCount = this.columnCount + 1
}
},
beforeDestroy: function() {
console.log('removing: ' + this.uniqueId)
},
template: `
<div>
<div style="margin: 10px; padding: 20px; background: rgba(0,0,0, 0.5);">
row component: {{rowData.text}}
<div class="column" v-for="(column, index) in columns">
<column-component column-data="abc" :uniqueId="column.uniqueId"></column-component>
</div>
<div style="clear: both;"></div>
<div style="margin-top: 35px;">
<button #click="addColumn()">add column</button>
</div>
</div>
</div>`
})
new Vue({
el: '#app',
template: `
<div>
<div v-for="(row, index) in rows">
<row-component :uniqueId="row.uniqueId" :row-data="row" :key="row.uniqueId"></row-component>
<button #click="deleteThisRow(index)">remove row</button>
</div>
<button #click="addRow()">add</button>
</div>
`,
data: {
rows: [],
rowCount: 0
},
mounted: function() {
this.addRow()
this.addRow()
this.addRow()
},
methods: {
addRow() {
var row = new Object()
row.uniqueId = 'row-' + this.rowCount
row.text = 'row-'+(this.rows.length)
this.rows.push(row)
this.rowCount = this.rowCount + 1
},
deleteThisRow: function(index) {
this.rows.splice(index, 1)
console.log(this.rows)
}
}
})
Updating entirely
Ok, I learned something today:
:key goes on the v-for element. A lot of the time, the v-for is on the component itself, so there's no distinction between putting the key on the component or the v-for element. You have a div with a v-for wrapping the component, and it makes a difference. It should be:
<div class="column" v-for="(column, index) in columns" :key="column.uniqueId">
<column-component column-data="abc" :uniqueId="column.uniqueId"></column-component>
</div>
and
<div v-for="(row, index) in rows" :key="row.uniqueId">
<row-component :uniqueId="row.uniqueId" :row-data="row"></row-component>
<button #click="deleteThisRow(index)">remove row</button>
</div>
Updated fiddle

Vue alternate classes in v-for

I have an array (history) that is being pushed two items every time a button is pressed. Those two items will need to have different css styles when printed.
HTML
<ul class="info">
<li :class="{ 'style-one' : toggle, 'style-two' : toggle }" v-for="item in history">{{item}}</li>
</ul>
JS (Vue)
methods: {
attack: function() {
this.history.unshift(this.playerDamaged);
this.history.unshift(this.monsterDamaged);
}
The problem with this is there is no way to change the truthiness of toggle during the loop. Is there a better way to approach this?
SOLUTION 1 :
You can use this code :
<ul class="info">
<li v-for="item in history" :key="item"
:class="{ 'style-one' : item.isPlayer, 'style-two' : !item.isPlayer }"
>
{{ item.text }}
</li>
</ul>
methods: {
attack: function() {
this.history.unshift({ text: this.playerDamaged, isPlayer: true });
this.history.unshift({ text: this.monsterDamaged, isPlayer: false });
}
UPDATED - SOLUTION 2 [No use of objects] :
You can use an other solution with no objects :
<ul class="info">
<li v-for="(item, index) in history" :key="item"
:class="'style-' + ((index % numberOfPlayers) + 1)"
>
{{ item }}
</li>
</ul>
//This part don't have to deal with Array of Objects :
methods: {
attack: function() {
this.history.unshift( this.playerDamaged );
this.history.unshift( this.monsterDamaged );
},
computed: {
numberOfPlayers: function() {
return 2;
}
}
If you want to add a player (ex: monster 2) you have to update the computed numberOfPlayers to 3 (or better : listOfPlayers.length if you have) and create a class ".style-3".
Code example :
new Vue({
el: "#app",
data: function() {
return {
myArray: ['player attack', 'monster attack','player attack', 'monster attack']
}
},
computed: {
numberOfPlayers: function() {
return 2;
}
}
});
.style-1 {
color: blue;
}
.style-2 {
color: red;
}
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<div v-for="(item, index) in myArray" :key="item"
:class="'style-' + ((index % numberOfPlayers) + 1)"
>
{{ item }}
</div>
</div>