(Vue) I have problems reusing references from a composable function - vue.js

I hope it is okay that I included my full code. Otherwise it would be difficult to understand my question.
I have made a composable function for my Vue application, which purpose is to fetch a collection of documents from a database.
The composable looks like this:
import { ref, watchEffect } from 'vue'
import { projectFirestore } from '../firebase/config'
const getCollection = (collection, query) => {
const documents = ref(null)
const error = ref(null)
let collectionRef = projectFirestore.collection(collection)
.orderBy('createdAt')
if (query) {
collectionRef = collectionRef.where(...query)
}
const unsub = collectionRef.onSnapshot(snap => {
let results = []
snap.docs.forEach(doc => {
doc.data().createdAt && results.push({ ...doc.data(), id: doc.id })
})
documents.value = results
error.value = null
}, (err) => {
console.log(err.message)
document.value = null
error.value = 'could not fetch data'
})
watchEffect((onInvalidate) =>{
onInvalidate(() => unsub());
});
return {
documents,
error
}
}
export default getCollection
Then I have a component where I store the data from the database
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="info">
<h3>{‌{ playlist.title }}</h3>
<p>created by {‌{ playlist.userName }}</p>
</div>
<div class="song-number">
<p>{‌{ playlist.songs.length }} songs</p>
</div>
</div>
</div>
</template>
<script>
export default {
// receiving props
props: ['playlists'],
}
</script>
And finally, I output the data inside the main Home component, where I use the documents and error reference from the composable file.
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
</div>
</div>
</template>
<script>
import ListView from '../components/ListView.vue'
import getCollection from '../composables/getCollection'
export default {
name: 'Home',
components: { ListView },
setup() {
const { error, documents } = getCollection('playlists')
return { error, documents }
}
}
</script>
That is all well and good.
But now I wish to add data from a second collection called "books", and the idea is to use the same composable to fetch the data from that collection as well,
but the problem is that inside the Home component, I cannot use the references twice.
I cannot write:
<template>
<div class="home">
<div v-if="error" class="error">Could not fetch the data</div>
<div v-if="documents">
<ListView :playlists="documents" />
<ListView2 :books="documents" />
</div>
</div>
</template>
export default {
name: 'Home',
components: { ListView, ListView2 },
setup() {
const { error, documents } = getCollection('playlists')
const { error, documents } = getCollection('books')
return { error, documents }
}
}
This will give me an error because I reference documents and error twice.
So what I tried was to nest these inside the components themselves
Example:
<template>
<div v-for="playlist in playlists" :key="playlist.id">
<div class="single">
<div class="thumbnail">
<img :src="playlist.coverUrl">
</div>
<div class="title">
{{ playlist.title }}
</div>
<div class="description">
{{ playlist.description }}
</div>
<div>
<router-link :to="{ name: 'PlaylistDetails', params: { id: playlist.id }}">Edit</router-link>
</div>
</div>
</div>
</template>
<script>
import getCollection from '../composables/getCollection'
export default {
setup() {
const { documents, error } = getCollection('playlists')
return {
documents,
error
}
}
}
</script>
This does not work either.
I will just get a 404 error if I try to view this component.
So what is the correct way of writing this?

Try out to rename the destructed fields like :
const { error : playlistsError, documents : playlists } = getCollection('playlists')
const { error : booksError, documents : books } = getCollection('books')
return { playlistsError, playlists , booksError , books }

Related

Unable to Define Variable in Vue

I'm just starting to use VueJS & Tailwind, having never really used anything related to npm before.
I have the below code, making use of Tailwind & Headless UI which through debugging, I know I'm like 99% of the way there... except for the continuous error message
Uncaught ReferenceError: posts is not defined
I know this should be straight forward, but everything I've found either here or with Google hasn't worked. Where am I going wrong?
<template>
<Listbox as="div" v-model="selected">
<ListboxLabel class="">
Country
</ListboxLabel>
<div class="mt-1 relative">
<ListboxButton class="">
<span class="">
<img :src="selected.flag" alt="" class="" />
<span class="">{{ selected.name }}</span>
</span>
<span class="">
<SelectorIcon class="" aria-hidden="true" />
</span>
</ListboxButton>
<transition leave-active-class="" leave-from-class="opacity-100" leave-to-class="opacity-0">
<ListboxOptions class="">
<ListboxOption as="template" v-for="country in posts" :key="country" :value="country" v-slot="{ active, selected }">
<li :class="">
<div class="">
<img :src="country.flag" alt="" class="" />
<span :class="[selected ? 'font-semibold' : 'font-normal', 'ml-3 block truncate']">
{{ country.latin }}
</span>
</div>
<span v-if="selected" :class="">
<CheckIcon class="" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</template>
<script>
import { ref } from 'vue'
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '#headlessui/vue'
import { CheckIcon, SelectorIcon } from '#heroicons/vue/solid'
import axios from 'axios'
export default {
data() {
return {
response: null,
posts: undefined,
};
},
components: {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
CheckIcon,
SelectorIcon,
},
mounted: function() {
axios.get('http://localhost')
.then(response => {
this.posts = response.data;
});
},
setup() {
const selected = ref(posts[30])
return {
selected,
}
},
}
</script>
The offending line is const selected = ref(posts[30]) which I know I need to somehow define posts, but I don't get how?
CAUSE OF YOUR ERROR:
You are trying to access an array element before the array is populated. Thus the undefined error.
EXPLANATION
You are using a mix of composition api and options api. Stick to one.
I am writing this answer assuming you will pick the composition api.
Follow the comments in the below snippet;
<script>
// IMPORT ONMOUNTED HOOK
import { ref, onMounted } from 'vue'
import { Listbox, ListboxButton, ListboxLabel, ListboxOption, ListboxOptions } from '#headlessui/vue'
import { CheckIcon, SelectorIcon } from '#heroicons/vue/solid'
import axios from 'axios'
export default {
// YOU DO NOT NEED TO DEFINE THE DATA PROPERTY WHEN USING COMPOSITION API
/*data() {
return {
response: null,
posts: undefined,
};
},*/
components: {
Listbox,
ListboxButton,
ListboxLabel,
ListboxOption,
ListboxOptions,
CheckIcon,
SelectorIcon,
},
// YOU DO NOT NEED THESE LIFE CYCLE HOOKS; COMPOSITION API PROVIDES ITS OWN LIFECYCLE HOOKS
/*mounted: function() {
axios.get('http://localhost')
.then(response => {
this.posts = response.data;
});
},*/
setup() {
// YOU ARE TRYING TO ACCESS AN ELEMENT BEFORE THE ARRAY IS POPULATED; THUS THE ERROR
//const selected = ref(posts[30])
const posts = ref(undefined);
const selected = ref(undefined);
onMounted(()=>{
// CALL THE AXIOS METHOD FROM WITHIN THE LIFECYCLE HOOK AND HANDLE THE PROMISE LIKE A BOSS
axios.get('http://localhost')
.then((res) => {
selected.value = res[30];
});
});
return {
selected,
}
},
}
</script>
According to your comment; you should first check if the “selected != null” before using ‘selected’ inside the template. You can use a shorthand version like this
<img :src=“selected?.flag” />

Vue JS Pass Data From Parent To Child Of Child Of Child

In Vue.js, how to correctly pass data from parent component to a chain of multi level child components?
You have a few options:
Props
Event Bus
Vuex
Provide/Inject
Find out more here: https://www.smashingmagazine.com/2020/01/data-components-vue-js/
#RoboKozo listed some really great options for you, it just depends on what you're doing with the data.
If it's static data (and you like to be modular), you can always make use of mixins.
From the Vue.js docs:
Mixins are a flexible way to distribute reusable functionalities for Vue components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be “mixed” into the component’s own options.
Example 1: Mixins
File/Folder Setup:
/src/mixins/defaultDataMixin.js // Call this whatever you want
/src/mixins/defaultDataMixin.js:
export const default defaultData {
data() {
return {
property1: "Foo",
property2: "Bar"
}
}
};
Now, on any component you want to have access to property1 or property2, you can just import your mixin.
/src/pages/MyComponent.vue
<template>
<div id="wrapper">
<div class="container">
<div class="row">
<div class="col">
{{ property1 }}
</div>
<div class="col">
{{ property2 }}
</div>
</div>
</div>
</div>
</template>
<script>
import { defaultData } from "#/mixins/defaultDataMixin"
export default {
mixins: [defaultData],
}
</script>
Example 2: Sharing data via Props & Emit
Props down, Emit up
Perhaps you return some data in a Parent component that you wish to pass down to be used in a child component. Props are a great way to do that!
File/Folder Setup
/src/pages/ParentComponent.vue
/src/components/ChildComponent.vue
ParentComponent.vue
<template>
<div id="wrapper">
<div class="container">
<div class="row">
<div class="col">
<child-component :defaultDataProp="defaultData" #emitted-text="helloWorld">
</child-component>
</div>
</div>
</div>
</div>
</template>
<script>
import { defaultData } from "#/mixins/defaultDataMixin"
import ChildComponent from "#/components/ChildComponent.vue"
export default {
mixins: [defaultData],
components: {
ChildComponent
},
data() {
return {
emittedText: "",
}
},
methods: {
helloWorld(e){
this.emittedText = e;
}
}
}
</script>
ChildComponent.vue
<template>
<div id="wrapper">
<div class="container">
<div class="row">
<div class="col">
{{ defaultDataProp }}
</div>
</div>
<div class="row">
<div class="col">
<button #click="emitData">Emit Data</button>
</col>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
defaultDataProp: {
type: String
}
},
data() {
return {
text: "Hello, world!"
};
},
methods: {
emitData() {
this.$emit('emitted-text', this.text);
}
}
}
</script>
Cool, right?
These are just a couple of ways you can share data and functions between components.
** Edit **
Example 3: Vuex state + mapGetters
I just noticed you tagged this state as well, so I'm assuming you have already installed and are using Vuex.
While it's not "passing" the data from parent to child directly, you could have the Parent component can mutate a state that a child component is watching.
Let's imagine you want to return the user information and then have access to that data across multiple components.
File/Folder setup:
/.env
/src/main.js
/src/store/store.js
/src/store/modules/user.js
/src/mixins/defaultDataMixin.js
/src/util/api.js
/src/pages/ParentComponent.vue
/src/components/ChildComponent.vue
main.js
import Vue from "vue";
import Vuex from "vuex";
import { store } from "#/store/store";
// ... add whatever else you need to import and use
Vue.use(vuex); // but make sure Vue uses vuex
// Vue can "inject" the store into all child components
new Vue({
el: "#app",
store: store, // <-- provide the store to the vue instance
// ..
});
.env:
VUE_APP_API_URL='http://127.0.0.1:8000'
api.js:
import axios from "axios";
import { store } from "#/store/store";
const apiClient = axios.create({
baseURL: process.env.VUE_APP_API_URL,
});
export default apiClient;
user.js:
import apiClient from "#/util/api"
import { store } from "../store"
const userModule = {
state: () => ({
user: {
firstName: "",
}
}),
mutations: {
setUser(state, payload) {
state.user = payload;
}
},
actions: {
async loadUser({ commit, dispatch }) {
try {
const user = (await apiClient.get("/user")).data;
commit("setUser", user); // mutate the vuex state
} catch (error) {
dispatch("logout");
}
},
logout({ commit }) {
commit("setUser", {});
}
},
getters: {
firstName: (state) => {
return state.user.firstName;
}
}
};
export default userModule;
store.js
import Vue from "vue";
import Vuex from "vuex";
import userModule from "#/store/modules/user"; // Grab our newly created module
Vue.use(Vuex);
export const store = new Vuex.Store({
modules: {
userState: userModule,
},
});
defaultDataMixin.js
import { mapGetters } from "vuex";
export const default defaultData {
data() {
return {
property1: "Foo",
property2: "Bar"
}
},
// We can make use of mapGetters here
computed: {
...mapGetters(["firstName"]),
}
};
Now, both Parent and Child have access to the centralized state and can mutate it. For example, let's call the loadUser() from the parent and watch for the state change on the child component.
ParentComponent.vue
<template>
<div id="wrapper">
<div class="container">
<div class="row">
<div class="col" v-if="firstName != ''">
{{ firstName }}
</div>
<div class="col">
<button #click="mutateState">Click Me</button>
</div>
</div>
<div class="row">
<div class="col">
<child-component #emitted-text="helloWorld">
</child-component>
</div>
</div>
</div>
</div>
</template>
<script>
import { defaultData } from "#/mixins/defaultDataMixin"
import ChildComponent from "#/components/ChildComponent.vue"
export default {
mixins: [defaultData],
components: {
ChildComponent
},
methods: {
// our new method to mutate the user state
mutateState() {
this.$store.dispatch("loadUser");
},
helloWorld(e){
this.emittedText = e;
}
}
}
</script>
ChildComponent.vue
<template>
<div id="wrapper">
<div class="container">
<div class="row">
<div class="col">
{{ defaultData }}
</div>
</div>
<div class="row">
<div class="col">
<button #click="emitData">Emit Data</button>
</col>
</div>
</div>
</div>
</template>
<script>
import { defaultData } from "#/mixins/defaultDataMixin"
export default {
mixins: [defaultData],
data() {
return {
text: "Hello, world!"
};
},
methods: {
emitData() {
this.$emit('emitted-text', this.text);
}
},
// Let's watch for the user state to change
watch: {
firstName(newValue, oldValue) {
console.log('[state] firstName state changed to: ', newValue);
// Do whatever you want with that information
}
}
}
</script>

Get URL query parameters in Vue 3 on Component

i am new to Vue JS, i must say i really love this platform. I started using it just 3 days back. I am just trying to get the URL query parameter and i am using vue-router as well. Here is how i have it:
http://localhost:8001/login?id=1
Here is how my controller look like.
<template>
<section class="contact-info-area">
<div class="container">
<div class="row">
<div class="col-lg-12">
<section class="write-massage-area">
<div class="mb-5"></div>
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="write-massage-content">
<div class="write-massage-title text-center">
<h3 class="title">Login {{ $route.query.id }}</h3> <!-- THIS IS WORKING -->
</div>
<div class="write-massage-input">
<form #submit.prevent="onSubmitForm">
<div class="row">
<div class="col-lg-12">
<div class="input-box mt-10">
<input type="text" placeholder="Email" v-model="form['email']">
<ErrorMessage :validationStatus="v$.form.email"></ErrorMessage>
</div>
</div>
<div class="col-lg-12">
<div class="input-box mt-10">
<input type="text" placeholder="Password" v-model="form['password']">
</div>
</div>
<div class="col-lg-12">
<div class="input-box mt-10 text-center">
<button type="submit" class="main-btn main-btn-2">Login</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="mb-5"></div>
</section>
</div>
</div>
</div>
</section>
</template>
<script>
import { ref } from 'vue'
import useVuelidate from '#vuelidate/core'
import { required, minLength, email } from '#vuelidate/validators'
import ErrorMessage from '../components/ErrorMessage'
export default {
name: "login",
components: {
ErrorMessage
},
created(){
},
setup(){
const form = ref({})
const rules = {
form: {
email: {
required,
email
},
password: {
required,
minLength : minLength(5)
}
}
}
const v$ = useVuelidate(rules, { form })
function onSubmitForm(){
console.log(this.$route.query.id) **<!-- THIS IS NOT WORKING -->**
v$.value.$touch()
console.log(form.value)
}
return { form, onSubmitForm, v$}
}
}
</script>
here on the above code. On button submit i am going to a function called onSubmitForm, there i am using console.log(this.$route.query.id) but this is giving a below error :
Uncaught TypeError: Cannot read property 'query' of undefined
at Proxy.onSubmitForm (Login.vue?a55b:84)
Why this is happening? I am seeing that in the vue document as well, they mentioned in the same way. What i am missing in this?
Thank you!
You can call useRoute to access the query params...
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.query)
</script>
If you use parameters and your endpoint looks something like this:
{
path: '/inspect_detail/:id',
name: 'inspect_detail',
component: function() {
return import ( '../views/Inspect_detail.vue')
},
params: true
},
and you are routing like this:
<router-link :to="{ name: 'inspect_detail', params: { id: akey }}">...</router-link>
then you can pickup the values like this:
<script>
import { useRoute } from 'vue-router';
export default {
setup(){
const route = useRoute()
console.log( route.params );
}
}
</script>
Bit late but if you want query params to work on page refresh you have to wait for the router to get ready as its asynchronous. The router wont be ready if its a page refresh. The router will be ready if its navigation from another page in that case the query params will be available from the begining.
<script setup>
import { onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
//just to demonstrate
console.log(route.query)
// If it is a page refresh it will print null.
// The router wont be ready if its a page refresh. In that case the query wont be available.
// The router will be ready if its navigation from another page in that case the query params will be available
onMounted(() => {
getUrlQueryParams()
});
getUrlQueryParams = async () => {
//router is async so we wait for it to be ready
await router.isReady()
//once its ready we can access the query params
console.log(route.query)
});
</script>
I hadn't done frontend in a couple of months and had to refresh on Vue 3 and route query properties, and this question came up first.
Now the memory jog has kicked in, I believe the correct way is to pass the props from the router to the component as shown in the examples here https://router.vuejs.org/guide/essentials/passing-props.html
Essentially, call your route
{
path: "/login",
name: "Login",
props: route => ({ id: route.query.id }),
component: () => import(/* webpackChunkName: "login" */ "../views/Login.vue"),
},
to be able to access the id field.
Alternately, you can sent the whole lot with props: route => ({ query: route.query })
Pick it up as a prop in your view/component
export default {
name: "login",
components: {
ErrorMessage
},
created(){
},
props: {
id: {
type: String,
default: "",
}
}
setup(props){
const form = ref({})
const rules = {
form: {
email: {
required,
email
},
password: {
required,
minLength : minLength(5)
}
}
}
const v$ = useVuelidate(rules, { form })
function onSubmitForm(){
console.log(props.id)
v$.value.$touch()
console.log(form.value)
}
return { form, onSubmitForm, v$}
}
}

How to had a conditional render with a v-if in nuxt.js

I have an application that displays companies and their documents.
I would like to display a main title if no application is selected, and display the content of the application and removing the main title if an application is selected.
<template>
<div class="container">
<div v-if="docsAppDisplayed === false">
<Logo />
<h1 class="title">
<em class="text-red-900 font-bold">D</em>ocs<em class="text-yellow-700 font-bold">C</em>loud<em class="text-purple-900 font-bold">M</em>anager
</h1>
</div>
<div>
<DocumentationCard />
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data () {
return {
docsAppDisplayed: false
}
},
computed: {
...mapGetters(['applications', 'selectedApp', 'documentations', 'selectedDoc'])
},
async mounted () {
await this.$store.dispatch('getApplications', 'getDocumentations')
},
methods: {
selectApplications (id) {
this.$store.dispatch('selectedApp', id)
this.docsAppDisplayed = true
}
}
}
</script>
I don't know why my method doesn't work. An idea, please?
I specify that I have a sidebar that returns my company names and that when I click on it I can access their documents. My problem only concerns the display of the (logo + title) or my DocumentCard.
This never seems to get invoked by your component.
selectApplications (id) {
this.$store.dispatch('selectedApp', id)
this.docsAppDisplayed = true
}
Where is id coming from? Is it a route parameter, like https://example.com/myroute/1234 where 1234 is the id? Without knowing much about your code, my guess is, you're looking to do something like this:
export default {
data () {
return {
docsAppDisplayed: false
}
},
computed: {
...mapGetters(['applications', 'selectedApp', 'documentations', 'selectedDoc'])
},
async mounted () {
await this.$store.dispatch('getApplications', 'getDocumentations')
selectApplications(this.$route.params.id) // THE BIG CHANGE
},
methods: {
selectApplications (id) {
this.$store.dispatch('selectedApp', id)
this.docsAppDisplayed = true
}
}
}
You also might want to add a v-else to the div wrapping <DocumentationCard />.
<template>
<div class="container">
<div v-if="!docsAppDisplayed">
<Logo />
<h1 class="title">
<em class="text-red-900 font-bold">D</em>ocs<em class="text-yellow-700 font-bold">C</em>loud<em class="text-purple-900 font-bold">M</em>anager
</h1>
</div>
<div v-else>
<DocumentationCard />
</div>
</div>
</template>

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.