Firestore: Unable to fetch nested documents. Why? - javascript

I'm working on a Vue (with Vuex) app, with a firebase/firestore backend, and I'm having trouble with fetching documents referenced by other documents. Specifically, I have a recipes collection (together with users and comments collections as seen in the linked photo) collection, with each contained document having, among others, addedBy and comments fields. Both are id strings (the comments field being an array of ids) of the respective documents referenced. Now, I'm not sure if this is the best way of going about it, but coming from a MongoDB background, I thought it'd be possible fetch the details of these fields like we do with MongoDB.
I have had a couple of tries but nothing seems to work. An example of this is seen in the code snippets below.
Main Recipe Component/Container (I query the DB for a specific recipe document)
<template>
<div class="recipe-detail">
<loader v-if="isLoading" message="Loading Recipe" size="huge" />
<div v-else-if="!recipe" class="no-recipe">
No such recipe in DB
</div>
<div v-else class="comments-and-similar">
<div class="comments">
<h3 class="comments-title">Comments</h3>
<comment-form />
<comment-list :comment-list="recipe.comments" />
</div>
<div class="similar-recipes">
<similar-recipes />
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import Loader from "#/components/shared/Loader";
import PostedBy from "#/components/recipes/detail/PostedBy";
import CommentForm from "#/components/forms/CommentForm";
import CommentList from "#/components/recipes/detail/CommentList";
export default {
name: "recipe-detail",
components: {
Loader,
PostedBy,
CommentForm,
CommentList,
},
data() {
return {
recipeId: this.$route.params.recipeId,
fullPath: this.$route.fullPath
};
},
computed: {
...mapGetters(["isLoading"]),
...mapGetters({ recipe: "recipes/recipe" }),
},
watch: {
"$route.params.recipeId"(id) {
this.recipeId = id;
}
},
methods: {
...mapActions({ getRecipeById: "recipes/getRecipeById" })
},
created() {
if (!this.recipe || this.recipe.id !== this.recipeId) {
this.getRecipeById(this.recipeId);
}
}
};
</script>
<style lang="scss" scoped>
</style>
Comment List Component (Here, I receive the comment id list via props)
<template>
<section class="comments">
<div v-if="commentList.length === 0">Be the first to comment on recipe</div>
<template v-else v-for="comment in commentList">
<comment :comment-id="comment" :key="comment" />
</template>
</section>
</template>
<script>
import Comment from "./Comment";
export default {
name: "comment-list",
components: {
Comment
},
props: {
commentList: {
type: Array,
required: true
}
}
};
</script>
<style lang="scss" scoped>
</style>
Comment Component
<template>
<article>
<div v-if="isLoading">Loading comment...</div>
<div v-else>{{ JSON.stringify(comment) }}</div>
</article>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
name: "comment",
props: {
commentId: {
type: String,
required: true
}
},
computed: {
...mapGetters(["isLoading"]),
...mapGetters({ comment: "recipes/comment" })
},
methods: {
...mapActions({ getCommentById: "recipes/getCommentById" })
},
created() {
this.getCommentById(this.commentId);
}
};
</script>
Now, the Comment component is where I'm having trouble. I get each individual comment id and use it to query the DB, specifically the comments collection. I actually get the comment detail body from the DB, this query wont stop and results in an infinite loop. I have to comment out the method inside created life-cycle for it to stop. I tried the same approach for the addedBy field to query for the user and got the same issue. So, what I'm I doing wrong.
DB structure
PS: I did not feel the need to include the Vuex methods (actions) in order to reduce verbosity. They work just fine sending the corresponding queries.

It looks like you're sharing an isLoading flag between all your components.
I believe what is happening is this:
You try to load a comment and isLoading is set to true.
The component recipe-detail re-renders to show the Loading Recipe message. Note that this will destroy the comment-list.
When the comment finishes loading isLoading will be set back to false.
recipe-details will re-render again, this time showing the comment-list. This will create a new set of comment components, each of which will try to load their data again. This jumps us back to step 1.
On an unrelated note, it looks like your comment component is relying on a single comment being held in the store. This might be fine when there's only one comment but when there are multiple comments they'll all load at the same time and only one of them will ultimately end up in the store.

Related

VUEjs 3 Warning: Maximum recursive updates exceeded

currently I am facing the problem that Vue issues a warning:
"Maximum recursive updates exceeded. This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function."
I can't figure out where the error should be.
I iterate over 2 loops and then want to give a counter value to the component inside, which is then passed on and then interpreted by modulus to CSS classes in 3rd level.
Unfortunately I have to do it this way, because the created components are parts of a dynamically created CSS grid. I would like to provide virtually every row, so all cells at the same height with a uniform "even/odd" class.
Here is the vue-component that creates this increment:
<template>
<template v-for="(project, p) in projects" :key="project.id">
<template v-for="(component, c) in project.components" :key="component.id">
<grid-swim-lane
:project="project"
:component="component"
:grid-row-even-odd-count="evenOddCount++"
/>
</template>
</template>
</template>
<script>
import GridSwimLane from "./GridSwimLane";
import {mapGetters} from "vuex";
export default {
components: { GridSwimLane },
data() {
return {
evenOddCount: -1
}
},
computed: {
...mapGetters('projects', { projects: 'getAllProjects' })
},
}
</script>
<style scoped></style>
This increment value is generated and successfully passed through to the last component despite the warning. But how can I make it work without the warning?
I have already tried a few things. But I can't get any further.
I can't do this with CSS selectors, because I want to work with fixed classes.
Thanks in advance for your tips.
I figured out, the each grid-swim-lane component's this.$.uid value is not sequential, but in sequence even and odd :-)
So i use this value to determine the 'even' and 'odd' css-class:
<template>
<!-- ------------------ BEGIN LEFT SIDE BAR ------------------ -->
<grid-swim-lane-info
:grid-row-even-odd-count="gridRowEvenOddCount"
/>
<!-- ------------------ BEGIN RELEASES ------------------- -->
<grid-swim-lane-releases
:grid-row-even-odd-count="gridRowEvenOddCount"
/>
</template>
<script>
import GridSwimLaneInfo from "./GridSwimLaneInfo";
import GridSwimLaneReleases from "./GridSwimLaneReleases";
export default {
components: {
GridSwimLaneInfo, GridSwimLaneReleases
},
props: {
component: { type: Object, default: { id: 0, name: 'no-component'} }
},
data() {
return {
gridRowEvenOddCount: 0
}
}
mounted() {
this.gridRowEvenOddCount = this.$.uid;
}
}
</script>
<style scoped></style>

Vue.js on render populate content dynamically via vue.router params

AS title sates, I don't so much need a solution but I don't understand why I'm getting the undesired result;
running v2 vue.js
I have a vue component in a single component file.
Basically the vue should render data (currently being imported from "excerciseModules" this is in JSON format).
IT's dynamic so based on the url path it determines what to pull out of the json and then load it in the page, but the rendering is being done prior to this, and I'm unsure why. I've created other views that conceptually do the samething and they work fine. I dont understand why this is different.
I chose the way so I didn't have to create a ton of routes but could handle the logic in one view component (this one below).
Quesiton is why is the data loading empty (it's loading using the empty "TrainingModules" on first load, and thereafter it loads "old" data.
Example url path is "https...../module1" = page loads empty
NEXT
url path is "https..../module 2" = page loads module 1
NEXT
url path is "https..../module 1" = page loads module 2
//My route
{
path: '/excercises/:type',
name: 'excercises',
props: {
},
component: () => import( /* webpackChunkName: "about" */ '../views/training/Excercises.vue')
}
<template>
<div class="relatedTraining">
<div class="white section">
<div class="row">
<div class="col s12 l3" v-for="(item, index) in trainingModules" :key="index">
<div class="card">
<div class="card-content">
<span class="card-title"> {{ item.title }}</span>
<p>{{ item.excercise }}</p>
</div>
<div class="card-action">
<router-link class="" to="/Grip">Start</router-link>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
console.log('script');
let trainingModules; //when initialized this is empty, but I would expect it to not be when the vue is rendered due to the beforeMount() in the component options. What gives?
/* eslint-disable */
let init = (params) => {
console.log('init');
console.log(trainingModules);
trainingModules = excerciseModules[params.type];
//return trainingModules
}
import { getRandom, randomImage } from '../../js/functions';
import { excerciseModules } from '../excercises/excercises_content.js'; //placeholder for JSON
export default {
name: 'excercises',
components: {
},
props: {
},
methods: {
getRandom,
randomImage,
init
},
data() {
return {
trainingModules,
}
},
beforeMount(){
console.log('before mount');
init(this.$route.params);
},
updated(){
console.log('updated');
},
mounted() {
console.log('mounted');
//console.log(trainingModules);
}
}
</script>
I can't tell you why your code is not working because it is an incomplete example but I can walk you through a minimal working example that does what you are trying to accomplish.
The first thing you want to do, is to ensure your vue-router is configured correctly.
export default new Router({
mode: "history",
routes: [
{
path: "/",
component: Hello
},
{
path: "/dynamic/:type",
component: DynamicParam,
props: true
}
]
});
Here I have a route configured that has a dynamic route matching with a parameter, often called a slug, with the name type. By using the : before the slug in the path, I tell vue-router that I want it to be a route parameter. I also set props: true because that enables the slug value to be provided to my DynamicParam component as a prop. This is very convenient.
My DynamicParam component looks like this:
<template>
<div>
<ul>
<li v-for="t in things" :key="t">{{ t }}</li>
</ul>
</div>
</template>
<script>
const collectionOfThings = {
a: ["a1", "a2", "a3"],
b: ["b1", "b2"],
c: [],
};
export default {
props: ["type"],
data() {
return {
things: [],
};
},
watch: {
type: {
handler(t) {
this.things = collectionOfThings[t];
},
immediate: true,
},
},
};
</script>
As you can see, I have a prop that matches the name of the slug available on this component. Whenever the 'slug' in the url changes, so will my prop. In order to react to those changes, I setup a watcher to call some bit of code. This is where you can make your fetch/axios/xhr call to get real data. But since you are temporarily loading data from a JSON file, I'm doing something similar to you here. I assign this data to a data value on the component whenever the watcher detects a change (or the first time because I have immediate: true set.
I created a codesandbox with a working demo of this: https://codesandbox.io/s/vue-routing-example-forked-zesye
PS: You'll find people are more receptive and eager to help when a minimal example question is created to isolate the problematic code. You can read more about that here: https://stackoverflow.com/help/minimal-reproducible-example

Pass the clicked element's data by using eventbus in Vue.js

Im pretty new to Vue.js, thank you for your understanding. Im setting up a Vue project where I want to show Patients and their data. I want to tell from the beginning that Im not planning to use Vuex :)
My project has 3 layers.
Home.vue file where I import the data (patients)
Next layer is Patients.vue component where I have a for loop and output all the patients. In this case, I am getting the patient Array by using props.
And the last layer is called ViewPatient.vue view. What I want to do here is showing more details of the clicked Patient. I want to inherit for example the name to make one more call to the endpoint to retrieve some observations of the patient. For example: endpoint/patient/(theName) <-- the name should come from the previous Patients.vue component.
I tried a lot of different approaches: eventbus, dynamic router and data-attrbutes.
Home.vue
<template>
<div class="container">
<keep-alive>
<Patients :PatientsData="PatientsData" />
</keep-alive>
</div>
</template>
<script>
// # is an alias to /src
import PatientsData from "../data/messages";
import Patients from "../components/Patients.vue";
export default {
name: "home",
data() {
return {
PatientsData: PatientsData
};
},
components: {
Patients
}
};
</script>
Patients.vue (component)
<template>
<div v-if="PatientsData.length > 0">
<div class="row row-eq-height">
<div v-for="PatientData in PatientsData" class="col-12 col-sm-6 col-md-3 mb-3" :key="PatientData.content" :data-id="PatientData.content" #click.prevent="passPatientData" >
<router-link to="/patient" >
<div class="col-12 patientsTiles p-4">
<p class="patientsName">
<span>Navn</span>
{{ PatientData.content }}
</p>
<p class="patientsCPR">
<span>CPR.nr</span>
{{ PatientData.subject }}
</p>
<p class="patientsAge">
<span>Alder</span>
{{PatientData.age}}
</p>
<i :class="['fa', 'fa-star', {important: PatientData.isImportant}]"></i>
</div>
</router-link>
</div>
</div>
</div>
</template>
<script>
import router from "../main";
import { eventBus } from "../main";
export default {
props: {
PatientsData: Array,
},
data(){
return{
patientName: ""
}
},
methods: {
passPatientData() {
this.patientName = this.PatientData;
alert(this.patientName);
eventBus.$emit("passPatientData", this.patientName);
}
}
};
</script>
ViewPatient.vue (view)
<template>
<div class="container">
<h1>The Patient detail</h1>
</div>
</template>
<script>
// # is an alias to /src
import { eventBus } from "../main";
export default {
props: {
// patientId:{
// type: String
// }
},
data() {
return {
selectedPatient : ""
};
},
created() {
eventBus.$on("passPatientData", data => {
this.selectedPatient = data;
// console.log("yeaah");
})}
}
</script>
IMO, the problem is lying on the passPatientData function.
this.PatientData is empty and I dont know how to pass the clicked element's data to the empty string (this.patientName), so I can emit it to the eventbus
passPatientData() {
this.patientName = this.PatientData;
alert(this.patientName);
eventBus.$emit("passPatientData", this.patientName);
}
Here is my approach which worked for me (few changes):
#bbsimonbb, thank you for the answer, but I´m not going to use that approach, because its a bit overkill when compared to my small task.
In Patients.vue while looping patients, I have modified the click event:
Im actually passing the single element that is being clicked, which solved med a lot of time.
<div v-for="PatientData in storedPatients" class="col-12 col-sm-6 col-md-3 mb-3" :data-id="PatientData.content" #click="passPatientData(PatientData)" >
Before:
#click="passPatientData"
After:
#click="passPatientData(PatientData)"
And then in my event bus im able to "work" with the data im passing:
methods: {
passPatientData(element) {
this.patientName = element.content;
alert(this.patientName);
eventBus.$emit("passPatientData", this.patientName);
}
}
The purpose is to pass the patientName to ViewPatient.vue file by using eventbus and call a new endpoint which looks like this: endpoint/patient/(patientName) . The result of the endpoint will then be the details of the single patient that has been clicked in patients.vue
It´s working. Hope it can be useful for others that is struggling with the same issue.
You've decided not to use Vuex. That doesn't mean you shouldn't use a store. The simplest store is just an object in the data of your root Vue, passed around via provide/inject. Put your shared data in there.
This is a much simpler pattern to get your head around than an event bus. You get the shared state right out of Vue components, and because it's just an object, that does nothing but store state, you will be more in control of what you have and how it's working.
// Home.vue
export default {
name: "home",
provide: { mySharedData: this.mySharedData },
data() {
return {
mySharedData: {
patientData: {}
}
};
},
created() {
fetch("http://patientData")
.then(response.json)
.then((patientData) => this.mySharedData.patientData = patientData)
}
...
// Then, in all other components of your app...
export default {
inject: ['mysharedData'],
...
}
This way, you'll be making full use of Vue reactivity to propagate your changes. You'll need to understand how Vue makes properties reactive. In particular, you can't just assign new props in mySharedData. The simplest way is to make sure all the props are there to start with. To do it on the fly, use Vue.set().

Props passed into a a component do not render

I am passing some data values into a component but when I try to render that component, the data is not received correctly. I get an error stating:
Cannot read property 'title' of undefined at Proxy.render (eval at
./node_modules/cache-loader/dist/cjs.js?
I am sure the data I am attempting to receive is correct because after a second the data loads on the page correctly. However I need the data to load on page load because I plan on implementing an edit feature. I have done things similar to this throughout my project in different sections and it works perfectly. I am not sure why an error is being thrown here though.
I have attempted to change at what point the data is recognized, passing in concrete data values, etc. But nothing has worked.
Component:
<template>
<div class="column">
<h1 class="title">{{ this.relevantData.title }}</h1>
<div class="buttons are-large">
<a class="button is-info is-outlined" #click="journalPush()">Journal</a>
<a class="button is-success">Edit</a>
</div>
</div>
</template>
<script>
import router from "#/router.js";
export default {
props: ['relevantData'],
methods: {
journalPush() {
router.push("/recipes/" + this.relevantData.documentID + "/journal");
}
},
mounted() {
console.log(this.relevantData);
}
}
</script>
Parent:
<template>
<section class="overall">
<div class="is-mobile">
<div class="columns">
<Content :relevantData="sidebarData"/>
</div>
</div>
</section>
</template>
<script>
// # is an alias to /src
import Content from '#/components/recipes/contentComponent.vue';
import { db } from '#/main.js'
export default {
data() {
return {
sidebarData: null,
}
},
components: {
Sidebar,
},
created(){
db.collection('recipes').orderBy('dateMade', 'desc').get()
.then((snapshot) => {
this.sidebarData = snapshot.docs.map(doc => doc.data());
})
}
}
I expect when I log relevantData in the component in the mounted section, I should be able to see all the data immediately.
If you need the data to exist in the mounted hook then you'll need to use a v-if to delay the creation of the component:
<Content v-if="sidebarData" :relevantData="sidebarData"/>
Without the v-if the Content component will be created and rendered immediately, before the sidebarData has loaded from your server.

Change a property's value in one component from within another component

I'm trying to wrap my head around hoe Vue.js works, reading lots of documents and tutorials and taking some pluralsight classes. I have a very basic website UI up and running. Here's the App.vue (which I'm using kinda as a master page).
(To make reading this easier and faster, look for this comment: This is the part you should pay attention to)...
<template>
<div id="app">
<div>
<div>
<CommandBar />
</div>
<div>
<Navigation />
</div>
</div>
<div id="lowerContent">
<!-- This is the part you should pay attention to -->
<template v-if="showLeftContent">
<div id="leftPane">
<div id="leftContent">
<router-view name="LeftSideBar"></router-view>
</div>
</div>
</template>
<!-- // This is the part you should pay attention to -->
<div id="mainPane">
<div id="mainContent">
<router-view name="MainContent"></router-view>
</div>
</div>
</div>
</div>
</template>
And then in the same App.vue file, here's the script portion
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import CommandBar from './components/CommandBar.vue';
import Navigation from './components/Navigation.vue';
#Component({
components: {
CommandBar,
Navigation,
}
})
export default class App extends Vue {
data() {
return {
showLeftContent: true // <--- This is the part you should pay attention to
}
}
}
</script>
Ok, so the idea is, one some pages I want to show a left sidebar, but on other pages I don't. That's why that div is wrapped in <template v-if="showLeftContent">.
Then with the named <router-view>'s I can control which components get loaded into them in the `router\index.ts\ file. The routes look like this:
{
path: '/home',
name: 'Home',
components: {
default: Home,
MainContent: Home, // load the Home compliment the main content
LeftSideBar: UserSearch // load the UserSearch component in the left side bar area
}
},
So far so good! But here's the kicker. Some pages won't have a left side bar, and on those pages, I want to change showLeftContent from true to false. That's the part I can't figure out.
Let's say we have a "Notes" component that looks like this.
<template>
<div class="notes">
Notes
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
data() {
return {
showLeftContent: false // DOES NOT WORK
}
}
}
</script>
Obviously, I'm not handling showLeftContent properly here. It would seem as if the properties in data are scoped only to that component, which I understand. I'm just not finding anything on how I can set a data property in the App component and then change it in a child component when that child is loaded through a router-view.
Thanks!
EDIT:
I changed the script section of the Notes component from:
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
data() {
return {
showLeftContent: false // DOES NOT WORK
}
}
}
</script>
to:
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
#Component
export default class Notes extends Vue {
mounted() {
this.$root.$data.showLeftContent = false;
}
}
</script>
And while that didn't cause any compile or runtime errors, it also didn't have the desired effect. On Notes, the left side bar still shows.
EDIT 2:
If I put an alert in the script section of the Notes component:
export default class Notes extends Vue {
mounted() {
alert(this.$root.$data.showLeftContent);
//this.$root.$data.showLeftContent = false;
}
}
The alert does not pop until I click on "Notes" in the navigation. But, the value is "undefined".
EDIT 3:
Struggling with the syntax here (keep in mind this is TypeScript, which I don't know very well!!)
Edit 4:
Inching along!
export default class App extends Vue {
data() {
return {
showLeftContent: true
}
}
leftContent(value: boolean) {
alert('clicked');
this.$root.$emit('left-content', value);
}
}
This does not result in any errors, but it also doesn't work. The event never gets fired. I'm going to try putting it in the Navigation component and see if that works.
As it says on #lukebearden answer you can use the emit event to pass true/false to the main App component on router-link click.
Assuming your Navigation component looks like below, you can do something like that:
#Navigation.vue
<template>
<div>
<router-link to="/home" #click.native="leftContent(true)">Home</router-link> -
<router-link to="/notes" #click.native="leftContent(false)">Notes</router-link>
</div>
</template>
<script>
export default {
methods: {
leftContent(value) {
this.$emit('left-content', value)
}
}
}
</script>
And in your main App you listen the emit on Navigation:
<template>
<div id="app">
<div>
<Navigation #left-content="leftContent" />
</div>
<div id="lowerContent">
<template v-if="showLeftContent">
//...
</template>
<div id="mainPane">
//...
</div>
</div>
</div>
</template>
<script>
//...
data() {
return {
showLeftContent: true
}
},
methods: {
leftContent(value) {
this.showLeftContent = value
}
}
};
</script>
A basic approach in a parent-child component relationship is to emit events from the child and then listen and handle that event in the parent component.
However, I'm not sure that approach works when working with the router-view. This person solved it by watching the $route attribute for changes. https://forum.vuejs.org/t/emitting-events-from-vue-router/10136/6
You might also want to look into creating a simple event bus using a vue instance, or using vuex.
If you'd like to access the data property (or props, options etc) of the root instance, you can use this.$root.$data. (Check Vue Guide: Handling Edge)
For your codes, you can change this.$root.$data.showLeftContent to true/false in the hook=mounted of other Components, then when Vue creates instances for those components, it will show/hide the left side panel relevantly.
Below is one demo:
Vue.config.productionTip = false
Vue.component('child', {
template: `<div :style="{'background-color':color}" style="padding: 10px">
Reach to root: <button #click="changeRootData()">Click me!</button>
<hr>
<slot></slot>
</div>`,
props: ['color'],
methods: {
changeRootData() {
this.$root.$data.testValue += ' :) '
}
}
})
new Vue({
el: '#app',
data() {
return {
testValue: 'Puss In Boots'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<h2>{{testValue}}</h2>
<child color="red"><child color="gray"><child color="green"></child></child></child>
</div>

Categories

Resources