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
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>
import axios from "axios";
export const routerid = async (itemId) =>
await axios.get("https://fakestoreapi.com/products?limit=" + itemId);
<template>
<div>
<div v-for="(item, key) in user" :key="key">
{{ item.price }} <br />
{{ item.description }} <br />
</div>
</div>
</template>
<script>
import { routerid } from "./routerid";
export default {
name: "User",
components: {},
data() {
return {
lists: [],
};
},
mounted() {
if (this.$route.params.id)
routerid(this.$route.params.id).then((r) => {
let obj = r.data;
this.lists = [{ ...obj }];
});
},
computed: {
user: function () {
return this.lists.filter((item) => {
return item.id === this.$route.params.id;
});
},
},
};
</script>
And this is my complete code:- https://codesandbox.io/s/late-brook-eg51y3?file=/src/components/routerid.js
Above is my api call, with url query like url...../?limit=" + id
Above is the logic , which i tried. But not sure whats wrong with code. getting blank screen.
please provide some suggestions, on how to call. and please go through my code once, if there is any other issues. Thanks
It's all about spread operator, you should spread object inside array correctly, below example works fine.
<template>
<div>
<div v-for="(item, key) in user" :key="key">
{{ item.price }} <br />
{{ item.description }} <br />
</div>
</div>
</template>
<script>
import { routerid } from "./routerid";
export default {
name: "User",
components: {},
data() {
return {
lists: [],
};
},
mounted() {
if (this.$route.params.id)
routerid(this.$route.params.id).then((r) => {
let obj = r.data;
//changed from [{...obj}] to [...obj]
this.lists = [...obj];
});
},
computed: {
user: function () {
return this.lists.filter((item) => {
return item.id === this.$route.params.id;
});
},
},
};
</script>
You have 2 problems.
1 - Firstly you're using user instead of lists in the for loop.
2 - Secondly you're using spread operator on the retuned data which is already an array so you don't need to do that.
<template>
<div>
<div v-for="(item, key) in lists" :key="key">
{{ item.price }} <br />
{{ item.description }} <br />
</div>
</div>
</template>
<script>
import { routerid } from "./routerid";
export default {
name: "User",
components: {},
data() {
return {
lists: [],
};
},
mounted() {
if (this.$route.params.id)
routerid(this.$route.params.id).then((r) => {
this.lists = r.data;
});
},
computed: {
user: function () {
return this.lists.filter((item) => {
return item.id === this.$route.params.id;
});
},
},
};
</script>
I am trying to wrap content in my React app to use a different top-level element.
In my state I have defined wrapContent: false
I have defined a toggleWrap method.
toggleWrap = () => {
this.setState(state => state.wrapContent = !state.wrapContent);
}
And finally in my input checkbox I have included the toggleWrap method in
onChange={this.toggleWrap}
Unfortunately when I run my code, pressing on the checkbox does not wrap my component content in the following code inside my wrapContent method
wrapContent(content){
return this.state.wrapContent
? <div className="bg-secondary p-2">
<div className="bg-light"> {content} </div>
which wraps the contents of my render(){} return in this component.
What is strange is that when I manually change wrapContent: false in my state to True, I see that the checkbox is checked in my browser and that the code within my render is properly wrapped.
So it seems that just my toggleWrap function is not working as it should.
Could someone help here?
Full code:
import React, { Component } from 'react'
import { ValidationDisplay } from './ValidationDisplay';
import { GetValidationMessages } from './ValidationMessages';
export class Editor extends Component {
constructor(props) {
super(props);
this.formElements = {
name: { label: "Name", name: "name", validation: { required: true, minLength: 3 } },
category: { label: "Category", name: "category", validation: { required: true, minLength: 5 } },
price: { label: "Price", name: "price", validation: { type: "number", required: true, min: 5 } }
}
this.state = {
errors: {},
wrapContent: false
}
}
// 06.11.21- method is invoked when the content is rendered
setElement = (element) => {
if (element !== null) {
this.formElements[element.name].element = element;
}
}
// handleChange = (event) => {
// event.persist()
// this.setState(state => state[event.target.name] = event.target.value);
// }
handleAdd = () => {
if (this.validateFormElements()) {
let data = {};
Object.values(this.formElements)
.forEach(v => {
data[v.element.name] = v.element.value;
v.element.value = "";
});
this.props.callback(data);
this.formElements.name.element.focus();
}
}
validateFormElement = (name) => {
let errors = GetValidationMessages(this.formElements[name].element);
this.setState(state => state.errors[name] = errors);
return errors.length === 0;
}
validateFormElements = () => {
let valid = true;
Object.keys(this.formElements).forEach(name => {
if (!this.validateFormElement(name)) {
valid = false;
}
})
return valid;
}
toggleWrap = () => {
this.setState(state => state.wrapContent = !state.wrapContent);
}
wrapContent(content) {
return this.state.wrapContent
? <div className="bg-secondary p-2">
<div className="bg-light"> {content} </div>
</div>
: content;
}
render() {
return this.wrapContent(
<React.Fragment>
<div className="form-group text-center p-2">
<div className="form-check">
<input className="form-check-input"
type="checkbox"
checked={this.state.wrapContent}
onChange={this.toggleWrap} />
<label className="form-check-label">Wrap Content</label>
</div>
</div>
{
Object.values(this.formElements).map(elem =>
<div className="form-group p-2" key={elem.name}>
<label>{elem.label}</label>
<input className="form-control"
name={elem.name}
autoFocus={elem.name === "name"}
ref={this.setElement}
onChange={() => this.validateFormElement(elem.name)}
{...elem.validation} />
<ValidationDisplay
errors={this.state.errors[elem.name]} />
</div>)
}
<div className="text-center">
<button className="btn btn-primary" onClick={this.handleAdd}>
Add
</button>
</div>
</React.Fragment>)
}
}
This was an issue with how you update the state
Try below
toggleWrap = () => {
this.setState({ wrapContent: !this.state.wrapContent });
};
or
toggleWrap = () => {
this.setState((state) => {
return { ...state, wrapContent: !this.state.wrapContent };
});
};
instead of
toggleWrap = () => {
this.setState(state => state.wrapContent = !state.wrapContent);
}
I'm making a sorting function by Vue js
I'd like to make a list by the ID order for the default, then, sorting function can be occured by clicking asc/desc by name button.
Also, when clicking all button, the list sorts by the ID order again and adding the class named is-active
I know I've added sorting function by the default but I don't know how to combine with the order of ID number.
If somebody know how to, please help.
Thank you
new Vue({
el: '#app',
data: {
allItem: true,
order: null,
list: [],
},
created: function () {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(function (response) {
this.list = response.data
}.bind(this)).catch(function (e) {
console.error(e)
})
},
methods: {
all: function() {
this.full = true //for button class 'is-active'... NOT WORKING ...
},
},
computed: {
sort: function() {
console.log(this.order);
return _.orderBy(this.list, 'name', this.order ? 'desc' : 'asc') //loadash
},
sorted: function() {
if (this.order || !this.order) { //sort by arc/desc
this.ordered = true //for button class 'is-active'... NOT WORKING ...
this.allItem = false //for button class 'is-active'... NOT WORKING ...
console.log(this.order);
return this.sort
} else if (this.order = null){ //defalut order by ID number ... NOT WORKING ...
this.ordered = false //for button class 'is-active'... NOT WORKING ...
console.log(this.full);
return this.list
}
},
}
})
span {font-weight: bold;}
.is-active {background: turquoise;}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios#0.17.1/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
<div id="app">
<ul>
<li v-for="item in sorted" :key="item.id">
<span>ID:</span> {{item.id}} , <span>Name:</span> {{item.name}} , <span>Company:</span> {{item.company.name}}
</li>
</ul>
<button :class="{'is-active': allItem}" #click="all">all</button>
<button :class="{'is-active': ordered}" #click="order=!order">asc/desc by name</button>
</div>
new Vue({
el: '#app',
template: `
<div v-if="!loading">
<ul>
<li v-for="item in sorted" :key="item.id">
<strong>ID:</strong> {{item.id}} ,
<strong>Name:</strong> {{item.name}} ,
<strong>Company:</strong> {{item.company.name}}
</li>
</ul>
<button :class="{ 'is-active': sortId === 'id' }" #click="sortById">all</button>
<button :class="{ 'is-active': sortId === 'name' }" #click="sortByName">asc/desc by name</button>
</div>
`,
data() {
return {
list: [],
loading: true,
sortId: "id",
directionAsc: true,
};
},
computed: {
sorted() {
const DIRECTION = this.directionAsc ? "asc" : "desc";
return _.orderBy(this.list, [this.sortId], [DIRECTION]);
},
},
created: function() {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(function(response) {
this.list = response.data;
this.loading = false;
}.bind(this)).catch(function(e) {
console.error(e)
})
},
methods: {
sortById() {
if (this.sortId === "id") {
return;
}
this.sortId = "id";
this.directionAsc = true;
},
sortByName() {
if (this.sortId === "name") {
this.directionAsc = !this.directionAsc;
} else {
this.sortId = "name";
}
},
},
})
.is-active {
background: turquoise;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios#0.17.1/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
<div id="app"></div>
You can do it simpler with computed and switch. Code:
new Vue({
el: '#app',
data: {
sort: '',
order: false,
list: [],
},
created: function () {
axios.get('https://jsonplaceholder.typicode.com/users')
.then(function (response) {
this.list = response.data
}.bind(this)).catch(function (e) {
console.error(e)
})
},
methods: {
setSort(sort) {
if(this.sort === sort) {
this.toggleOrder();
} else {
this.sort = sort;
}
}
toggleOrder() {
this.order = !this.order;
}
},
computed: {
sortedList: function() {
switch(this.sort) {
case 'name':
return _.orderBy(this.list, 'name', this.order ? 'desc' : 'asc');
case 'id':
return _.orderBy(this.list, 'id', this.order ? 'desc' : 'asc');
default:
return this.list;
}
},
}
})
And template:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios#0.17.1/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.15/lodash.min.js"></script>
<div id="app">
<ul>
<li v-for="item in sorted" :key="item.id">
<span>ID:</span> {{item.id}} , <span>Name:</span> {{item.name}} , <span>Company:</span> {{item.company.name}}
</li>
</ul>
<button :class="{'is-active': !sort}" #click="setSort('')">all</button>
<button :class="{'is-active': sort === 'name'}" #click="setSort('name')">asc/desc by name</button>
</div>
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>