I've a page with 4 times the same vue component.
In HMTL:
<div class="column">
<my-comp url="http://example.com/?q=today" :unique-id="1" text="Random text"
locale="en"
statistics-type="count_today"
progress-color="#DA4169"></my-comp>
</div>
<div class="column">
<my-comp url="http://example.com/?q=yesterday" :unique-id="2" text="Random text"
locale="en"
statistics-type="count_yesterday"
progress-color="#DA4169"></my-comp>
</div>
In my vue component data is fetched from the url and displayed in the component.
<template>
<div :key="uniqueId">
<!-- more HTML -->
</div>
</template>
I'm using the key attribute to prevent the components from re-using.
Unfortunately the data from the first component always returns to the default values, when the second component loads its data.
Example after loading all the data:
First my-comp:
amount: 0 <- default
capacity: 0 <- default
Second my-comp:
amount: 9
capacity: 15
EDIT
<template>
<div :key="uniqueId" class="tile">
<p class="tile-title">{{ amount }}</p>
<p class="tile-text text-muted">{{ text }}</p>
<div class="progress" v-if="showProgress">
<div class="progress-bar" role="progressbar"
:aria-valuenow="progress"
aria-valuemax="100"
aria-valuemin="0"
v-bind:style="{ backgroundColor: progressColor, width: progress+'%' }">
</div>
</div>
</div>
</template>
<script>
export default {
props:{
locale: {
type: String,
required: true,
},
text: {
type: String,
required: true
},
progressColor: {
type: String,
required: true
},
url: {
type: String,
required: true
},
statisticsType: {
type: String,
required: true
},
uniqueId:{
type: Number,
required: true
}
},
data: function() {
return {
amount: 0,
total: 0,
showProgress: true,
}
},
computed: {
progress: function(){
return (this.amount/this.total)*100;
}
},
created() {
this.requestStatistics();
},
methods: {
requestStatistics: function(){
self = this;
axios.get(self.url,{
params: {
what: self.statisticsType
}
}).then(function(response){
self.showData(response.data);
}).catch(function(error){
console.log(error);
});
},
showData: function(data) {
if(typeof(data) === 'number'){
this.amount = data;
this.showProgress = false;
}else{
this.amount = data.count;
this.total = data.capacity;
this.showProgress = true;
}
}
}
}
</script>
You need to use v-bind instead of simple html attribute so that :key binding would be respected:
:uniqueId="1"
Related
This is the data I get and I what to be able to replace the data content with readable text:
From parent:
data: [
{ name: 'discounts_offers', type: 'EMAIL', consent: true },
{ name: 'newsletter', type: 'EMAIL', consent: true },
{ name: 'product_upgrade', type: 'EMAIL', consent: true },
{ name: 'sms_offer', type: 'SMS', consent: true },
{ name: 'post_offer', type: 'POST', consent: true }
]
This is my included component
<CommunicationPreference
v-for="(communication, index) in data"
:key="index"
:communication="communication"
/>
Then the communicationPreference.vue:
<template>
<section>
{{ communication.name }} //This should be Newsletters etc
</section>
</template>
<script>
export default {
props: {
communication: {
type: Object,
default: null,
},
},
data() {
return {}
},
computed: {},
}
</script>
Then what I would like to do is if {{ communication.name }} equals 'discounts_offers' to use the text "Discounts and offers" like the images attached. Any solutions for the best approach to this?
You can use computed property for scenario like this. Something like this.
App.vue
<template>
<CommunicationPreference
v-for="(communication, index) in preferences"
:key="index"
:type="communication.type"
:name="communication.name"
v-model:consent="preferences[index].consent"
/>
</template>
<script>
import CommunicationPreference from "./components/CommunicationPreference.vue";
export default {
name: "App",
components: {
CommunicationPreference,
},
data() {
return {
preferences: [
{ name: "discounts_offers", type: "EMAIL", consent: true },
{ name: "newsletter", type: "EMAIL", consent: true },
{ name: "product_upgrade", type: "EMAIL", consent: true },
{ name: "sms_offer", type: "SMS", consent: true },
{ name: "post_offer", type: "POST", consent: true },
],
};
},
};
</script>
and in CommunicationPreference.vue
<template>
<div>
<label
><input
type="checkbox"
name="preference"
:value="consent"
:checked="consent === true"
#change="$emit('update:consent', $event.target.checked)"
/>{{ label }}</label
>
</div>
</template>
<script>
export default {
name: "CommunicationPreference",
props: {
type: String,
name: String,
consent: Boolean,
},
computed: {
label() {
if (this.name === "newsletter") {
return "Newsletters";
}
if (this.name === "discounts_offers") {
return "Discount and offers";
}
if (this.name === "product_upgrade") {
return "Upgrade recommendations";
}
return this.name;
},
},
};
</script>
Something like that....not tested though.
I have an input component in vue and I gave the type as a date.
So as you see, the black icon is the default for html. And what I am trying to achieve, first I want to click whole input field to select the date, instead of only clicking the black icon. And also, I want to remove the black icon.
So here is my input component in vue:
<template>
<div>
<div v-bind:class="{row: rowStyle, 'form-group': !smallSize}">
<label v-if="label" for=inputName v-bind:class="labelClass" :style="labelStyle">{{ label }}</label>
<div class="input-group" v-bind:class="inputColumnAmount">
<div v-if="inputPrefix" class="input-group-prepend">
<span v-html="inputPrefix"/>
</div>
<input
v-if="inputType == 'text' || inputType == 'email' || inputType == 'password' || inputType == 'date'"
:type="inputType"
class="form-control"
v-bind:class="inputClasses"
v-on:focusout="$emit('value', $event.target.value)"
:id="inputName"
:placeholder="placeholder"
:value="value"
:pattern="pattern"
:maxlength="maxlength"
:disabled="disabled">
<div v-if="inputSuffix" class="input-group-append">
<span v-html="inputSuffix"/>
</div>
<div v-if="icon" class="input-group-append">
<div class="input-group-text"><i :class="icon"></i></div>
</div>
</div>
</div>
</div>
</template>
<script>
import {v4 as uuidv4} from 'uuid';
import GENERAL_COMPONENT_CONSTANTS from "../constants/GeneralComponentConstants";
export default {
props: {
label: { type: String, default: '' },
error: { type: String, default: '' },
inputType: { type: String, default: 'text' },
componentStyle: { type: String, default: GENERAL_COMPONENT_CONSTANTS.componentStyle.Row },
inputPrefix: { type: String, default: '' },
inputSuffix: { type: String, default: '' },
icon: { type: String, default: '' },
labelColumns: { type: Number | String, default: 3 },
placeholder: { type: String, default: "" },
value: { type: String | Number, default: "" },
pattern: { type: String, default: "" },
maxlength: { type: String, default: "150" },
disabled: { type: Boolean, default: false },
smallSize: { type: Boolean, default: false },
},
data() {
return {
inputName: "input-" + uuidv4(),
}
},
computed: {
rowStyle: function() {
return this.componentStyle == GENERAL_COMPONENT_CONSTANTS.componentStyle.Row;
},
labelClass: function() {
let labelClass = "";
if (this.rowStyle) {
labelClass += 'col-form-label ';
labelClass += this.labelColumnAmount;
}
return labelClass;
},
labelColumnAmount: function () {
return "col-sm-" + this.labelColumns;
},
inputColumnAmount: function () {
if (!this.rowStyle) {
return '';
} else if (this.label) {
return "col-sm-" + (12 - this.labelColumns);
} else {
return "col-sm-12";
}
},
labelStyle() {
if (this.disabled) {
return "color: #6c757d;";
} else {
return "";
}
},
inputClasses() {
return {
'is-invalid': this.error,
'form-control-sm': this.smallSize,
};
}
},
}
</script>
And here, how I am using it:
<cc-input-component
label="Create from"
labelColumns=4
inputType="date"
:value="newAvailabilitySetting.from_date"
v-on:value="newAvailabilitySetting.from_date = $event"
icon="fa fa-calendar"/>
Any recommendations will be appreciated. Thanks.
First you should set a class input-component and then you can hide default icon
input[type="date"]::-webkit-inner-spin-button,
input[type="date"]::-webkit-calendar-picker-indicator {
display: none;
-webkit-appearance: none;
}
Add icon-calendar slot with an empty <svg></svg> tag inside.
<date-picker>
<template v-slot:icon-calendar>
<svg></svg>
</template>
</date-picker>
https://github.com/mengxiong10/vue2-datepicker/issues/722#issuecomment-1301691106
I want to sum every existing and added value to a total sum, which I can always see.
I'm kinda struggling with how to include my existing values and those I want to add afterward.
In IncomeList I have two inputs. One for the incomeReason and with the other I can add the value (newIncomeValue). Every Item in my List contains an id. With sumValue I want to display the total amount, but I'm not sure how to do it
It looks like this:
IncomeItem.vue
<template>
<div class="income-item">
<div class="income-item-left">
<div v-if="!editing" #dblclick="editincome" class="income-item-label"
:class="{ completed : completed }">{{ title }}</div>
</div>
<div class="income-item-right"> {{ value }} </div>
<div class="remove-item" #click="removeincome(income.id)">
×
</div>
</div>
</template>
<script>
export default {
name: 'income-item',
props: {
income: {
type: Object,
required: true,
},
checkAll: {
type: Boolean,
required: true,
}
},
data() {
return {
'id': this.income.id,
'title': this.income.title,
'value': this.income.value,
'completed': this.income.completed,
'editing': this.income.editing,
'beforeEditCache': '',
}
},
watch: {
checkAll() {
// if (this.checkAll) {
// this.completed = true
// } else {
// this.completed = this.income.completed
// }
this.completed = this.checkAll ? true : this.income.completed
}
},
directives: {
focus: {
inserted: function (el) {
el.focus()
}
}
},
methods: {
removeincome(id) {
this.$emit('removedincome', id)
},
}
}
</script>
IncomeList.vue
<template>
<div>
<input type="text" class="income-input" placeholder="What needs to be done" v-model="newIncome" #keyup.enter="addincome">
<input type="text" class="value-input" placeholder="€" v-model="newIncomeValue" #keyup.enter="addValue">
<transition-group name="fade" enter-active-class="animated fadeInUp" leave-active-class="animated fadeOutDown">
<income-item v-for="income in incomesFiltered" :key="income.id" :income="income" :checkAll="!anyRemaining"
#removedIncome="removeincome" #finishedEdit="finishedEdit">
</income-item>
</transition-group>
<div class="extra-container">
<div><label><input type="checkbox" :checked="!anyRemaining" #change="checkAllincomes"> Check All</label></div>
<div>{{ remaining }} items left</div>
</div>
<div class="sum-container">
<div><label> Einkommen: </label></div>
<div>{{ sumValue }} €</div>
</div>
</div>
</template>
<script>
import IncomeItem from './IncomeItem'
export default {
name: 'income-list',
components: {
IncomeItem,
},
data () {
return {
newIncome: '',
newIncomeValue: '',
idForincome: 3,
incomes: [
{
'id': 1,
'title': 'Finish Vue Screencast',
'value': 300,
'completed': false,
'editing': false,
},
{
'id': 2,
'title': 'Take over world',
'value': 315,
'completed': false,
'editing': false,
},
]
}
},
computed: {
remaining() {
return this.incomes.filter(income => !income.completed).length
},
anyRemaining() {
return this.remaining != 0
},
incomesFiltered() {
return this.incomes
},
sumValue() {
var total = parseInt(document.getElementsByClassName('newIncomeValue').value)
return total;
},
},
methods: {
addincome() {
if (this.newIncome.trim().length == 0) {
return
}
this.incomes.push({
id: this.idForincome,
title: this.newIncome,
value: this.newIncomeValue,
completed: false,
})
this.newIncome = ''
this.newIncomeValue = ''
this.this.idForincome++
},
removeincome(id) {
const index = this.incomes.findIndex((item) => item.id == id)
this.incomes.splice(index, 1)
},
checkAllincomes() {
this.incomes.forEach((income) => income.completed = event.target.checked)
},
clearCompleted() {
this.incomes = this.incomes.filter(income => !income.completed)
},
finishedEdit(data) {
const index = this.incomes.findIndex((item) => item.id == data.id)
this.incomes.splice(index, 1, data)
},
//Same for Value
addValue() {
if (this.newIncomeValue.trim().length == 0) {
return
}
this.incomes.push({
id: this.idForincome,
title: this.newIncome,
value: this.newIncomeValue,
completed: false,
})
this.newIncome = ''
this.newIncomeValue = ''
this.this.idForincome++
},
}
}
</script>
If you want to sum the value property of your incomesFiltered, you can use reduce in your computed:
sumValue() {
return this.incomesFiltered.reduce((a, c) => a + c.value, 0);
}
I'm trying to attach a simple character counter to an input element but the second I display it back to the user, the input breaks in that I'm unable to enter any additional characters in the input box.
<template>
<div>
<label class="label" :class="{ 'label-large' : large }" v-if="label">
{{ label }} <sup class="is-required" v-if="isRequired">Req</sup>
</label>
<input class="input-control" :class="{ 'input-large' : large }" :maxlength="maxLength" :placeholder="placeholderText" ref="input" :value="text" #change="formatValue($event.target.value)" #keyup="countCharacters($event.target.value)" />
<div class="flex text-x-small-regular mt-2" :class="large ? 'px-4' : 'px-2'" v-if="maxLength || validationFailed">
<div class="validation-message">
<template v-if="validationFailed">{{ validationMessage }}</template>
</div>
<div class="character-count" v-if="maxLength">
<span :class="characterCountWarningStyle">{{ characterCount }}</span> / {{ maxLength }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
isRequired: {
default: false,
required: false,
type: Boolean
},
label: {
required: false,
type: String
},
large: {
default: false,
required: false,
type: Boolean,
},
maxLength: {
required: false,
type: Number
},
placeholder: {
required: false,
type: String
},
text: {
required: false,
type: String
},
validationMessage: {
default: "Required field.",
required: false,
type: String
}
},
data() {
return {
characterCount: 0,
validationFailed: false,
value: undefined
}
},
computed: {
characterCountWarningStyle() {
return "" // Simplified.
},
placeholderText() {
return "" // Simplified.
}
},
methods: {
countCharacters(value) {
// Works:
console.log(value.length);
// Breaks form input: this.characterCount = value.length;
},
formatValue(value) {
this.validationFailed = false;
if (value) value = value.trim();
this.validate(value);
},
validate(value) {
if (this.isRequired && !value) {
this.validationFailed = true;
}
this.$emit('update', value);
}
}
}
</script>
To summarize the code above, I'm doing some basic cleansing on change, and am looking to trigger a character count on key up. What am I missing?
The characterCount update in the keyup handler is triggering a rerender of the entire component in order to render the new value of the characterCount string interpolation in the template. The rendering includes the <input>, whose value is bound to text. If text is an empty string or null, the <input> is effectively cleared on keyup.
To resolve the issue, use a local copy of the text prop that can be modified, and bind it to the <input>'s v-model.
Create a data property (named "value"), and a watcher on the text prop that copies text into value:
export default {
props: {
text: {/*...*/},
},
data() {
return {
value: ''
}
},
watch: {
text(newText) {
this.value = newText
}
},
}
Use the new value property as the <input>'s v-model:
<input v-model="value">
Remove the keyup handler and the characterCount data property, and instead use a computed prop that returns the length of value:
export default {
computed: {
characterCount() {
return this.value.length
}
},
}
You need to replace the :value with v-model for a two way data binding, and instead of mutating the text prop directly, add a local property (e.g. localText). I'd also use a computed property to calculate the length instead of an event for better readability. So, something like this:
<template>
<div>
<label class="label" :class="{ 'label-large' : large }" v-if="label">
{{ label }} <sup class="is-required" v-if="isRequired">Req</sup>
</label>
<input class="input-control" :class="{ 'input-large' : large }" :maxlength="maxLength" :placeholder="placeholderText" ref="input" v-model="localText" #change="formatValue($event.target.value)" />
<div class="flex text-x-small-regular mt-2" :class="large ? 'px-4' : 'px-2'" v-if="maxLength || validationFailed">
<div class="validation-message">
<template v-if="validationFailed">{{ validationMessage }}</template>
</div>
<div class="character-count" v-if="maxLength">
<span :class="characterCountWarningStyle">{{ characterCount }}</span> / {{ maxLength }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
isRequired: {
default: false,
required: false,
type: Boolean
},
label: {
required: false,
type: String
},
large: {
default: false,
required: false,
type: Boolean,
},
maxLength: {
required: false,
default: 10,
type: Number
},
placeholder: {
required: false,
type: String
},
text: {
required: false,
type: String
},
validationMessage: {
default: "Required field.",
required: false,
type: String
}
},
data() {
return {
localText: this.text || '',
validationFailed: false,
value: undefined
}
},
computed: {
characterCount() {
return this.localText.length;
},
characterCountWarningStyle() {
return "" // Simplified.
},
placeholderText() {
return "" // Simplified.
}
},
methods: {
formatValue(value) {
this.validationFailed = false;
if (value) value = value.trim();
this.validate(value);
},
validate(value) {
if (this.isRequired && !value) {
this.validationFailed = true;
}
this.$emit('update', value);
}
}
}
</script>
This is a simple shopping Cart
Actually, when we clic on items, it updated the total number in the Cart.
But i want now to make a better Cart: showing each items in the list. Like when we chose 2 croissiants, it adds 2 croissiants in the Cart.
My problem is, in this course, i didn't really learn how to make a Mutation dependings of the ID of the item. I'm looking for the syntax witch is sending the ID of the item where we clicked, to the store.
Here my my 3 files : (Store's "index", "Home" and it's children "MenuItem")
Store :
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
restaurantName: 'Cafe with A Vue',
shoppingCart: 0,
croissiantNumber: 0,
baguetteNumber: 0,
eclairNumber: 0,
simpleMenu: [
{
id: 1,
name: 'Crossiant',
image: {
source: '/images/crossiant.jp',
alt: 'A crossiant'
},
inStock: true,
quantity: 1,
price: 2.99
},
{
id: 2,
name: 'French Baguette',
image: {
source: '/images/french-baguette.jpe',
alt: 'Four French Baguettes'
},
inStock: true,
quantity: 1,
price: 3.99
},
{
id: 3,
name: 'Éclair',
image: {
source: '/images/eclair.jp',
alt: 'Chocolate Éclair'
},
inStock: false,
quantity: 1,
price: 4.99
}
]
},
getters: {
copyright: state => {
const currentYear = new Date().getFullYear()
return `Copyright ${state.restaurantName} ${currentYear}`
}
},
mutations: {
ADD_ITEMS_TO_SHOPPING_CART(state, amount) {
state.shoppingCart += amount
}
},
actions: {
updateShoppingCart({ commit }, amount) {
commit('ADD_ITEMS_TO_SHOPPING_CART', amount),
commit('ADD_ITEM_TO_SHOPPING_CART', amount)
}
},
modules: {}
})
Home :
<template>
<div>
<h1>{{ restaurantName }}</h1>
<p class="description">
Welcome to {{ restaurantName }}! We are known for our freshly baked bread
and french pastries! Give you morning a warm start or treat yourself in
the middle of the day. Our butter is imported from local farmers in
France. Once you take your first bite, you will see why everyone can't get
enough!
</p>
<section class="menu">
<h2>Menu</h2>
<MenuItem
v-for="item in simpleMenu"
:name="item.name"
:image="item.image"
:price="item.price"
:quantity="item.quantity"
:inStock="item.inStock"
:key="item.name"
:id="item.id"
/>
</section>
<div class="shopping-cart">
<h2>Shopping Cart: {{ shoppingCart }} items</h2>
<h2>Croissiant: {{ croissiantNumber }} items</h2>
<h2 v-if="baguetteNumber">French Baguette: {{ baguetteNumber }} items</h2>
<h2 v-if="eclairNumber">Eclair: {{ eclairNumber }}items</h2>
</div>
<footer class="footer">
<p>{{ copyright }}</p>
</footer>
</div>
</template>
<script>
import MenuItem from '../components/MenuItem'
import { mapGetters, mapState } from 'vuex'
export default {
name: 'Home',
components: {
MenuItem
},
computed: {
...mapGetters({
copyright: 'copyright'
}),
...mapState({
restaurantName: 'restaurantName',
shoppingCart: 'shoppingCart',
croissiantNumber: 'croissiantNumber',
baguetteNumber: 'baguetteNumber',
eclairNumber: 'eclairNumber',
simpleMenu: 'simpleMenu'
})
}
}
</script>
MenuItem :
<script>
import { mapActions } from 'vuex'
import BaseButton from './BaseButton.vue'
export default {
name: 'MenuItem',
components: {
BaseButton
},
props: {
image: {
type: Object,
required: true
},
inStock: {
type: Boolean,
required: true
},
name: {
type: String,
required: true
},
price: {
type: Number,
required: true
},
quantity: {
type: Number,
defaut: 1
}
},
data() {
return {
onSale: false
}
},
computed: {
generatedPrice() {
if (this.onSale) {
return (this.price * 0.9).toFixed(2)
} else {
return this.price
}
}
},
methods: {
...mapActions(['updateShoppingCart'])
},
beforeMount() {
const today = new Date().getDate()
if (today % 2 === 0) {
this.onSale = true
}
}
}
</script>
<template>
<div class="menu-item">
<img class="menu-item__image" :src="image.source" :alt="image.alt" />
<div>
<h3>{{ name }}</h3>
<p>Price: {{ generatedPrice }} <span v-if="onSale">(10% off!)</span></p>
<p v-if="inStock">In Stock</p>
<p v-else>Out of Stock</p>
<div>
<label for="add-item-quantity">Quantity: {{ quantity }}</label>
<input v-model.number="quantity" id="add-item-quantity" type="number" />
<BaseButton #click="updateShoppingCart(quantity, id)" class="test">
Add to shopping cart
</BaseButton>
</div>
</div>
</div>
</template>
So i want to update this value for example :
Croissiant: 0 items
Website preview
Thanks.
You have to pass either object or array to your action to handle multiple parameters.
#click="updateShoppingCart({quantity, id})"
and then in store:
...
actions: {
updateShoppingCart({ commit }, {quantity, id}) {
commit('ADD_ITEMS_TO_SHOPPING_CART', amount),
//here you have id that you can map to item type
}
},
I would suggest having item type that is constant in simpleMenu and not depending on IDs.