We are currently migrating an externally developed Vue 2 application to Vue 3 to fix issues with deprecated libraries and to be more 'future-proof'. Due to lack of knowledge about the Vue framework and the differences between Vue 2 and 3 (we read the migration guide), we are stuck on the use of the Vuelidate Next library to validate form fields.
We make use of:
Vue 3.2.13
#Vuelidate/core 2.0.0
#vuelidate/validators 2.0.0
The below code describes the situation as created by the original programmers in Vue 2 with minor adjustments. First we import the Vuelidate libraries and the form compoments we want to pass data and validation rules to. Next in setup(), we initiate Vuelidate by calling the useVuelidate hook and binding it to v$. Then in data(), the form fields, based upon a JSON schema, are defined. In validations(), we define a single rule for the 'nameEng' form field. Finally created() calls upon the setValidation method from the methods section.
<template>
<div v-if="facility">
{{ /* When we echo v$ we do get the full object */ }}
{{ v$ }}
<form #submit.prevent>
<component v-for="field in fields" :is="field.type" :key="field.id" :value="facility.fieldValues[field.id]"
:path="[field.id]" :context="{ resource: 'facilities', institutionId: facility.institutionId }"
:label="field.label" :fields="field.fields" :editable="editable" v-bind="{ ...field.options }" />
</form>
</div>
</template>
<script>
import useVuelidate from '#vuelidate/core';
import { fetchInstitution } from '#/services/institutionsService';
import FieldRow from '#/modules/core/components/ui/formElements/FieldRow.vue';
/* The JSON schema fields aka:
{
"general": [
{
"type": "FieldRow",
"id": "nameEng",
"label": "Facility name (English)",
"options": {
"field": "StringField",
"fieldOptions": {
"errorMessage": "Please fill in a name for this facility"
}
}
},
}
*/
import fields from '../schemas/fields.json';
export default {
components: {
FieldRow,
},
props: {
facility: {
type: [Object, Array],
},
editable: {
type: Boolean,
default: false,
},
},
setup() {
const v$ = useVuelidate();
return { v$: v$ };
},
data() {
return {
fields: fields.general,
};
},
validations() {
return {
facility: {
fieldValues: {
nameEng: {
required,
},
},
},
};
},
created() {
this.setValidation();
},
methods: {
setValidation() {
this.fields = fields.general.map((field) => {
if (field.options.fieldOptions?.errorMessage) {
/* Returns empty object: {} */
console.log(this.v$);
field.options.fieldOptions.validation = this.v$.facility.fieldValues[field.id];
}
return field;
});
},
},
};
</script>
The setValidation method needs to add the validation rules to the data which gets passed on to a form field component as props. However, when trying to access the v$ in the method, it is a completely empty object. We expected to be able to call v$.facility to get the rule set, but this logically results in an undefined. Contrary, when we echo v$ in the HTML template, we do get the full object returned including the facility rule set.
Our questions would be: why can we not use the v$ object in the methods section like this? should this method still be the correct way achieve the desired result in Vue 3?
Thanks in advance!
~ Tom
Related
This question already has answers here:
Access vue instance/data inside filter method
(3 answers)
Closed 2 years ago.
I'm creating a simple Vuejs div component (to show a specific value) which needs to receive: a lists, a placeholder and a value as props. What I'm trying to do is displaying the value with the data from my database, if the user picks a new value from the lists, it should take that new value and display it. However, if the user never picks a new value and the data from the database is empty, it should display the placeholder.
So I have used filters to achieve this. However, it outputs an error: "Cannot read property 'lists' of undefined", which comes from the filters (I know because it outputs no error if I comment out the filters). When I changed the filter to this:
filters: {
placeholderFilter () {
return this.placeholderText || this.placeholder
}
}
It says:""Cannot read property 'placeholderText' of undefined"". So I was wondering if the filters properties executed before the data and props properties. What is the execution order of them? I have attached some of the relevant code down below. Anyway, If you could come up with a better way to achieve this. I would appreciate it!
Here is my component:
<template>
<div>{{ placeholderText | placeholderFilter }}</div>
<li #click="pickItem(index)" v-for="(list,index) in lists" :key="index">{{ list }}</li>
</template>
<script>
export default {
props: {
lists: {
type: Array,
required: true
},
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: String,
default: ''
}
},
data () {
return {
selected: -1,
placeholderText: this.value || this.placeholder
}
},
methods: {
pickItem (index) {
this.selected = index
}
},
filters: {
placeholderFilter () {
return this.lists[this.selected] || this.placeholderText || this.placeholder
}
}
}
</script>
And this is where I use it:
<my-component
placeholder="Please type something"
value="Data from database"
lists="['option1','option2','option3']"
>
</my-component>
Filters aren't bound to the component instance, so they simply don't have access to it through the this keyword. They are meant to always be passed a parameter and to return a transformed version of that parameter. So in other words, they're just methods. They were removed in Vue 3 entirely probably for that reason.
And yeah, what you're looking for here is a computed!
Showing only relevant code, if more is needed tell me.
My problem is that even when a name is entered to the text-field. I cannot go to the next card since !this.$v.$invalid never becomes false.. I have no idea what i am missing here..
Note that the errors are showing fine when clicked on the button without it having any value.
Vuelidate.js
import Vuelidate from 'vuelidate'
import VueTimepicker from 'vue-time-picker'
Vue.use(Vuelidate)
CardOne.js
import { validationMixin } from 'vuelidate'
import { required, maxLength ,email } from 'vuelidate/lib/validators'
mixins: [validationMixin],
validations: {
name: { required, name },
},
methods {
showNextCard(){
this.$v.$touch() //it will validate all fields
console.log(this.$v);
if (!this.$v.$invalid) { //invalid, becomes true when a validations return false
//you dont have validation error.So do what u want to do here
console.log("in here");
this.$emit("nextCard");
}
},
}
computed {
name: {
get () {
return this.$store.state.cardOne.name;
},
set (value) {
this.$store.commit('updateName', value)
}
},
nameErrors () {
const errors = []
if (!this.$v.name.$dirty) return errors
!this.$v.name.required && errors.push('Måste fyllas i')
return errors
},
}
CardOne.html
<div >
<form>
<v-text-field
v-model="name"
label="För- och efternamn"
required
#blur="$v.name.$touch()"
:error-messages="nameErrors"
:v="$v.name">
</v-text-field>
<v-btn
v-bind:style="[formCardIndexx === 0 ? {'margin-left': 'auto'} : {}]"
v-if="formCardIndexx != 10"
active-class="no-active"
class="next-button"
type="button"
:ripple="false"
#click="showNextCard()">
NÄSTA
</v-btn>
</form>
</div>
I saw that i had added that when i checked to see. But it did not help the problem. This is the v object printed right before it should validate the form.
In validations, you have:
validations: {
name: { required, name },
},
required makes sense and is a valid vuelidate built-in validator per https://vuelidate.js.org/#sub-builtin-validators. However, name doesn't. Unless you have a customer name validator defined somewhere in code you aren't showing, you should try removing that. It could be failing to validate because it can't validate against undefined, which from what I see in your code, name would surely evaluate to.
Try:
validations: {
name: { required },
},
I am using VueJS in my first real life webdev project, and am having trouble trying to query Firebase's firestore. I am using VueFire plugin for Vue FYI. I have users in a 'Patient' collection in firebase, and can access this list of patients by using the following query:
const db = firebase.firestore();
export default {
...
data () {
return {
email: firebase.auth().currentUser.email,
therapistData: [],
patientsList: []
}
},
firestore () {
return {
therapistData: db.collection('Therapist').doc(this.email),
patientsList: db.collection('Patient').where('practiceName', 'array-contains', 'TestPractice')
}
}
Each patient contains an array practiceName, and each therapist contains a string practiceName. patientsList should give me an object containing every patient in the Patient collection where their practiceName array contains the currently logged in user's (the Therapist) practiceName.
When hard coding in a string such as 'TestPractice' in my where query, the patientsList is populated as expected, and I can display it using the following vue code:
<div v-for="(userItem, idx) in patientsList) :key="idx">
<p>{{ patientsList[idx].firstName }} {{ patientsList[idx].lastName }}</p>
</div>
So this code is all working properly. My issue is that I am trying to dynamically (based on the logged in Therapist) pull the practiceName value, and pass it as the third argument to my patientsList where query. So it should read:
patientsList: db.collection('Patient').where('practiceName', 'array-contains', this.therapistData.practiceName)
When I save this code, it gives me an error saying that where() expects a valid third argument, and got "undefined". Why is this undefined? When I render in vue {{ therapistData.practiceName }} or
<button #click="patientsList(therapistData.practiceName)">Test method</button>
...
methods: {
patientsList: function(practice) {
console.log('Practice: ' + practice)
}
}
I can see the proper value of therapistData.practiceName print in the console. Can anyone please help me understand or fix this issue? Why is practiceName showing as undefined when trying to use it in my where() query?
Maybe you need to do programmatic binding
export default {
data() {
return {
therapistData: [],
}
},
watch: {
therapistData: {
// call it upon creation too
immediate: true,
handler(therapistData) {
this.$bind('practiseName', therapistData.practiseName)
},
},
},
}
I am attempting to create Gatsby pages programmatically using the Gatsby API createPages and data from Firebase. I've set up everything successfully up to the point where Firebase data is accessible via GraphQL and now I want to query specifict data for each of the new pages that were created using id (which are in string format). However, when I create the template component and try to query the data i get this error:
Variable "$clientId" of type "String!" used in position expecting type "StringQueryOperatorInput".
I have looked everywhere for a reference of this StringQueryOperatorInput and can't find any info on it. Google and graphql docs don't seem to mention the term and this is my first time seeing it. After troubleshooting for an hour I got a different error:
If you're e.g. filtering for specific nodes make sure that you choose the correct field (that has the same type "String!") or adjust the context variable to the type "StringQueryOperatorInput".
File: src/templates/Homeowner/Homeowner.js:24:9
However, I still don't know what a StringQueryOperatorInput is or how to fix this.
Below is my code for this component and my gatsby-node.js, and my gatsby-config.js where i use a plugin to source the Firebase data.
I could really use some help on this, I can't seem to find any reference of this StringQueryOperatorInput.
Everything else works fine, I just can't get this query on the Homeowner.js template to work.
gatsby-node.js
exports.createPages = async ({ graphql, actions }) => {
const { createPage } = actions;
const result = await graphql(`
query {
allClients {
nodes {
firstName
lastName
id
}
}
}
`);
console.log(JSON.stringify(result, null, 4));
result.data.allClients.nodes.forEach(node => {
const slug = `/client/${node.id}`;
createPage({
path: slug,
component: require.resolve(`./src/templates/Homeowner/Homeowner.js`),
context: { clientId: node.id },
});
});
};
src/templates/Homeowner/Homeowner.js
import React from 'react';
import { graphql } from 'gatsby';
import { withFirebase } from '../../components/Firebase';
import { withStyles } from '#material-ui/core/styles';
import Layout from '../../components/layout';
const Homeowner = ({ data }) => {
console.log(data.clients, 'data');
return (
<>
<Layout>
<h1>Home Owner Component</h1>
{/* <h3>{client.firstName}</h3>
<h3>{client.lastName}</h3>
<h3>{client.email}</h3> */}
</Layout>
</>
);
};
export default Homeowner;
export const query = graphql`
query($clientId: String!) {
clients(id: $clientId) {
firstName
lastName
email
}
}
`;
gatsby-config.js
require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
});
module.exports = {
siteMetadata: {
title: `SiteTitle`,
siteUrl: `https://www.mysitwe.com`,
description: `YourSite`,
},
plugins: [
`gatsby-plugin-react-helmet`,
`gatsby-plugin-sitemap`,
`gatsby-plugin-styled-components`,
`gatsby-plugin-sharp`,
`gatsby-transformer-sharp`,
{
resolve: `gatsby-source-firebase`,
options: {
credential: require('./firebase-key.json'),
databaseURL: 'https://firebaseurl/',
types: [
{
type: 'Clients',
path: 'clients',
},
{
type: 'Users',
path: 'users',
},
],
},
},
{
resolve: `gatsby-plugin-prefetch-google-fonts`,
options: {
fonts: [
{
family: `Nunito Sans`,
variants: [`400`, `600`, `800`],
},
{
family: `Montserrat`,
variants: [`300`, `400`, `400i`, `500`, `600`],
},
{
family: `Spectral`,
variants: [`400`, `600`, `800`],
},
{
family: `Karla`,
variants: [`400`, `700`],
},
],
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `images`,
path: `${__dirname}/src/images`,
},
},
`gatsby-plugin-offline`,
],
};
THank you in advance if anyone can help me out.
Actually literally right after I posted this question I found the solution. I needed to set up my query like so:
export const query = graphql`
query($clientId: String!) {
clients(id: { eq: $clientId }) {
firstName
lastName
email
}
}
`;
I assume that leaving out the {eq: $clientId} throws that StringQuery error on the GraphQL side. I still do not know what a StringQueryOperatorInput is, however, I have successfully generated the pages with the data from firebase.
StringQueryOperatorInput is the type of the id argument of the clients field. GraphQL includes scalar types like String, ID or Int as well as types that describe more complex data structures like arrays or objects. In this case, StringQueryOperatorInput is an input object type -- it describes objects that can be used as inputs like argument values or variables.
When filtering fields, Gatsby uses an input object like this to enable using a variety of comparison operators to filter the exposed data -- in addition to eq (equals), you can use other operators like ne, regex, in, gt, etc. You can see the full list here. Because not all inputs apply to all scalars (regex makes sense for a String field but lte does not), there's a different input type for each scalar (IntQueryOperatorInput, BooleanQueryOperatorInput, etc.)
Gatsby exposes a GraphiQL endpoint in development. Writing queries using GraphiQL allows you to utilize autocomplete and syntax highlighting so that you can avoid unexpected syntax errors like this. You can also use the "Docs" button to search and browse the entire schema.
I have an list of objects in firebase called meals.
In this component I want to show information on only one meal so I pass an id field as a prop and I want to get only that meal from firebase.
This is what I tried. It didn't work because this.id was undefined:
import db from '#/firebase'
export default {
name: 'meal',
props: {
id: {
type: String,
required: true
}
},
firebase: {
meals: db.ref('meals').child(this.id)
}
}
Did I do something wrong or does the firebase call happens before the props are initialized?
EDIT:
I managed to do it using the created hook but it looks pretty bad. Is there any other way?
created() {
this.$bindAsObject('meal', db.ref('meals').child(this.id))
}
This one burned me pretty hard. You have to use the firebase function AND create a reference to your variable like this:
import db from '#/firebase'
export default {
name: 'meal',
props: {
id: {
type: String,
required: true
}
},
firebase() {
const id = this.$props.id // Pass the reference instead of the prop directly
return {
meals: db.ref('meals').child(id)
}
}
}
According to the official doc of VueFire, your way to use the creation hook is exactly correct. Firebase basically works in a different lifecycle with Vue.js, so you need to use that doller syntax provided VueFire.