Can I get a second set of eyes on using a <b-form-input> with debounce prop?
Use case: I am making an expensive API call to check if a username already exist in a database:
<b-form-input
id="username_input"
v-model="formValues.username"
type="text"
debounce="500"
#input="usernameCheck"
></b-form-input>
and here's the input handler usernameCheck:
async usernameCheck() {
const username = this.formValues.username
if (username.length >= 3 && username.length <= 15) {
const ref = this.$fire.firestore.doc(`usernames/${username}`)
const { exists } = await ref.get() // here I'm checking if document exists already
this.usernameAvailable = !exists
} else {
...
}
Is this a good approach?
Or should I be using a watcher?
I think there is a better way to approach this. First, the key point I discovered is that there is a difference between using #input and #update as mentioned here:
The input event is emitted on user update. You have to use the update event which is triggered to update the v-model to get your expected result.
So, I have now updated my code as follows (with some additional validation UX messaging):
<b-form-input
id="username_input"
v-model="formValues.username"
type="text"
debounce="500"
:state="usernameValid"
trim
#update="usernameCheck"
></b-form-input>
valid: {{ state }}
<p v-show="formValues.username != '' && !usernameValid">
Username must be between 3 and 15 characters
</p>
<p v-show="usernameAvailable && usernameValid">Username is available!</p>
import { BFormInput } from 'bootstrap-vue'
export default {
components: {
BFormInput
},
data() {
return {
formValues: {
username: ''
},
usernameAvailable: false,
state: null,
}
},
computed: {
usernameValid() {
return (
this.formValues.username.length >= 3 &&
this.formValues.username.length <= 15
)
}
},
methods: {
async usernameCheck() {
const username = this.formValues.username
if (this.usernameValid) {
const ref = this.$fire.firestore.doc(`usernames/${username}`)
const { exists } = await ref.get() // checks if doc exists in firebase db
this.usernameAvailable = !exists
this.state = true // input now in valid state
} else {
console.log('not a valid username')
this.state = false
}
So this works for me, but I have not done a full test. I do see that the network polling is reduced due to debounce, too.
I'd be curious if anyone has any ways I can improve this (if needed!). Thanks!
Related
I'm using Pinia as Store for my Vue 3 application. The problem is that the store reacts on some changes, but ignores others.
The store looks like that:
state: () => {
return {
roles: [],
currentRole: 'Administrator',
elements: []
}
},
getters: {
getElementsForCurrentRole: (state) => {
let role = state.roles.find((role) => role.label == state.currentRole);
if (role) {
return role.permissions.elements;
}
}
},
In the template file, I communicate with the store like this:
<template>
<div>
<draggable
v-model="getElementsForCurrentRole"
group="elements"
#end="onDragEnd"
item-key="name">
<template #item="{element}">
<n-card :title="formatElementName(element.name)" size="small" header-style="{titleFontSizeSmall: 8px}" hoverable>
<n-switch v-model:value="element.active" size="small" />
</n-card>
</template>
</draggable>
</div>
</template>
<script setup>
import { NCard, NSwitch } from 'naive-ui';
import draggable from 'vuedraggable'
import { usePermissionsStore } from '#/stores/permissions';
import { storeToRefs } from 'pinia';
const props = defineProps({
selectedRole: {
type: String
}
})
const permissionsStore = usePermissionsStore();
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const onDragEnd = () => {
permissionsStore.save();
}
const formatElementName = (element) => {
let title = element.charAt(0).toUpperCase() + element.slice(1);
title = title.replace('-', ' ');
title = title.split(' ');
if (title[1]) {
title = title[0] + ' ' + title[1].charAt(0).toUpperCase() + title[1].slice(1);
}
if (typeof title == 'object') {
return title[0];
}
return title;
}
</script>
My problem is the v-model="getElementsForCurrentRole". When making changes, for example changing the value for the switch, the store is reactive and the changes are made successfully. But: If I try to change the Array order by dragging, the store does not update the order. I'm confused, because the store reacts on other value changes, but not on the order change.
What can be the issue here? Do I something wrong?
-Edit- I see the following warning on drag: Write operation failed: computed value is readonly
Workaround
As workaround I work with the drag event and write the new index directly to the store variable. But...its just a workaround. I would really appreciate a cleaner solution.
Here is the workaround code:
onDrag = (event) => {
if (event && event.type == 'end') {
// Is Drag Event. Save the new order manually directly in the store
let current = permissionsStore.roles.find((role) => role.value == permissionsStore.currentRole);
var element = current.permissions.elements[event.oldIndex];
current.permissions.elements.splice(event.oldIndex, 1);
current.permissions.elements.splice(event.newIndex, 0, element);
}
}
You should put reactive value on v-model.
getElementsForCurrentRole is from getters, so it is treated as computed value.
Similar to toRefs() but specifically designed for Pinia stores so
methods and non reactive properties are completely ignored.
https://pinia.vuejs.org/api/modules/pinia.html#storetorefs
I think this should work for you.
// template
v-model="elementsForCurrentRole"
// script
const { getElementsForCurrentRole, roles } = storeToRefs(permissionsStore);
const elementsForCurrentRole = ref(getElementsForCurrentRole.value);
I want this select multiple to pre-select one option, and not be able to deselect all options.
Whenever the last selected option is deselected it should be reselected. In other words when the user tries to deselect the last selected option it should visually not be deselected.
<template>
<b-select
if="Object.keys(doc).length !== 0 /* wait until firebase has loaded */"
:options="computedOptions"
v-model="model"
multiple
#input="onChange"
/>
</template>
<script>
//import Vue from 'vue'
import { fb } from "../fbconf";
export default {
name: "MyMultiSelect",
props: {
doc: Object, // firestore document
},
data() {
return {
options: []
};
},
firestore() {
var options = fb.db.collection("options");
return {
options: options
};
},
computed: {
computedOptions: function() {
return this.options.map(function(option) {
return {
text: option.name,
value: option.id
};
});
},
// to make sure mySelectedOptions is an array, before this.doc is loaded
// I use the following custom model
// because not using 'get' below causes a warning:
// [Vue warn]: <select multiple v-model="localValue"> expects an Array value for its binding, but got Undefined
model: {
get: function() {
if (!this.doc.hasOwnProperty('mySelectedOptions')) return []; // empty array before this.doc is loaded
else return this.doc['mySelectedOptions'];
},
set: function(newValue) {
// here I can prevent the empty array from being stored
// but visually the user can deselect all options, which is bad UX
//if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
}
},
},
methods: {
onChange: function(newValue){
// I can manually store the array as I want here
// but I cannot in any way prevent the user from deselecting all options
if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
else {
// none of these reselects the last selected option
var oldValue = this.doc['mySelectedOptions'];
this.doc['mySelectedOptions'] = this.doc['mySelectedOptions'];
//this.$forceUpdate();
//this.$emit("change", newValue);
//Vue.set(this.doc, 'mySelectedOptions', this.doc['mySelectedOptions']);
}
}
}
};
</script>
You could add watcher and when length becomes 0 just add previous value.
watch: {
model(val, oldVal) {
if(val.length == 0 && oldVal.length > 0) {
// take only one item in case there's clear button or etc.
this.model = [oldval[0]];
}
}
}
I am trying to implement font-awesome-picker to a website that i am making using vue2/php/mysql, but within standard js scripting, so no imports, .vue etc.
The script i am trying to add is taken from here: https://github.com/laistomazz/font-awesome-picker
The problem that i am facing is that i have 3 columns that have a title and an icon picker next it, that will allow the user to select 1 icon for each title. It is kinda working well...but if the same icon is used in 2 different columns then any time the user clicks again any of the 2 icons both instances of the picker will fire up, thus showing 2 popups. I need to somehow make them unique.
I've tried using
:key="list.id"
or
v-for="icon in icons" :icon:icon :key="icon"
but nothing worked. Somehow i have to separate all the instances (i think) so they are unique.
This is the template code:
Vue.component('font-awesome-picker', {
template: ' <div><div class="iconPicker__header"><input type="text" class="form-control" :placeholder="searchPlaceholder" #keyup="filterIcons($event)" #blur="resetNew" #keydown.esc="resetNew"></div><div class="iconPicker__body"><div class="iconPicker__icons"><i :class="\'fa \'+icon"></i></div></div></div>',
name: 'fontAwesomePicker',
props: ['seachbox','parentdata'],
data () {
return {
selected: '',
icons,
listobj: {
type: Object
}
};
},
computed: {
searchPlaceholder () {
return this.seachbox || 'search box';
},
},
methods: {
resetNew () {
vm.addNewTo = null;
},
getIcon (icon) {
this.selected = icon;
this.getContent(this.selected);
},
getContent (icon) {
const iconContent = window
.getComputedStyle(document.querySelector(`.fa.${icon}`), ':before')
.getPropertyValue('content');
this.convert(iconContent);
},
convert (value) {
const newValue = value
.charCodeAt(1)
.toString(10)
.replace(/\D/g, '');
let hexValue = Number(newValue).toString(16);
while (hexValue.length < 4) {
hexValue = `0${hexValue}`;
}
this.selecticon(hexValue.toUpperCase());
},
selecticon (value) {
this.listobj = this.$props.parentdata;
const result = {
className: this.selected,
cssValue: value,
listobj: this.listobj
};
this.$emit('selecticon', result);
},
filterIcons (event) {
const search = event.target.value.trim();
let filter = [];
if (search.length > 3) {
filter = icons.filter((item) => {
const regex = new RegExp(search, 'gi');
return item.match(regex);
});
}else{
this.icons = icons;
}
if (filter.length > 0) {
this.icons = filter;
}
}
},
});
I've setup a fiddle with the problem here:
https://jsfiddle.net/3yxk1ahb/1/
Just pick the same icon in both cases, and then click any of the icons again. You'll see that the popups opens for both columns.
How can i separate the pickers ?
problem is in your #click and v-show
you should use list.id instead of list.icon (i.e #click="addNewTo = list.id")
working fiddle https://jsfiddle.net/q513mhwt/
Hi every one i am going to build a real-estate application
i want to add a condition in Bedrooms filter function like a "greater than equal "
Eg:- if i select 2 on Bedroom list i want to filter greater than equal 2 Bedrooms properties
How can i do this
export default {
data() {
return {
blogs: [],
minbed: this.$route.params.bed,
}
},
created() {
this.$http.get("https://test.json").then(function(data) {
console.log(data);
this.blogs = data.body;
});
},
computed: {
filteredList() {
const { blogs, search, UnitType } = this;
return this.blogs
.filter(blog => blog.Bedrooms.includes(this.minbed))
}
}
<select
v-model="minbed"
id="formInput202"
class="form-control"
value="MaxBedrooms"
>
<option>Max.Bedrooms</option>
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
<option>ST</option>
</select>
includes method is not working in this case. Only accepts string as the param (Ref-Link1).
The inline callback function will sort out this case,
Check the below code.
filteredList() {
const { blogs, search, UnitType } = this;
var MaxBedroomsVal = this.minbed;
return this.blogs.filter( function (blog) {
return blog.Bedrooms >= MaxBedroomsVal;
})
}
Ref: Link2
I am trying to get the array values from
"validateBeforeSubmit" function to "saveForm" function. But I am
getting values of "undefined" in "arrlength". Please help me to solve.
This my code in vue.js
export default {
name: '',
data() {
return {}
},
ready: function() {
this.validateBeforeSubmit()
this.saveForm();
},
methods: {
validateBeforeSubmit() {
var fieldsVal = new Array();
var firstName = document.getElementById('firstName').value
var lastName = document.getElementById('lastName').value
var designation = document.getElementById('designation').value
if (firstName != "" && lastName != "" && designation != "") {
fieldsVal.push(firstName);
fieldsVal.push(lastName);
fieldsVal.push(designation);
return fieldsVal;
} else {
fieldsVal.length = 0;
return fieldsVal;
}
return fieldsVal;
},
saveForm() {
var fieldsValArray = this.validateBeforeSubmit();
var arrLength = fieldsValArray.length;
}
}
}
I can see multiple issues in your code:
1) Don't apply jQuery-like approach for getting input values. Use v-model instead. This will simplify your code
<template>
<input v-model="form.firstName" type="text"/>
</template>
<script>
export default {
data: {
form: {
firstName: '',
}
},
methods: {
validateBeforeSubmit() {
// take `firstName` directly from `data` not need for `getElementById`
const firstName = this.form.firstName;
}
},
}
</script>
2) Remove validateBeforeSubmit and saveForm from ready. Ready hook is obsolete in vue#2. And also it makes no sense. It's better to call it on form #submit.
3) It's better to create array using [] syntax instead of new Array()
Why never use new Array in Javascript
4) Always provide name for your component for easier debug
export default {
name: 'ValidationForm',
}
5) I don't know where was an issue but it works. Check this link below. I have updated your code. Try to submit form and check the console:
https://codesandbox.io/s/w6jl619qr5?expanddevtools=1&module=%2Fsrc%2Fcomponents%2FForm.vue