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>
Related
I have made a child component - Dropdown based on Devextreme DxSelectBox.
I set in parent component v-model as attribute and forward to it an variable as ref, which is set to initial selected valueconst item = ref({ value: "option 1" }), but the DxSelectBox is empty when loading the project.
Child component - Dropdown is emitting the right selected option, but unfortunately the initial selected value is not set.
Code for Parent App.vue component is:
<template>
<template>
<Dropdown :items="items" v-model:option="item" />
<div class="emitting">Emitting: {{ item }}</div>
</template>
<script>
import { ref } from "vue";
import Dropdown from "./components/Dropdown.vue";
export default {
name: "App",
components: {
Dropdown: Dropdown,
},
setup(props, context) {
const emitValue = (e) => {
context.emit("update:option", e.value);
};
const items = ref([
{
value: "option 1",
},
{
value: "option 2",
},
{
value: "option 3",
},
]);
const item = ref({
value: "option 1",
});
return {
items,
item,
emitValue,
};
},
};
</script>
<style>
#import "https://cdn3.devexpress.com/jslib/18.2.8/css/dx.light.css";
#import "https://cdn3.devexpress.com/jslib/18.2.8/css/dx.common.css";
</style>
and code for Dropdown.vue component is:
<template>
<template>
<div class="dx-field">
<DxSelectBox
:data-source="items"
:value="option"
#valueChanged="emitValue"
:width="width"
:height="height"
:drop-down-options="{ width: width }"
display-expr="value"
:disabled="disabled"
:read-only="readOnly"
:hint="hint"
:placeholder="placeholder"
/>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, watch, ref } from "vue";
import DxSelectBox from "devextreme-vue/select-box";
export default defineComponent({
name: "DpmDropdown",
components: {
DxSelectBox,
},
emits: ["update:option"],
props: {
label: { type: String, default: "" },
showLabel: { type: Boolean, default: true },
headingTooltip: { type: String, default: "" },
items: { type: Array, default: () => [] },
option: { type: Object, default: () => ({}) },
width: { type: [Number, String], default: "100%" },
height: { type: String, default: "40px" },
icon: { type: String, default: "" },
hint: { type: String, default: "" },
disabled: { type: Boolean, default: false },
readOnly: { type: Boolean, default: false },
placeholder: { type: String, default: "" },
},
setup(props, context) {
const emitValue = (e: any) => {
context.emit("update:option", e.value);
};
return {
emitValue,
};
},
});
</script>
The link for sandbox project is here.
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'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"