How to trigger rules validation inside a method vue? - javascript

I'm working with a component which is a date-picker and i have some rule validations(is required, not valid etc). The thing is that right now the validations triggers every time i type in a caracter, so i want to change it to trigger when the user lose the focous(onBlur).
My component looks like this:
<template>
<v-menu
v-model="pickerVisible"
:close-on-content-click="true"
:nudge-right="40"
transition="scale-transition"
offset-x
min-width="290px"
>
<template v-slot:activator="{ attrs }">
<app-text-box
v-model="model"
#blur="textFieldBlurHandler($event.target.value)"
:label="label"
#click:append="pickerVisible = true"
append-icon="mdi-calendar"
:filled="filled"
v-bind="attrs"
:rules="componentRules"
:disabled="disabled"
hide-details="auto"
class="mb-2"
ref="picker"
></app-text-box>
</template>
<v-date-picker
v-model="pickerModel"
#input="pickerInputHandler"
ref="picker"
reactive
></v-date-picker>
</v-menu>
</template>
<script lang="ts">
import Vue from 'vue'
import { displayDate, displayUTCDate } from '#/types/string-helpers'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
dayjs.extend(utc)
export default Vue.extend({
name: 'app-date-picker',
data: function () {
return {
pickerVisible: false,
pickerModel: ''
}
},
props: {
label: {
type: String,
required: false
},
filled: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
rules: {
type: Array,
required: false,
default: function () {
return []
}
},
value: String
},
computed: {
model: {
get: function (): any {
return displayUTCDate(this.value) ?? null
},
set: function (newValue: string) {
// The text field binds to this setter, so we must define it to avoid runtime errors.
// However, we do not want to begin the process of formatting/updating the value until they are done typing in the date.
// Thus, we shall throw away the new value while the user is still typing; we will update the value on blur.
}
},
componentRules: function () {
return this.rules.concat([
(value: string) => this.dateIsValid(value) || 'Date must be standard MM/DD/YYYY format.'
])
}
},
watch: {
value: function (newValue: string) {
this.pickerModel = dayjs.utc(newValue).format('YYYY-MM-DD') // Update the datepicker selection when the user enters a new date
}
},
methods: {
emitNewValue (newValue: string) {
this.$emit('input', newValue)
this.$emit('blur')
this.$emit('onChange', newValue)
},
pickerInputHandler (pickerSelection: string) {
this.emitNewValue(dayjs(pickerSelection).format('MM/DD/YYYY')) // Pass the new value up to the parent component, who will pass it back down into this.value
},
textFieldBlurHandler (textFieldInput: string) {
const date = displayDate(textFieldInput)
if (date !== '') {
this.emitNewValue(date) // Pass the new value up to the parent component, who will pass it back down into this.value
}
},
dateIsValid (value: string): boolean {
if (!value || value === '') {
return true
}
return dayjs(value, 'MM/DD/YYYY', true).isValid()
}
},
mounted: function () {
const date = displayUTCDate(this.value)
if (date !== '') {
this.pickerModel = date
} else {
this.pickerModel = dayjs().format('MM/DD/YYYY')
}
this.$nextTick(() => {
this.pickerModel = dayjs.utc(this.pickerModel).format('YYYY-MM-DD') // Update the datepicker selection when the user enters a new date YYYY-MM-DD
})
}
})
</script>
I want to execute the componentRules computed property inside the textFieldBlurHandler() method.

Related

Update State in Vuex?

I am trying to create a user Profile form where you can edit user's information such as name and age. I have 2 routes set up, the /which is for the main user component and the /edit which leads to the user Edit component. I have a user state that i am looping over to output name and age for a user in my User Component. I have added a method in my User component named enterEditMode which on click fetches name and age property of the user selected and then outputs that into the form in my EditMode Component. I am trying to create a method which onClick would update the name or age of the user. So i'd like to click on Sam and on the next page, update his name to Samuel and then click on Update Info button which should update the name Sam to Samuel on my User component page.
But i am having a hard time figuring out how i will do it.
Please check this complete working Example.
This is my Vuex Store:-
state: {
user: [{ name: "Max", age: 29 }, { name: "Sam", age: 28 }],
name: "",
age: null
},
getters: {
getUser: state => state.user,
getName: state => state.user.name,
getAge: state => state.user.age
},
mutations: {
setName(state, payload) {
state.name = payload;
},
setAge(state, payload) {
state.age = payload;
}
}
This is my User Component:-
<template>
<v-container>
<v-flex v-for="(user,index) in getUser" :key="index">
{{ user.name }} {{user.age}}
<v-icon #click="enterEditMode(index)">create</v-icon>
</v-flex>
</v-container>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
export default {
name: "User",
computed: {
...mapGetters({
getUser: "getUser"
})
},
methods: {
...mapMutations({
setName: "setName",
setAge: "setAge"
}),
enterEditMode(index) {
this.setName(this.getUser[index].name);
this.setAge(this.getUser[index].age);
this.$router.push({ name: "EditMode", params: { index: index } });
}
}
};
</script>
This is my EditMode Component:-
<template>
<v-card>
<v-text-field solo v-model="name"></v-text-field>
<v-text-field solo v-model="age"></v-text-field>
<v-btn>Update Info</v-btn>
</v-card>
</template>
<script>
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters({
getName: "getName",
getAge: "getAge"
}),
name: {
get() {
return this.getName;
},
set(val) {
return this.$store.commit("setName", val);
}
},
age: {
get() {
return this.getAge;
},
set(val) {
return this.$store.commit("setAge", val);
}
}
},
methods: {
updateInfo() {
this.$router.push('/')
}
}
};
</script>
Thank you for all the help guys. Thank you.
You need to create a mutation in the store to update the user list. For instance to update the selected user name:
Create a updateUserName mutation, and make sure the payload contains both the user index and name to be updated:
mutations: {
updateUserName(state, payload) {
state.user[payload.index].name = payload.name;
}
}
And then in the EditMode.vue file, let the set method of computed name to commit the updateUserName mutation we just created, keep in mind to pass in both the index and name properties:
name: {
get() {
return this.getName;
},
set(val) {
return this.$store.commit("updateUserName", {
name: val,
index: this.index
});
}
}
Here index is another computed property taken from the route parameters for convenience:
index() {
return this.$route.params.index;
},
Check the CodeSandbox working example.

How to create infinite scroll in Vuetify Autocomplete component?

I have a page with Vuetify Autocomplete component, and REST API backend with '/vendors' method. This method takes limit, page and name parameters and returns JSON with id and name fields.
I made some code with lazy list load on user input event. But now I want to add the ability to load this list on user scroll event.
For example, by default there is a list of 100 vendors. User scrolled this list until the end, then "some event" is called and loads next 100 of vendors. Then user keeps scrolling and the action is repeated.
Is it possible to made this with Vuetify Autocomplete component, or should i use another library?
Example code of current component is shown below:
<template>
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
></v-autocomplete>
</template>
<script>
export default {
data () {
return {
page: 0,
limit: 100,
selectedVendorId: null,
vendors: [],
loading: true
}
},
created: function (){
this.getVendorsFromApi();
},
methods: {
getVendorsFromApi (event) {
return new Promise(() => {
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + this.limit
+ '&page=' + this.page
+ '&name=' + (event ? event.target.value : ''))
.then(response => {
this.vendors = response.data;
})
})
}
}
}
</script>
I was able to get auto-loading going with the Vuetify AutoComplete component. It's a bit of a hack, but basically the solution is to use the v-slot append item, the v-intersect directive to detect if that appended item is visible, and if it is, call your API to fetch more items and append it to your current list.
<v-autocomplete
:items="vendors"
v-model="selectedVendorId"
item-text="name"
item-value="id"
label="Select a vendor"
#input.native="getVendorsFromApi"
>
<template v-slot:append-item>
<div v-intersect="endIntersect" />
</template>
</v-autocomplete>
...
export default {
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting) {
let moreVendors = loadMoreFromApi()
this.vendors = [ ...this.vendors, ...moreVendors]
}
}
}
}
In my use case, I was using API Platform as a backend, using GraphQL pagination using a cursor.
<component
v-bind:is="canAdd ? 'v-combobox' : 'v-autocomplete'"
v-model="user"
:items="computedUsers"
:search-input.sync="search"
item-text="item.node.userProfile.username"
hide-details
rounded
solo
:filter="
(item, queryText, itemText) => {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
} "
:loading="loading"
item-value="username"
class="text-left pl-1"
color="blue-grey lighten-2"
:label="label"
>
<template v-slot:selection="{ item }">
<v-chip v-if="typeof item == 'object'">
<v-avatar left>
<v-img v-if="item.node.userProfile.image" :src="item.node.userProfile.image" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-avatar>
{{ item.node.userProfile.firstName }} {{ item.node.userProfile.lastName }}
</v-chip>
<v-chip v-else-if="typeof item == 'string'">
{{ item }}
</v-chip>
</template>
<template v-slot:item="{ item: { node } }">
<v-list-item-avatar >
<img v-if="node.userProfile.avatar" :src="node.userProfile.avatar" />
<v-icon v-else>mdi-account-circle</v-icon>
</v-list-item-avatar>
<v-list-item-content class="text-left">
<v-list-item-title>
{{ $t('fullName', { firstName: node.userProfile.firstName, lastName: node.userProfile.lastName } )}}
</v-list-item-title>
<v-list-item-subtitle v-html="node.userProfile.username"></v-list-item-subtitle>
</v-list-item-content>
</template>
<template v-slot:append-item="">
<div v-intersect="endIntersect" >
</div>
</template>
</component>
import { VCombobox, VAutocomplete } from "vuetify/lib";
import debounce from "#/helpers/debounce"
import { SEARCH_USER_BY_USERNAME } from "#/graphql/UserQueries";
const RESULTS_TO_SHOW = 5
export default {
props: {
canAdd: {
type: Boolean,
default: false,
},
value: [Object, String],
label: String,
},
components: { VCombobox, VAutocomplete },
apollo: {
users: {
query: SEARCH_USER_BY_USERNAME,
variables() {
return {
username: this.search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
}
},
watchLoading(isLoading) {
this.loading = isLoading
},
skip() {
if (this.search) {
return !(this.search.length > 1)
}
return true
},
},
},
data() {
return {
user: this.value,
search: "",
cursor: null,
loading: false,
};
},
watch: {
user(newValue) {
let emit = newValue
if (newValue) {
emit = newValue.node
}
this.$emit("input", emit);
},
value(newValue) {
if (this.user && this.user.node != newValue) {
if (newValue == null) {
this.user = null
}
else {
this.user = { node: newValue };
}
}
},
search(newValue) {
this.debouncedSearch(newValue)
},
},
methods: {
endIntersect(entries, observer, isIntersecting) {
if (isIntersecting && this.users && this.users.pageInfo.hasNextPage) {
let cursor = this.users.pageInfo.endCursor
this.$apollo.queries.users.fetchMore({
variables: { cursor: cursor},
updateQuery: (previousResult, { fetchMoreResult }) => {
let edges = [
...previousResult.users.edges,
...fetchMoreResult.users.edges,
]
let pageInfo = fetchMoreResult.users.pageInfo;
return {
users: {
edges: edges,
pageInfo: pageInfo,
__typename: previousResult.users.__typename,
}
}
}
})
}
},
debouncedSearch: debounce(function (search) {
if (this.users) {
this.$apollo.queries.users.refetch({
username: search,
numberToShow: RESULTS_TO_SHOW,
cursor: null,
});
}
}, 500),
filter(item, queryText) {
return item.node.userProfile.username.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1
}
},
computed: {
computedUsers() {
if (this.users){
return this.users.edges
}
return []
},
skip() {
if (this.search) {
return this.search.length > 1
}
return false
}
}
};
</script>
Update June 12, 2021:
If you are using Vuetify 2.X, use Brettins' solution based on append-item slot and v-intersect directive.
Old answer:
Looks like it's not possible with default v-autocomplete component (at least in vuetify 1.5.16 or lower).
The component that provides the most similar functionality is VueInfiniteAutocomplete.
But keep in mind that in this case there may be problems with styles, validation, etc.
There is an example with this library.
<template>
<div>
<vue-infinite-autocomplete
:data-source="getAsyncOptions"
:fetch-size="limit"
v-on:select="handleOnSelect"
:value="autocompleteViewValue"
>
</vue-infinite-autocomplete>
</div>
</template>
<script>
export default {
data () {
return {
selectedVendorId : null,
limit: 100,
autocompleteViewValue: null
}
},
methods: {
getAsyncOptions(text, page, fetchSize) {
return new Promise((resolve, reject) => {
resolve(
this.$axios.get(this.$backendLink
+ '/vendors?limit=' + fetchSize
+ '&page=' + page
+ '&name=' + text)
.then(response => {
//Response MUST contain 'id' and 'text' fields, and nothing else.
//If there are other fields, you should remove it here
//and create 'id' and 'text' fields in response JSON by yourself
return response.data;
})
)
});
},
handleOnSelect(selectedItem) {
this.autocompleteViewValue = selectedItem.text;
this.selectedVendorId = selectedItem.id;
}
}
}
</script>
P.S.: If you just want to use v-autocomplete component with server-side pagination, you could create a "Load more..." button using append-item slot, as suggested in this issue.

How can I modify event payload within a directive?

I have custom input component that uses v-model directive, so on input it emits input event with value, and v-mask directive, that modifies value by conforming current input value to the mask and emitting another input event with modified value. However this approach fires two input events, and toggling two model changes - one raw, and one masked. Can I modify existing input event value within a directive?
const maskDirective = (() => {
const state = new Map();
return {
bind: (el) => {
const element = el instanceof HTMLInputElement ? el : el.querySelector('input');
const textMaskInput = createTextMaskInputElement({
inputElement: element,
mask: TextMasks.phoneNumber,
});
state.set('element', element);
state.set('input', textMaskInput);
},
update: () => {
const textMaskInput = state.get('input');
const element = state.get('element');
const {
state: { previousConformedValue },
} = textMaskInput;
textMaskInput.update();
// otherwise there's call stack size exceeded error, because it constantly fires input event from component, catches it, and fires event from directive
if (previousConformedValue !== element.value) {
const event = new Event('input', { bubbles: true });
element.dispatchEvent(event);
}
},
};
})();
<template>
<div
:class="{ 'is-disabled': disabled }"
class="c-base-input"
>
<input
ref="control"
v-bind="$attrs"
:class="{
'has-leading-icon': $slots['leading-icon'],
'has-trailing-icon': $slots['trailing-icon'],
'has-prepend-content': $slots['prepend'],
'has-append-content': $slots['append'],
'has-value': value !== null,
}"
:disabled="disabled"
:value="value"
:type="type"
class="c-base-input__control"
#input="onInput($event.target.value)"
>
<div
v-if="$slots['leading-icon']"
class="c-base-input__icon is-leading"
>
<slot name="leading-icon" />
</div>
<div
v-if="$slots['trailing-icon']"
class="c-base-input__icon is-trailing"
>
<slot name="trailing-icon" />
</div>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'text',
validator: value => ['text', 'tel', 'email', 'password'].indexOf(value) !== -1,
},
},
methods: {
onInput(value) {
if (value === '') {
this.$emit('input', null);
} else {
this.$emit('input', value);
}
},
},
};
</script>

Why `#change` trigger don't work for v-data-picker?

I use v-calendar package in my Vue.js application.
I want to send selected data range values to parent component. Why #change trigger don't work?
Parent.vue:
<template>
<div>
<Child #setRange="setRange" :range="range"/>
</div>
</template>
<script>
data() {
return {
range: this.range,
}
},
mounted() {
firstCallToPage();
},
methods: {
firstCallToPage(){
axios.get('URL').then(response => {
let self = this;
this.range = {
start: response.startDate,
end: response.endDate,
};
}
},
setRange(range_value) {
this.range = range_value;
}
}
</script>
Child.vue:
<v-date-picker class='v-date-picker'
mode='range'
v-model='rangeValue'
:show-day-popover=false
:max-date='new Date()'
show-caps
:input-props='{placeholder: "", readonly: true}'
#change="sendRange">
</v-date-picker>
props: {
range: {
type: Object,
},
},
data() {
return {
rangeValue: this.range
}
},
sendRange: function () {
this.$emit('setRange', this.rangeValue);
}
ERROR in console:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
Instead, use a data or computed property based on the prop's value. Prop being mutated: "range"
Try #input instead of #change. In v-datetime-picker works only with #input.
The error message is pretty explicit. The problem is that you give a prop to your child component (the one that contains v-date-picker) and you are overriding this prop with v-model (v-model is just syntactic sugar for :value and #change).
Derive your prop's value with a data value and use it for your operations:
<v-date-picker class='v-date-picker'
mode='range'
v-model='rangeValue'
:show-day-popover=false
:max-date='new Date()'
show-caps
:input-props='{placeholder: "", readonly: true}'
>
</v-date-picker>
props: {
range: {
type: Object,
},
},
data() {
return {
rangeValue: this.range
}
},
sendRange: function () {
this.$emit('setRange', this.rangeValue);
}
Instead of using a method, you can make use of watch...
Consider you have the following attributes in the range Object
range: {
start:value,
end: value
}
<v-date-picker class='v-date-picker'
mode='range'
v-model='rangeValue'
:show-day-popover=false
:max-date='new Date()'
show-caps
:input-props='{placeholder: "", readonly: true}'
>
</v-date-picker>
props: {
range: {
type: Object,
},
},
watch: {
'rangeValue.start': function(newVal){
this.$emit('setRange', newVal);
}
},
data() {
return {
rangeValue: this.range
}
}

VueJS pass default prop without reference to child component

I've stumbled upon this situation where I want to pass a prop to a child component that will be the default value of the component, but it will only be showed when the initial value is empty.
Parent Component:
<multi-line-input v-model="data.something" placeholder="Enter Something" :default="data.something"/>
Child Component
props: {
value: {
type: String,
default: ''
},
default: {
type: String,
default: ''
},
},
methods: {
emitBlur (e) {
if (!this.value && this.default) {
this.value = this.default
}
this.$emit('blur')
},
emitInput () {
this.$emit('input', this.$el.value)
}
}
So what I am trying to achieve basically, is when the component loads will get the value from v-model it will also receive a default value that shouldn't change, and only used as a value when the actual value is empty on blur
The default will have the initial value of data.something and it should not change!
I tried to get rid of the reference using JSON.parse(JSON.stringify(this.value)) but it doesn't seem to work either!
So if I understand your question correctly, you want this behavior: upon the blur event on your <multi-line-input> component, if the value of the input is empty, then set the value to a default value which is specified by the parent (through a prop).
First of all, it is an error to do this.value = ... in your component. You must not modify props, props pass data from parent to child only, the data passed through props is not yours to modify directly from within the component.
Try something like this:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
default: {
type: String,
default: '',
},
},
methods: {
onBlur() {
if (!this.value && this.default) {
this.$emit('input', this.default);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
initialUser: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
// Make a copy of the data for the purpose of assigning the
// default prop of each input
this.initialUser = _.cloneDeep(this.user);
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name" :default="initialUser.name"></multi-line-input>
<multi-line-input v-model="user.email" :default="initialUser.email"></multi-line-input>
<multi-line-input v-model="user.address" :default="initialUser.address"></multi-line-input>
</template>
</div>
Or, if you want the default value to be determined by the component instead of the parent (through a prop), you can do something like this instead:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
},
created() {
this.def = this.value;
},
methods: {
onBlur() {
if (!this.value && this.def) {
this.$emit('input', this.def);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name"></multi-line-input>
<multi-line-input v-model="user.email"></multi-line-input>
<multi-line-input v-model="user.address"></multi-line-input>
</template>
</div>
However I do not recommend the second approach because the child component instance will only every have one default value for its entire lifetime. Vue reuses component instances whenever possible, so it wouldn't work if Vue were to bind it to a different parent component (how/when would it update its own default state?).

Categories

Resources