Vue 3 Vuelidate not working with component - vue.js

I am using vue 3 / typescript . I have 2 forms one is for patient registration and other is for patient visits both forms required validation on some fields. I am using vuelidate to validate both forms. I am have applied validation on both forms patient and visit . patient form is a component and i injected it inside the visit page .The problem is when i add patient component inside visit page vailidation on visit page get fails. but then i comment out the patient compoment on visit page it works fine. both forms are independent form each other.
Here is the patient registration component
<template>
<Dialog
id="previewReceiptDailog"
v-model:visible="productDialog"
:style="{ width: '60vw' }"
position="top"
class="p-fluid"
:modal="true"
:closable="true"
#hide="closeDialog"
>
<template #header>
<h6 class="p-dialog-titlebar p-dialog-titlebar-icon">
<i class="pi pi-plus-circle"></i> {{ dialogTitle }}
</h6>
</template>
<div class="p-grid">
<div class="p-col-6">
<div class="p-field">
<label
for="firstName"
:class="{
'p-error': v$.firstName.$invalid && submitted
}"
>First Name</label
>
<InputText
id="firstName"
v-model="v$.firstName.$model"
:class="{
'p-invalid': v$.firstName.$invalid && submitted
}"
/>
<small
v-if="
(v$.firstName.$invalid && submitted) ||
v$.firstName.$pending.$response
"
class="p-error"
>{{
v$.firstName.required.$message.replace(
"Value",
"First Name"
)
}}</small
>
</div>
</div>
</div>
<template #footer>
<Button
type="button"
label="Save"
icon="pi pi-check-circle"
class="p-button-primary pull-left"
#click.prevent="saveItem(!v$.$invalid)"
/>
</template>
</Dialog>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { reactive } from "vue";
import { required } from "#vuelidate/validators";
import useVuelidate from "#vuelidate/core";
#Options({
props: {
PatientRegistration: Object
},
watch: {
PatientRegistration(obj) {
this.openDialog();
this.dialogTitle = obj.dialogTitle;
this.productDialog = obj.status;
if (obj.patientID != 0) {
this.loadPatient(obj.patientID);
}
}
},
emits: ["updatePatientStatus"]
})
export default class PatientDialog extends Vue {
private submitted = false;
private state = reactive({
firstName: "",
});
private validationRules = {
firstName: {
required
},
};
private v$ = useVuelidate(this.validationRules, this.state);
//DEFAULT METHOD OF TYPE SCRIPT
//CALLING WHENEVER COMPONENT LOADS
created() {
this.toast = new Toaster();
this.patientService = new PatientService();
}
saveItem(isFormValid) {
this.submitted = true;
if (isFormValid) {
//DO SOMETHING
}
}
}
</script>
Here is the visit page where i injeceted the component of patient
<template>
<section>
<div class="app-container">
<Dialog
v-model:visible="productDialog"
:style="{ width: '50vw' }"
:maximizable="true"
position="top"
class="p-fluid"
>
<template #header>
<h5 class="p-dialog-titlebar p-dialog-titlebar-icon">
{{ dialogTitle }}
</h5>
</template>
<form #submit.prevent="saveVisitItem(!v1$.$invalid)">
<div class="p-field">
<label for="notes">Notes</label>
<InputText
id="notes"
v-model="v1$.notes.$model"
:class="{
'p-invalid': v1$.notes.$invalid && submitted
}"
/>
<small
v-if="
(v1$.notes.$invalid && submitted) ||
v1$.notes.$pending.$response
"
class="p-error"
>{{
v1$.notes.required.$message.replace(
"Value",
"Notes"
)
}}</small
>
</div>
<Button
type="submit"
label="Save"
icon="pi pi-check"
class="p-button-primary"
/>
</form>
</Dialog>
</div>
</section>
<PatientDialog
:PatientRegistration="{
patientID: this.patientID,
status: this.patientDialogStatus,
dialogTitle: this.dialogTitle
}"
v-on:updatePatientStatus="updatePatientStatus"
/>
</template>
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import VisitService from "../../service/VisitService.js";
import { reactive } from "vue";
import useVuelidate from "#vuelidate/core";
import { required } from "#vuelidate/validators";
import PatientDialog from "../../components/PatientDialog.vue";
import AutoComplete from "primevue/autocomplete";
#Options({
components: {
PatientDialog,
AutoComplete
}
})
export default class EncounterVisit extends Vue {
private validationRules = {
notes: {
required
}
};
private visitFormState = reactive({
notes: "",
});
private v1$ = useVuelidate(this.validationRules, this.visitFormState);
//ADD OR UPDATE THE ITEM VIA HTTP
saveVisitItem(isFormValid) {
this.submitted = true;
console.log(this.v1$);
console.log(isFormValid);
//this.submitted = false;
}
}
</script>
The response i get from the console log is
$anyDirty: true
$clearExternalResults: ƒ $clearExternalResults()
$dirty: false
$error: false
$errors: []
$getResultsForChild: ƒ $getResultsForChild(key)
$invalid: true
$model: null
$path: "__root"
$pending: false
$reset: () => {…}
$silentErrors: (5) [Proxy, Proxy, Proxy, Proxy, Proxy]
$touch: () => {…}
$validate: ƒ ()
notes: Proxy {$dirty: ComputedRefImpl, $path: 'notes', required: {…}, $touch: ƒ, $reset: ƒ, …}
_vuelidate_72: Proxy {$dirty: ComputedRefImpl, $path: '__root', $model: null, $touch: ƒ, $reset: ƒ, …}
[[Prototype]]: Object
false

Related

mitt event bus with combobox usage in VueJS 3

I have configured mitt as a global event bus in my main.js:
import { createApp } from "vue";
import App from './App.vue'
import mitt from "mitt";
const emitter = mitt();
const app = createApp(App);
app.config.globalProperties.emitter = emitter;
app.mount("#app");
And I'm publishing an event using the event-bus from the child component as follows:
<template>
<div class= "combobox">
<label for={{selector_name}}>
<p>{{ selector_title }}</p>
</label>
<select :name="selector_name" :id="selector_id" :disabled="disabled" #change="onChange">
<option v-for="(opt, index) in selector_options" :key="`opt ${index}`">
{{ opt }}
</option>
</select>
</div>
</template>
<script>
export default {
name: 'ComboBox',
data() {
return {
disabled: false,
};
},
props: {
selector_title: {
type: String,
required: true
},
selector_name: {
type: String,
required: true
},
selector_id: {
type: String,
default: "combobox"
},
selector_options: {
type: Array,
required: true
},
},
methods: {
onChange(event){
this.emitter.emit('lock-combobox', { id: this.selector_id });
},
},
}
</script>
Then I try to subscribe to the event in the parent component as follows:
<template>
<div id="controls" class="param">
<div class="control" id="combobox1">
<ComboBox
selector_title = "Pattern"
selector_name = "pattern"
:selector_options="['complex', 'simple']"
selector_id="combo1"
ref="combo1"
/>
</div>
<div class="control" id="combobox2">
<ComboBox
selector_title = "Hand"
selector_name = "pattern"
:selector_options="['Left', 'Right']"
selector_id="combo2"
ref="combo2"
/>
</div>
<div class="control" id="speedslider">
<SpeedSlider/>
</div>
<div class="control" id="startstop">
<StartStopButton/>
</div>
<div class = control id="emergency">
<Emergency/>
</div>
</div>
</template>
<script>
import ComboBox from '../components/ComboBox.vue';
export default {
name: "Controls",
components: {
ComboBox,
},
created() {
this.emitter.on('lock-combobox', (event) => {
this.$refs.event.id.disabled = true;
});
},
}
</script>
But whenever I change the option in the Combobox, I get an error:
Uncaught TypeError: undefined has no properties
Can you please tell me how can I solve that? thanks in advance.

Trying to access the store in a main page and I'm getting the error

Main.vue:
<template>
<div>
<div class="home_page_header">
<h1>My Recipe</h1>
<button #click="toggleOpen">Add New Recipe</button>
</div>
<div v-for="recipe in $store.state.recipes" :key="recipe.Recipe_Name">
<h1 >{{recipe.Recipe_Name}}</h1>
<p>{{recipe.Ingredients}}</p>
<router-link :to="`/recipe/${recipe.Recipe_Name}`">
<button>View Recipe</button>
</router-link>
<router-view/>
</div>
<div>
<h1></h1>
<p></p>
</div>
<div class="popUp" v-show="openUp">
<div>
<label for="receipe_name">Recipe Name</label>
<input type="text" id="receipe_name" v-model="values.receipeName"/>
</div>
<div>
<label for="ingredients_name">Ingredients</label>
<input type="text" id="ingredients_name" v-model="values.ingredientsName" v-for="i in values.ingredientsRows" :key="i"/>
<button #click="addrows" >Add Ingredients</button>
</div>
<div><button #click="onSubmit">Submit</button></div>
<div><button #click="toggleClose">Close</button></div>
</div>
</div>
</template>
<script>
import store from '#/store/index.js'
export default {
data() {
return {
values:{
receipeName:'',
ingredientsName:'',
ingredientsRows:1
},
values_final:{
receipeName:'',
ingredientsName:'',
ingredientsRows:1
},
openUp : false
}
},
methods: {
toggleOpen(){
this.openUp = true
},
toggleClose(){
this.openUp = false
},
onSubmit(){
if(this.values.receipeName || this.values.ingredientsName == ''){
alert('enter the full details')
}
},
addrows(){
this.values.ingredientsRows++;
},
},
computed: this.$store.commit('Add_Recipes',{...values_final})
}
</script>
store:
import { createStore } from 'vuex'
export default createStore({
state: {
recipes:[
{
Recipe_Name: 'curd',
Ingredients:'xxxxxx'
}
]
},
mutations: {
Add_Recipes (state,recipe) {
state.recipes.push(recipe)
}
},
Error : app.js:340 Uncaught TypeError: Cannot read properties of undefined (reading '$store')...
I'm trying to create a recipe app by using option API, I have one main page. In that main page that contains one title and add recipe button to add the details. And another one is a popup to enter the recipe details. so here after entering all the details that should show in a main page. I'm trying to access the store in a main page but iam getting the above error.
I guess you need to move the import statement of your store to your main.js file (not your Main.vue). And add app.use(store) after you created the app there with const app = createApp(…).

Correctly testing vee-validate validated form submit with Jest

I am trying to submit a form that uses vee-validate and test if the form calls the underlying store with Jest.
Here is my code:
Form:
<template>
<div class="flex flex-col justify-center h-screen bg-site-100">
<!-- Login body -->
<div class="container">
<div class="mx-auto w-4/12 p-7 bg-white">
<!-- Form -->
<Form id="loginForm" #submit="login" :validation-schema="schema" v-slot="{ errors }">
<div class="mt-4">
<div>
<text-box
:type="'email'"
:id="'email'"
:label="'Your Email'"
v-model="email"
:place-holder="'Email'"
:required="true"
:error="errors.email"
/>
</div>
<div>
<text-box
:type="'password'"
:id="'password'"
:label="'Parool'"
v-model="password"
:place-holder="'Password'"
:required="true"
:error="errors.password"
/>
</div>
<!-- Submit -->
<Button
type="submit"
id="loginButton"
:disabled="Object.keys(errors).length > 0"
class="text-white bg-site-600 w-full hover:bg-site-700 focus:ring-4 focus:ring-site-300 font-medium rounded-md text-sm px-5 py-2.5 mr-2 mb-2 focus:outline-none"
>
Log In
</Button>
</div>
</Form>
</div>
</div>
</div>
</template>
<script lang="ts">
import * as Yup from "yup";
import { Form } from "vee-validate";
import { defineComponent } from "vue";
import Button from "../core/Button.vue";
import TextBox from "../core/TextBox.vue";
import { mapActions, mapStores } from "pinia";
import { useAuthStore } from "../../store/auth";
import LoginDataType from "../../types/login_data";
export default defineComponent({
name: "Login",
components: { TextBox, Form, Button },
computed: { ...mapStores(useAuthStore) },
data() {
return {
email: "",
password: "",
schema: Yup.object().shape({
email: Yup.string().required("Email is required").email("Email is invalid"),
password: Yup.string().required("Password is required"),
}),
};
},
methods: {
async login() {
console.log("Logged in mock");
let data: LoginDataType = {
email: this.email,
password: this.password,
};
await this.authStore.login(data);
},
},
});
</script>
Store:
import { defineStore } from "pinia";
export const useAuthStore = defineStore("auth", {
state: () => ({
}),
getters: {
},
actions: {
async login(data: LoginDataType) {
// do something
},
}
})
Test:
it('logs in correctly when right username and password sent to API', async () => {
const store = useAuthStore();
jest.spyOn(store, 'login');
const wrapper = mount(Login, {
stubs: ['router-link']
});
const email = wrapper.find('input[id="email"]');
await email.setValue('testEmail#gmail.com');
// Check if model is set
expect(wrapper.vm.email).toBe(testEmail);
const password = wrapper.find('input[id="password"');
await password.setValue('testPw');
// Check if model is set
expect(wrapper.vm.password).toBe(testPw);
// Check form exists
const loginForm = wrapper.find('#loginForm');
expect(loginForm.exists()).toBe(true);
await loginForm.trigger('submit');
// Check if store method has been called
expect(store.login).toHaveBeenCalled();
expect(store.login).toHaveBeenCalledWith({
email: 'testEmail#gmail.com',
password: 'testPw'
})
});
The test fails at expect(store.login).toHaveBeenCalled(). Implying the form doesn't get submitted. The test works just fine when I replace the vee-validate component Form with a regular HTML form tag.
What might be causing this behaviour any help is highly appreciated? :)

VueJS display dynamic modal component

I have posts and replys s.t. replies belong to posts via the attribute reply.posts_id.
I am attempting to show the reply form as a modal for the user to enter a reply. However, I want to create a generic Modal component that I can use everywhere with content that is specified in another component built for a specific context.
Reply to post is the first place I woul like this to work.
Currently, the Vuex correctly returns Modal visible:true when the reply button is clicked, but the modal does not render and I get the error message showing that the Modal component is not found:
Unknown custom element: <ModalReplyForm> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
I am using vuex to manage the visibility of the modal. Here are the relevant files:
store.js:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
...
Vue.use(Vuex)
export default new Vuex.Store({
state: {
status: '',
...
modalVisible: false,
modalComponent: null
},
mutations: {
...
showModal(state, componentName) {
console.log('showing the modal')
state.modalVisible = true;
state.modalComponent = componentName;
},
hideModal(state) {
console.log('hiding the modal')
state.modalVisible = false;
}
},
actions: {
...
}
},
getters: {
isAuthenticated: state => !!state.user,
authStatus: state => state.status,
user: state => state.user,
token: state => state.token,
posts: state => {
return state.posts;
}
...
}
})
App.vue
<template>
<div id="app">
<app-modal></app-modal>
<NavigationBar />
<div class="container mt-20">
<router-view />
</div>
<vue-snotify></vue-snotify>
</div>
</template>
<script>
import AppModal from '#/components/global/AppModal';
import NavigationBar from '#/components/layout/NavigationBar'
export default {
name: "App",
components: {
AppModal,
NavigationBar
}
};
</script>
<style>
body {
background-color: #f7f7f7;
}
.is-danger {
color: #9f3a38;
}
</style>
Post.vue (houses the button to call the reply modal):
<template>
<div class="row ui dividing header news">
<!-- Label -->
<div class="m-1 col-md-2 ui image justify-content-center align-self-center">
<img v-if="post.avatar_url" :src="post.avatar_url" class="mini rounded"/>
<v-gravatar v-else :email="post.email" class="mini thumbnail rounded image rounded-circle z-depth-1-half"/>
</div>
<!-- Excerpt -->
<div class="col-md-9 excerpt">
...
<!-- Feed footer -->
<div class="feed-footer row">
<div class="small"> {{ post.created_at | timeAgo }}</div>
<button type="button" flat color="green" #click="showModal('ModalReplyForm')">
<i class="fa fa-reply" ></i>
...
<div v-show="postOwner(post)" class="">
<button type="button" flat color="grey" #click="deletePost(post.id)">
<i class="fa fa-trash " ></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex';
import PostsService from '../../services/PostsService'
import RepliesService from '../../services/RepliesService'
import Replies from '#/components/Reply/Replies'
import ReplyForm from '#/components/Reply/ReplyForm'
export default {
name: "Post",
props: {
post: {
type: Object,
required: true
}
},
components: {
Replies,
ReplyForm
},
computed: {
me() {
return this.$store.getters.user
}
},
methods: {
...mapMutations(['showModal']),
...
}
};
</script>
AppModal.vue - generic Modal component
<template>
<div class="c-appModal">
<div class="c-appModal__overlay" v-if="visible"></div>
<div class="c-appModal__content" v-if="visible" #click.self="hideModal"></div>
<div class="c-appModal__innerContent">
<component :is="component"></component>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import { mapState, mapMutations } from 'vuex';
export default {
name: 'AppModal',
data() {
return {
component: null
}
},
computed: {
...mapState({
visible: 'modalVisible',
modalComponent: 'modalComponent'
}),
},
methods: {
...mapMutations(['hideModal'])
},
watch: {
modalComponent(componentName) {
if (!componentName) return;
Vue.component(componentName, () => import(`#/components/modals/${componentName}`));
this.component = componentName;
}
},
created() {
const escapeHandler = (e) => {
if (e.key === 'Escape' && this.visible) {
this.hideModal();
}
};
document.addEventListener('keydown', escapeHandler);
this.$once('hook:destroyed', () => {
document.removeEventListener('keydown', escapeHandler);
});
},
};
</script>
ModalReplyForm - specific reply modal content
<template>
<div>
<div class="c-modalReply">
<div>
<label for="reply">Your comment</label>
<div class="field">
<textarea name="reply" v-model="reply" rows="2" placeholder="Compose reply"></textarea>
</div>
</div>
<button class="c-modalReply__cancel" #click="hideModal">Cancel</button>
<button class="c-modalReply__post" :disabled="!isFormValid" #click="createReply">Reply</button>
</div>
</div>
</template>
<script>
import RepliesService from '#/services/RepliesService'
import { mapMutations } from 'vuex';
export default {
name: "ModalReplyForm",
// props: {
// post: {
// type: Object,
// required: true
// }
// },
data() {
return {
reply: ""
};
},
computed: {
isFormValid() {
return !!this.reply;
},
currentGroup() {
return this.$store.getters.currentPost;
}
},
methods: {
...mapMutations([
'hideModal'
]),
async createReply () {
let result = await RepliesService.addReply({
reply: {
body: this.reply,
postId: this.post.id
}
});
this.$emit("reply-created");
this.hideModal();
}
}
};
</script>
Unknown custom element: - did you register the
component correctly? For recursive components, make sure to provide
the "name" option.
This message says that you never imported/defined ModalReplyForm, which you have not.
In my own generic modal, I ended up having to import all the components that might appear within the modal itself.
If you add a:
import ModalReportForm from ...
and a:
components: {
ModalReplyForm
}
to AppModal.vue, the modal should then do what you expect.

I got Vue errors rendering component

I have a "vue-cli webpack" like the following :
src/components/Signin.vue:
<template>
...
<form v-on:submit.prevent="userSignIn">
...
<div class="field">
<p class="control has-icons-left has-icons-right">
<input
v-validate="'required|email'"
v-bind:class="{'is-danger': errors.has('name')}"
name="email"
v-model="form.email"
class="input"
id="email"
type="email"
placeholder="Email"
>
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
<span class="icon is-small is-right">
<i class="fa fa-check"></i>
</span>
<span class="help is-danger" v-show="errors.has('email')">{{ errors.first('email') }}</span>
</p>
</div>
<div class="field">
<p class="control has-icons-left">
<input
v-validate="'required|min:5'"
v-bind:class="{'is-danger': errors.has('name')}"
name="password"
v-model="form.password"
class="input"
id="password"
type="password"
placeholder="Password"
>
<span class="icon is-small is-left">
<i class="fa fa-lock"></i>
</span>
<span class="help is-danger" v-show="errors.has('password')">{{ errors.first('password') }}</span>
</p>
</div>
<div class="field is-grouped">
<div class="control">
<button v-bind:disabled="errors.any()" class="button is-primary" type="submit" :disabled="loading">
Submit
</button>
</div>
</div>
</form>
...
</template>
<script>
...
export default {
data () {
return {
form: {
email: '',
password: '',
alert: false
}
}
},
computed: {
error () {
return this.$store.getters.getError
},
loading () {
return this.$store.getters.getLoading
}
},
watch: {
error (value) {
if (value) {
this.alert = true
}
},
alert (value) {
if (!value) {
this.$store.dispatch('setError', false)
}
},
methods: {
userSignIn () {
this.$store.dispatch('userSignIn', {email: this.email, password: this.password})
}
}
},
...
}
</script>
src/App.vue:
<template>
<main>
<router-view></router-view>
</main>
</template>
<style lang="sass">
#import "~bulma"
/* Your css for this file... */
</style>
src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import firebase from 'firebase'
import { store } from './store'
import VeeValidate from 'vee-validate'
import { firebaseConfig } from './config'
Vue.use(VeeValidate)
Vue.config.productionTip = false
firebase.initializeApp(firebaseConfig)
/* eslint-disable no-new */
const unsubscribe = firebase.auth()
.onAuthStateChanged((firebaseUser) => {
new Vue({
el: '#app',
router,
store,
render: h => h(App),
created () {
store.dispatch('autoSignIn', firebaseUser)
}
})
unsubscribe()
})
and I get two errors when I click the button :
Property or method "userSignIn" is not defined on the instance but
referenced during render. Make sure to declare reactive data
properties in the data option.
Signin.vue?d58e:24 Uncaught TypeError: _vm.userSignIn is not a
function
You've defined your methods inside your watch. Move them outside.
watch: {
error (value) {
if (value) {
this.alert = true
}
},
alert (value) {
if (!value) {
this.$store.dispatch('setError', false)
}
},
},
methods: {
userSignIn () {
this.$store.dispatch('userSignIn', {email: this.email, password: this.password})
}
}