I have several components inside each of them have fields that are validated with vuelidate. All field validation rules are described in the parent component wrapper in the child components, I pass the object :v="$v", everything works. But in the test, when mounting, I get an error, and as I understand it, because when mounting, I do not pass this object, tell me how to do it right?
parent wrapper
<template>
<VModal>
<template v-slot:content>
<div class="layer">
<institutes-info
:v="$v"
ref="info-component"
/>
</div>
</template>
</VModal>
</template>
export default {
name: "WrapModalInstitutes",
validations: {
si: {
name: {
required,
maxLength: maxLength(256),
},
},
},
}
child component
<template>
<form action="" id="form-info" #submit.prevent="uploadInfo($event, si.id)">
<p class="sub-text">{{ $t("vnz.info") }}</p>
<div class="institut-info">
<div class="input-group" :class="{ 'form-group--error': v.si.name.$error, 'form-group--success': !v.si.name.$invalid }">
<label for="name-institut">{{ $t("vnz.nameVNZ") }}</label>
<div class="input-container">
<input id="name-institut" name="title" type="text" :placeholder="$t('vnz.nameVNZ')" v-model.trim="name" #keydown="trimSpecialCharacters($event)" #input="trimSpecialCharacters($event, 'name')">
<div class="error" v-if="!v.si.name.required && v.si.name.$dirty">{{ $t("vnz.notEmpty") }}</div>
</div>
</div>
</div>
<button v-if="edit" type="submit" class="btn-primary edit" :disabled="submitStatus === 'PENDING'">{{ $t("vnz.save") }}</button>
</form>
</template>
export default {
name: "InstitutesInfoModal",
props: ['v'],
}
test file
import Vuex from "vuex"
import {mount, createLocalVue, config} from "#vue/test-utils"
import InstitutesInfo from '../../assets/src/components/InstitutesInfo'
import Vuelidate from 'vuelidate'
import VueTheMask from 'vue-the-mask'
import translations from "../../assets/src/common-resources/translations/translations"
import fetchMock from 'jest-fetch-mock'
const locale = "uk";
config.mocks["$t"] = msg => translations[locale][msg]
fetchMock.enableMocks()
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(Vuelidate)
localVue.use(VueTheMask)
describe('testing institute-info component', () => {
let store;
let mutations;
let actions;
beforeEach(() => {
mutations = {
'institutes/EDIT_SI_OBJ': jest.fn(),
'institutes/SUBMIT_STATUS': jest.fn()
}
actions = {
'institutes/UPLOAD_INFO': jest.fn()
}
store = new Vuex.Store({
modules: {
institutes: {
state: {
si: {
name: null,
},
submitStatus: null
},
mutations,
actions
}
}
})
})
test('some test"',() => {
const wrapper = mount(InstitutesInfo, {
store, localVue
})
})
})
[Vue warn]: Error in render: "TypeError: Cannot read property 'si' of undefined"
Related
I am creating a custom input component using Vue 3. I'm trying to make the value of parent's data change when the value of input changes, but it doesn't work as intended.
Code :
CustomInput.vue
<template>
<input :type="type" :value="value" #input="onChange" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "custom-input",
props: {
type: String,
value: String,
},
methods: {
onChange(event: Event) {
const target = event.target as HTMLInputElement;
this.$emit("input", target.value);
},
},
});
</script>
Form.vue
<template>
<form #submit.prevent="onsubmit">
<h2>Bucket Login</h2>
<CustomInput type="text" :value="id" v-model="id" placeholder="ID" />
<CustomInput
type="password"
:value="password"
v-model="password"
placeholder="PASSWORD"
/>
<CustomButton>Login</CustomButton>
<p #click="moveJoin">Join</p>
</form>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import CustomButton from "./CustomButton.vue";
import CustomInput from "./CustomInput.vue";
export default defineComponent({
name: "login-form",
components: {
CustomButton,
CustomInput,
},
data: () => ({
id: "",
password: "",
}),
methods: {
onsubmit() {
console.log(this.id); // result => undefined
},
moveJoin() {
this.$router.push("/join");
},
},
});
</script>
If you execute onSubmit after entering a value in customInput, the value of this.id is undefined.
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? :)
I have a nuxtJS vue2 components as follows:
<template>
<div class="product-fullpage">
<div class="product-card">
<img :src="productById(this.$route.params.id).imageURL">
<div class="product-headings">
<div class="product-info" v-animate-on-scroll>
<h1>{{ productById(this.$route.params.id).name }}</h1>
<h2>£{{ productById(this.$route.params.id).price }}</h2>
</div>
<div class="product-cart" v-animate-on-scroll>
<div class="quantity-info">
<label for="quantity">Quantity:</label>
<input v-model.number="productInfo.quantity" name="quantity" type="number" min="0" max="99"/>
</div>
<button #click="addToCart" :disabled="noQuantity">Add To Cart ></button>
</div>
</div>
</div>
</div>
</template>
I'm using a getter that does the following:
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['productById'])
},
}
</script>
Here's the getter
export const getters = {
productById: (state) => (id) => {
return state.products.find(product => product.id === id)
},
}
my state is set to pull from firebase
export const actions = {
async setProducts({commit}) {
let colRef = collection(db, 'products')
const querySnapshot = await getDocs(colRef)
querySnapshot.forEach((doc) => {
const imageDownloadURL = getDownloadURL(ref(storage, `${doc.data().imageRef}`))
.then( url => {
// console.log(url)
let article = ({
id: doc.id,
name: doc.data().name,
price: doc.data().price,
description: doc.data().description,
imageURL: url
})
commit('setProducts', article)
})
})
},
}
the mutation to set the state:
export const mutations = {
setProducts(state, article) {
let matchProduct = state.products.find(product => product.id == article.id)
if(!matchProduct) {
state.products.push(article)
}
},
}
and this is my state:
export const state = () => ({
products: [],
})
i thought that if i load everything beforehand in default.vue under 'layouts' that i can then have the store.state.products set.
<template>
<div class="container">
<!-- <nuxt /> -->
<nuxt v-if="!loading"/>
<div class="overlay" v-else>
Loading...
</div>
</div>
</template>
<script>
export default {
created() {
this.loading = true
this.$store.dispatch('setCart')
this.$store.dispatch('setProducts')
.finally(() => (this.loading=false))
},
data() {
return {
loading: false,
}
},
}
</script>
sorry if this is turning out to be a long post - but basically on initial load, I get my imageURL, name and price. but then on reload it comes out empty. i believe the getter is occurring before the store is loaded. how do i set it so that i can state.products.find(product => product.id == article.id) for my getter after state is loaded?
I want to add a v-model on a component but I got this warning:
[Vue warn]: Component emitted event "input" but it is neither declared in the emits option nor as an "onInput" prop.
Here is my code:
// Parent.vue
<template>
<h2>V-Model Parent</h2>
<Child v-model="name" label="Name" />
<p>{{ name }}</p>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const name = ref('')
</script>
// Child.vue
<template>
<input
class="input"
type="text"
:placeholder="props.label"
:value="props.value"
v-on:input="updateValue($event.target.value)"
/>
</template>
<script setup>
import { defineProps, defineEmit } from 'vue'
const props = defineProps({
label: String,
value: String
})
const emit = defineEmit('input')
function updateValue(value) {
emit('input', value)
}
</script>
I was trying to reproduce this tutorial but I'am stuck and got no idea what I am missing.
I want to display {{ name }} in the Parent.vue component. Do you got an idea how to solve this?
In vue 3 value prop has been changed to modelValue and the emitted event input to update:modelValue:
// Child.vue
<template>
<input
class="input"
type="text"
:placeholder="props.label"
:value="props.modelValue"
v-on:input="updateValue($event.target.value)"
/>
</template>
<script setup>
const props = defineProps({
modelValue: String
})
const emit = defineEmits(['update:modelValue'])
function updateValue(value) {
emit('update:modelValue', value)
}
</script>
I like to use with computed as well
<template>
<div>
<input v-model="model">
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number],
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const model = computed({
get () {
return props.modelValue
},
set (value) {
return emit('update:modelValue', value)
}
})
</script>
I have the similar issues and finally I got it work. Here are one solution for one or multiple checkbox for Vue 3 and TypeScript.
ref: https://v2.vuejs.org/v2/guide/forms.html?redirect=true#Checkbox
solution : for one or multiple checkbox
CheckBox Component:
<template>
<input
type="checkbox"
:value="inputValue"
:disabled="isDisabled"
v-model="model"
:class="[defaultClass, inputClass, checkboxClass]"
/>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
export default defineComponent({
components: {},
props: {
inputValue: {
type: String,
required: false,
default: '',
},
modelValue: {
type: [Object, Boolean] as PropType<String[] | Boolean>,
required: false,
default: (() => ({})) || false,
},
isDisabled: {
type: Boolean,
required: false,
default: false,
},
checkboxClass: {
type: String,
required: false,
default: '',
},
},
data() {
return {
defaultClass: 'h-4 w-4 rounded text-primary shadow-sm',
};
},
emits: ['update:modelValue'],
computed: {
model: {
get() {
return this.modelValue;
},
set(value) {
this.$emit('update:modelValue', value);
},
},
inputClass() {
if (this.isDisabled) {
return 'bg-dark-17 border-dark-13';
}
return 'bg-dark-23 border-dark-10 hover:bg-dark-25 focus:border-primary';
},
},
});
</script>
import CheckBox and use it
import CheckBox in other components;
<div>
<div v-for="(option, index) in options" :key="index">
<div
class="flex items-center justify-between p-6 py-4 border-b border-b-dark-13"
>
<div class="w-10">
<Checkbox :inputValue="option.name" v-model="selectedOptions" />
</div>
</div>
</div>
</div>
data() {
return {
selectedOptions: [],
};
},
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.