I have the following Vue component that displays a save and cancel input group addon when the input changes. Can anyone explain why the keypress is lost from the input when the textChanged value changes and how to fix it, please?
JSFiddle
<div id="app">
<my-input></my-input>
</div>
<template id="my-template">
<div class="form-group">
<label>Test</label>
<div :class="{'input-group': textChanged}">
<input type="text" class="form-control" ref="inp"
#input="updateValue($event.target.value)" :value="value">
<div v-show="textChanged" class="input-group-addon">Save</div>
<div v-show="textChanged" class="input-group-addon" #click="cancel">Cancel</div>
</div>
</div>
</template>
new Vue({
el: '#app'
});
Vue.component('my-input', {
template: '#my-template',
props: ['value'],
mounted: function() {
this.original = this.value || "";
},
methods: {
updateValue: function(value) {
this.textChanged = this.$refs.inp.value !== this.original;
this.$emit('input', value);
},
cancel: function() {
this.$refs.inp.value = this.original;
this.textChanged = false;
}
},
data: function() {
return {
original: '',
textChanged: false
}
},
})
So as per #Bert indicated, I need to save the value. This is the final component
<div id="app">
<my-input #change="save"></my-input>
</div>
<template id="my-template">
<div class="form-group">
<label>Test</label>
<div :class="{'input-group': textChanged}">
<input type="text" class="form-control" ref="inp" v-model="current">
<div v-show="textChanged" class="input-group-addon" #click="save">Save</div>
<div v-show="textChanged" class="input-group-addon" #click="cancel">Cancel</div>
</div>
</div>
</template>
Vue.component('my-input', {
template: '#my-template',
props: ['value'],
data () {
return {
current: "",
original: ""
}
},
mounted: function() {
this.current = this.original = this.value || "";
},
methods: {
save: function() {
this.$emit('change', this.current);
this.original = this.current;
},
cancel: function() {
this.current = this.original;
}
},
computed: {
textChanged: function() {
return this.original !== this.current;
}
}
});
new Vue({
el: '#app',
methods: {
save(d) {
//axios.post(...)
console.log("saving: " + d)
}
}
});
Related
I'm trying to make a simple dropdown search select list.
I'd like to hide the name list by clicking outside of it. And the issue is that when I click outside the list of search items by using #blur directive the appropriate list item doesn't fill the input field. It's assumingly because of the #click="selectCategory(category)" is triggered later than #blur="isVisible = false" in the template.
<template>
<div сlass="search-bar" :style="{'position' : (isVisible) ? 'absolute' : 'fixed'}">
<input
type="text"
v-model="input"
#focus="isVisible = true"
// #blur="isVisible = false" doesn't work as required
/>
<div class="search-bar-options" v-if="isVisible">
<div v-for="category in filteredUser" :key="category.id" #click="selectCategory(category)">
<p>{{ category.name }}</p>
</div>
<div v-if="filteredUser.length === 0">
<p>No results found!</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
input: "",
selectedItem: null,
categoriesDynamic: [],
isVisible: false,
};
},
mounted() {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((json) => {
this.categoriesDynamic = json;
});
},
filteredUser() {
const query = this.input.toLowerCase();
if (this.input === "") {
return this.categoriesDynamic;
} else {
return this.categoriesDynamic.filter(category => {
return category.name.toLowerCase().includes(query);
});
}
},
},
methods: {
selectCategory(category) {
this.input = category.name;
this.isVisible = false;
},
},
};
</script>
<style scoped>
.pointer {
cursor: pointer;
}
.show {
visibility: show;
}
.hide {
visibility: hidden;
}
</style>
One solution could be to detect the clicked element. If the clicked element is not an input or a category element, then the dropdown can be closed.
Here is a working demo in which I gave a class "category" to each list item for element detecting purposes.
Vue.config.productionTip = false;
var app = new Vue({
el: '#app',
data() {
return {
input: "",
selectedItem: null,
categoriesDynamic: [],
isVisible: false,
};
},
mounted() {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((json) => {
this.categoriesDynamic = json;
});
},
created() {
document.addEventListener('click', (e) => {
let isInput = e.target instanceof HTMLInputElement;
let isCategoryEl = e.target.classList.contains('category');
if (isInput || isCategoryEl) return;
this.isVisible = false;
})
},
computed: {
filteredUser() {
const query = this.input.toLowerCase();
if (this.input === "") {
return this.categoriesDynamic;
} else {
return this.categoriesDynamic.filter(category => {
return category.name.toLowerCase().includes(query);
});
}
},
},
methods: {
selectCategory(category) {
this.input = category.name;
this.isVisible = false;
},
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div сlass="search-bar" :style="{'position' : (isVisible) ? 'absolute' : 'fixed'}">
<input
type="text"
v-model="input"
#focus="isVisible = true"
/>
<div class="search-bar-options" v-if="isVisible">
<div v-for="category in filteredUser" :key="category.id" #click="selectCategory(category)">
<p class="category">{{ category.name }}</p>
</div>
<div v-if="filteredUser.length === 0">
<p>No results found!</p>
</div>
</div>
</div>
</div>
I created type='radio' in 'ListSwitcher' component in Vue 3, radio have 2 option- 'listChecked' and 'mapChecked', checking the value 'mapChecked' with mounted, it shows correctly. But i cant to switch on 'listChecked' manually on the page. What i do wrong.
<div class="switcher">
<input
id="list"
:checked="checked.listChecked"
type="radio"
class="changeState changeStateList"
#change="listChange; checked.listChecked = true;"
/>
<label for="list" class="listBtn"> </label>
<input
id="map"
:checked="checked.mapChecked"
type="radio"
class="changeState changeStateMap"
#change="mapChange; checked.mapChecked = true"
/>
<label for="map" class="mapBtn"> </label>
<span class="roundToggle"></span>
</div>
and script
props: {
state: {
type: String,
},
},
emits: ['listSwitch'],
data() {
return {
checked: {
listChecked: null,
mapChecked: null,
},
};
},
mounted() {
if (this.state === 'map') {
this.checked.mapChecked = true;
}
console.log(this.checked.mapChecked)
},
methods: {
listChange() {
{
this.$emit('listSwitch', 'list');
}
},
mapChange() {
{
this.$emit('listSwitch', 'map');
}
},
emits: ['listSwitch'],
},
I have a separate component that I import radio into, and with importing this component have a code
<div class="state-change"> <ListSwitcher #listSwitch="onClickListSwitcher" state="map"/> </div>
Try this:
<template>
<div class="switcher">
<input id="list" :checked="checked.listChecked" type="radio" class="changeState changeStateList"
#change="listChange" />
<label for="list" class="listBtn"> </label>
<input id="map" :checked="checked.mapChecked" type="radio" class="changeState changeStateMap"
#change="mapChange" />
<label for="map" class="mapBtn"> </label>
<span class="roundToggle"></span>
</div>
</template>
<script>
export default {
props: {
state: {
type: String,
},
},
data() {
return {
checked: {
listChecked: false,
mapChecked: false,
},
};
},
mounted() {
if (this.state === 'map') {
this.checked.mapChecked = true;
} else if (this.state === 'list') {
this.checked.listChecked = true;
}
},
methods: {
listChange() {
this.checked.listChecked = true;
this.checked.mapChecked = false;
this.$emit('listSwitch', 'list');
},
mapChange() {
this.checked.listChecked = false;
this.checked.mapChecked = true;
this.$emit('listSwitch', 'map');
},
},
};
</script>
I have a form that uses dropdowns to allow the user to select their configuration and when they click apply a highchart is rendered. At first glance this works perfectly.
My problem is that if after the chart is rendered you open a dropdown again to make a change without closing the dropdown and click apply we get the chart with the previous configuration. I have to click apply a second time to get the new configuration to show up.
What am I missing?
Here is part of the complete code:
<template>
<div class="dropdown multiple-select">
<GlobalEvents v-if="open" #click="handleClick" #keydown.esc="close" />
<button
ref="button"
class="btn btn-block btn-multiple-select"
type="button"
:disabled="disabled"
#click="toggle"
>
<span v-if="internalValue.length === 0"> {{ emptyLabel }} </span>
<span v-else> {{ internalValue.length }} Selected </span>
<b-icon :icon="open | icon" :scale="0.5" />
</button>
<div ref="dropdown" class="dropdown-menu" :class="{ show: open }">
<template v-if="!loading">
<div class="px-4 pt-1">
<div class="form-group">
<b-form-input
v-model="search"
debounce="500"
placeholder="Search"
/>
</div>
</div>
<div class="scroll px-4">
<b-form-checkbox v-model="selectAll" class="py-1" #change="toggleAll">
Select all
</b-form-checkbox>
<b-form-checkbox
v-for="item in filtered"
:key="item.id"
v-model="internalValue"
:value="item"
class="py-1"
#input="checkSelectAllStatus"
>
{{ item.name }}
</b-form-checkbox>
<p v-if="filtered.length === 0">No results.</p>
</div>
</template>
<div v-else class="text-center my-2">
<b-spinner />
</div>
</div>
</div>
</template>
<script>
import { createPopper } from '#popperjs/core';
import GlobalEvents from 'vue-global-events';
export default {
components: {
GlobalEvents
},
filters: {
icon(item) {
return item ? 'caret-up-fill' : 'caret-down-fill';
}
},
model: {
prop: 'value',
event: 'change'
},
props: {
emptyLabel: {
type: String,
default: () => 'None Selected'
},
disabled: {
type: Boolean,
default: () => false
},
loading: {
type: Boolean,
default: () => false
},
options: {
type: Array,
default: () => []
},
value: {
type: Array,
default: () => []
}
},
data() {
return {
internalValue: this.value,
open: false,
popper: null,
search: '',
selectAll: false
};
},
computed: {
filtered() {
return this.options.filter((item) =>
item.name.toLowerCase().includes(this.search.toLowerCase())
);
},
showAll() {
return (
this.internalValue.length > 0 &&
this.internalValue.length === this.options.length
);
}
},
watch: {
options() {
this.checkSelectAllStatus();
},
internalValue() {
this.checkSelectAllStatus();
},
value(value) {
this.internalValue = value;
this.$emit('change', this.internalValue);
}
},
methods: {
checkSelectAllStatus() {
this.selectAll = this.internalValue.length === this.options.length;
},
close() {
this.open = false;
this.search = '';
this.$emit('change', this.internalValue);
},
create() {
this.popper = createPopper(this.$refs.button, this.$refs.dropdown, {
placement: 'bottom-start',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 10]
}
}
]
});
},
destroy() {
if (this.popper) {
this.popper.destroy();
this.popper = null;
}
},
handleClick(event) {
if (!this.$el.contains(event.target)) {
this.close();
}
},
toggle() {
this.open = !this.open;
if (this.open) {
this.$emit('open');
this.create();
} else {
this.destroy();
}
},
toggleAll(checked) {
if (checked) {
this.internalValue = this.options;
} else {
this.internalValue = [];
}
}
}
};
</script>
Found a solution that works with what we already have. My MultipleSelect component was invoking #input="checkSelectAllStatus"
I added this.$emit('change', this.internalValue); to the checkSelectAllStatus method and it worked.
You seem to be using vuelidate - is there a good reason why you aren't accessing your form values directly but are going through your validation model instead?
Without knowing your config, your code should look more like this:
async apply() {
try {
this.chartOptions.utilityMetric = this.form.metric;
this.chartOptions.title = this.getTitle();
this.loading = true;
this.$v.$touch()
if (this.$v.$error) throw new Error('form not valid')
const response = await await this.$api.organizations.getAnnualWidget(
this.organizationId,
this.parentUtility,
this.requestParams
);
this.chartOptions.categories = response.data.categories;
this.chartOptions.series = response.data.series;
this.chartOptions.yAxis = response.data.yAxis;
this.$forceUpdate();
} catch (error) {
const message = getErrorMessage(error);
this.$bvToast.toast(message, {
title: 'Error',
toaster: 'b-toaster-top-center',
variant: 'danger'
});
} finally {
this.loading = false;
}
}
and
requestParams() {
return {
calendarization: this.form.calendarization,
metrics: this.form.metric.map((metric) => metric.id),
meterIds: this.form.meterIds,
Years: this.form.fiscalYears.map((year) => year.value),
waterType: this.form.waterType,
includeBaseline: this.form.baseLine,
showDataLabels: this.form.showDataLabels
};
},
Again, without knowing your complete code it's difficult to tell, but I am guessing it's the pointing to vuelidate that's causing the issues
I have an input in a child component and when the user starts typing in the input it updates the data in the parent component, or should. This is my code, I can post other parts if useful.
Child
<input
:keyup="updateFilters(filters[data.field.key])"
:placeholder="data.label"
/>
methods: {
updateFilters(value) {
this.$emit("input", value);
}
}
Parent
data() {
return {
filters: {
name: "",
age: "",
address: "",
},
};
},
You can change the parent from child component the same way you do emiting other events like onchange, onkeyup, onkeydown etc.
Vue.component('parent', {
data() {
return {
parentValue: ''
};
},
template: `
<div>
<label>Parent state value:</label>
{{parentValue}}
<br/><br/>
<label>input is the child component:</label>
<br/>
<child #fromChild="fromChild"></child>
</div>
`,
methods: {
fromChild(value) {
this.parentValue = value
console.log(value) // someValue
}
}
})
Vue.component('child', {
template: `
<input
v-on:keyup="updateParent($event.target.value)"
placeholder="type something"
/>
`,
methods: {
updateParent(value) {
console.log(value)
this.$emit("fromChild", value);
}
},
})
new Vue({
el: "#app",
data: {
label: 'in Vue'
},
methods: {
toggle: function(todo) {
todo.done = !todo.done
}
}
})
I've prepared a working example here.
I am trying to update the data when doing on-change from the select box. Currently, i am able to the first time with the button click and also with on change. But I am not able to when doing the on-change multiple times.
<template>
<div>
<div class="row">
<select v-model="selectedemp" #change="filterempdata($event.target.value)">
<option value="">Select emp/option>
<option v-for="option in empfilterlist" v-bind:value="option.value" v-bind:key="option.value">{{ option.text }}</option>
</select>
</div>
<div class="row">
<empView v-if='loaded' :empDetails='empData'></empView>
</div>
<div class="col-lg-6 col-md-6">
<button type="button" id="btn2" class="btn btn-danger btn-md" v-on:click="getEmpDetails">Fetch Data</button>
</div>
</div>
</template>
Javascript part:
data () {
return {
loaded: false,
empData: {},
empfilterlist: [
{ text: 'Department', value: '1' },
{ text: 'Status', value: '2' },
],
selectedemp: '',
}
},
methods: {
filterempdata: function (selectedoption) {
console.log('Onchange value - ' + selectedOption)
Vue.set(this.empData, 'Department', selectedoption)
},
getEmpDetails: function () {
this.$http.get('http://localhost:7070/getemmdata')
.then((response) => {
this.empData = response.data
this.loaded = true
},
response => {
console.log('test' + JSON.stringify(response))
}
)
}
Child component javascript code:
export default {
name: 'empView',
props: ['empDetails'],
data () {
return {
empid: this.empDetails.id,
empname: this.empDetails.name
}
},
watch: {
empDetails: function (changes) {
console.log('data updated ' + JSON.stringify(changes))
this.empid = changes.id
this.empname = changes.name
this.department = changes.Department
}
}
}
Your code isn't complete. I've edited it and created a small example.
You call Vue.set(this.empData, 'Department', value);. Maybe there is a
spelling mistake, because I can't find this.empData.
UPDATE: Don't use camelCase for your html attributes (Use :empdetails instead of :empDetails). I've removed the on change attribute and replaced it with a computed values.
const empview = {
name: 'empview',
template: '<div>ID: {{empid}} TITLE: {{empname}} RANDNUM: {{random}}</div>',
props: ['empdetails'],
computed: {
empid() {
return this.empdetails.id;
},
empname() {
return this.empdetails.name;
},
random() {
return this.empdetails.random;
}
},
watch: {
workflowDetails(changes) {
console.log('data updated ' + JSON.stringify(changes))
this.empid = changes.id
this.empname = changes.name
this.department = changes.Department
}
}
};
new Vue({
el: '#app',
components: {
empview
},
data() {
return {
loaded: false,
empData: {},
empfilterlist: [
{
text: 'Department',
value: '1'
},
{
text: 'Status',
value: '2'
}
],
selectedemp: ''
}
},
watch: {
// triggers on change
selectedemp(value) {
// your filterempdata() code
console.log(value);
}
},
methods: {
/*getEmpDetails() {
this.$http.get('http://localhost:7070/getemmdata')
.then((response) => {
this.empData = response.data
this.loaded = true
}, (response) => {
console.log('test' + JSON.stringify(response))
})
}*/
getEmpDetails() {
console.log('getEmpDetails()');
const data = this.empfilterlist.filter((emp) => emp.value == this.selectedemp)[0];
if(data) {
this.empData = {
id: data.value,
name: data.text,
random: Math.random()
};
this.loaded = true;
}
}
}
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
</head>
<body class="container">
<div id="app">
<div>
<div class="row">
<select v-model="selectedemp">
<option value="">Select emp</option>
<option v-for="option in empfilterlist" :value="option.value" :key="option.value">{{option.text}}</option>
</select>
</div>
<div class="row">
<empview v-if='loaded' :empdetails='empData'></empview>
</div>
<div class="col-lg-6 col-md-6">
<button type="button" id="btn2" class="btn btn-danger btn-md" #click="getEmpDetails">Fetch Data</button>
</div>
</div>
</div>
<script src="https://vuejs.org/js/vue.js"></script>
</body>
</html>