Application gvies me a "Cannot read property" error, but only the layout is affected - vue.js

I am really scratching my head at this.
I am making a CRUD application, and this problem started when I was working on the Edit component.
I am getting the error Cannot read property 'id' of null
BUT! The interesting thing is that the data actually DOES get updated, both in the application and on the server side.
The error however affects the layout. First of all, the delete button appears two places in the template instead of one, and instead of redirecting me to the main page when I update, the main page appears like a new div on the edit page. I have no idea what is going on.
Here are the different components/composables:
The Details component: Here the information about a specific document is stored based on it's ID.
<template>
<div v-if="playlist" class="playlist-details">
<div class="playlist-info">
<div class="cover">
<img :src="playlist.coverUrl">
</div>
<h2> {{ playlist.title }}</h2>
<p> {{ playlist.description }} </p>
</div>
</div>
<button #click="handleDelete">Delete</button>
<EditSong :playlist="playlist" />
</template>
<script>
import EditSong from '../components/EditSong'
import useDocument from '../composables/useDocument'
import getDocument from '../composables/getDocument'
import useStorage from '../composables/useStorage'
import { useRouter } from "vue-router";
export default {
props: ['id'],
components: { EditSong },
setup(props) {
const { document: playlist } = getDocument('playlists', props.id)
const { deleteDoc } = useDocument('playlists', props.id)
const router = useRouter();
const { deleteImage } = useStorage()
const handleDelete = async () => {
await deleteImage(playlist.value.filePath)
await deleteDoc()
confirm('Do you wish to delete this content?')
router.push({ name: "Home" });
}
return {
playlist,
handleDelete
}
}
}
</script>
Here is the Edit component: This is where I edit and update the data inside the Details component. This is where I am getting the TypeError.
It has something to do with the props.playlist.id field
<template>
<div class="edit-song">
<form #submit.prevent="handleSubmit">
<input type="text" required placeholder="title" v-model="title">
<input type="text" required placeholder="description" v-model="description">
<button v-if="!isPending">Update</button>
<button v-else disabled>Updating...</button>
</form>
</div>
</template>
<script>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import useDocument from '../composables/useDocument'
import useCollection from '../composables/useCollection'
export default {
props: ['playlist'],
setup(props) {
const title = ref('')
const description = ref('')
const { updateDoc } = useDocument('playlists', props.playlist.id)
const { error } = useCollection()
const isPending = ref(false)
const router = useRouter();
const handleSubmit = async () => {
await updateDoc({
title: title.value,
description: description.value,
})
isPending.value = false
if(!error.value) {
router.push({ name: "Home" })
}
}
return {
title,
description,
handleSubmit,
isPending,
error
}
}
}
</script>
And last, this is the Update composable: that stores the update function
import { ref } from 'vue'
import { projectFirestore } from '../firebase/config'
const useDocument = (collection, id) => {
const error = ref(null)
const isPending = ref(false)
let docRef = projectFirestore.collection(collection).doc(id)
const updateDoc = async (updates) => {
isPending.value = true
error.value = null
try {
const res = await docRef.update(updates)
isPending.value = false
return res
}catch(err) {
console.log(err.message)
isPending.value = false
error.value = 'Could not update document'
}
}
return {
error,
isPending,
updateDoc
}
}
export default useDocument

The likely scenario is getDocument() returns a ref to null for document, which gets updated asynchronously:
const getDocument = (collection, id) => {
const document = ref(null)
someAsyncFunc(() => {
document.value = {...}
})
return {
document
}
}
Since the document (renamed to playlist) is bound to the EditSong component, it receives both the initial value (null) and then the asynchronously populated value, which leads to the behavior you're seeing.
One solution is to conditionally render EditSong on playlist:
<EditSong v-if="playlist" :playlist="playlist" />
Another is to move the updateDoc initialization into handleSubmit, and add a null-check there:
const handleSubmit = async () => {
if (!props.playlist) return
const { updateDoc } = useDocument('playlists', props.playlist.id)
await updateDoc(...)
}

Related

Mixing Vue Composables with Class Based Components

I'm attempting to mix composables into a class based component setup, as part of a slow migration from Vue 2 to Vue 3. However, I am struggling to referenced return values from the setup function within the class itself.
I have something similar to:
#Component({
setup() {
const fullscreenElement = ref<HTMLElement | undefined>();
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen(fullscreenElement);
return {
fullscreenElement,
isFullscreen,
toggleFullscreen,
};
},
})
export default class MyClassComponent extends Vue {
// How to access isFullscreen et al. here ??
}
As in the above example, I can't seem to reference how I would use e.g., isFullscreen etc from within the component itself?
Docs:
ref()
Computed Properties
Composables
const { ref, computed, createApp } = Vue;
const useFullscreen = function() {
const _isFullscreen = ref(false);
const isFullscreenFunc = function() {
return _isFullscreen;
}
const isFullscreenComputed = computed(function() {
return _isFullscreen;
})
const toggleFullscreen = function() {
_isFullscreen.value = !_isFullscreen.value;
}
return {isFullscreenFunc, isFullscreenComputed, toggleFullscreen}
}
const MyComponent = {
setup() {
const { isFullscreenFunc, isFullscreenComputed, toggleFullscreen } = useFullscreen();
return {
toggleFullscreen,
isFullscreenFunc,
isFullscreenComputed
}
},
methods: {
toggle() {
this.toggleFullscreen();
},
show() {
alert(`isFullscreenFunc: ${this.isFullscreenFunc().value}\n isFullscreenComputed: ${this.isFullscreenComputed.value}`);
}
},
template: `
<div>
isFullscreenFunc: {{isFullscreenFunc().value}}<br /><br />
isFullscreenComputed: {{isFullscreenComputed.value}}
<br/><br/><button type="button" #click="toggle()">toggle</button>
<button type="button" #click="show()">show</button>
</div>`
}
const App = {
components: {
MyComponent
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<my-component>
</my-component>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>

Why content of child component with props is not rendered on the page?

In vuejs3 app
I read data with axios request from backend API. I see that data are passed to internal
component, but I do not see content of the child component is rendered on the page.
Parent component:
<template>
<div class="row m-0 p-0" v-show="forumCategories.length && isPageLoaded">
<div v-for="(nextActiveForumCategory, index) in forumCategories" :key="nextActiveForumCategory.id" class="col-sm-12 col-md-6 p-2 m-0">
index::{{ index}}
<forum-category-block
:currentLoggedUser="currentLoggedUser"
:nextActiveForumCategory="nextActiveForumCategory"
:index="index"
:is_show_location="true"
></forum-category-block>
</div>
</div>
</template>
<script>
import ForumCategoryBlock from '#/views/forum/ForumCategoryBlock.vue'
import { useStore } from 'vuex'
export default {
name: 'forumsByCategoryPage',
components: {
ForumCategoryBlock,
},
setup () {
const store = useStore()
const orderBy = ref('created_at')
const orderDirection = ref('desc')
const forumsPerPage = ref(20)
const currentPage = ref(1)
let forumsTotalCount = ref(0)
let forumCategories = ref([])
let isPageLoaded = ref(false)
let credentialsConfig = settingCredentialsConfig
const currentLoggedUserToken = computed(
() => {
return store.getters.token
}
)
const currentLoggedUser = computed(
() => {
return store.getters.user
}
)
const forumsByCategoryPageInit = async () => {
loadForums()
}
function loadForums() {
isPageLoaded = false
let credentials = getClone(credentialsConfig)
credentials.headers.Authorization = 'Bearer ' + currentLoggedUserToken.value
let filters = { current_page: currentPage.value, order_by: orderBy.value, order_direction: orderDirection.value }
const apiUrl = process.env.VUE_APP_API_URL
axios.get(apiUrl + '/forums-by-category', filters, credentials)
.then(({ data }) => {
console.log('/forums-by-category data::')
console.log(data)
forumCategories.value = data.forumCategories
forumsTotalCount.value = data.forumsTotalCount
isPageLoaded = true
console.log('++forumCategories::')
console.log(forumCategories)
})
.catch(error => {
console.error(error)
isPageLoaded = true
})
} // loadForums() {
onMounted(forumsByCategoryPageInit)
return {
currentPage, orderBy, orderDirection, isPageLoaded, loadForums, forumCategories, getHeaderIcon, pluralize, forumsTotalCount, forumCategoriesTitle, currentLoggedUser
}
} // setup
</script>
and ForumCategoryBlock.vue:
<template>
<div class="">
<h1>INSIDE</h1>
<fieldset class="bordered" >
<legend class="blocks">Block</legend>
nextActiveForumCategory::{{ nextActiveForumCategory}}<br>
currentLoggedUser::{{ currentLoggedUser}}<br>
index::{{ index }}<br>
</fieldset>
</div>
</template>
<script>
import { computed } from 'vue'
export default {
name: 'forumCategoryBlock',
props: {
currentLoggedUser: {
type: Object,
default: () => {}
},
nextActiveForumCategory: {
type: Object,
default: () => {}
},
index: {
type: Number,
default: () => {}
}
},
setup (props) {
console.log('setup props::')
console.log(props)
const nextActiveForumCategory = computed({
get: () => props.value.nextActiveForumCategory
})
const currentLoggedUser = computed({
get: () => props.value.currentLoggedUser
})
const index = computed({
get: () => props.index
})
return { /* currentLoggedUser, nextActiveForumCategory, index */ }
}
}
</script>
What I see in browser : https://prnt.sc/vh7db9
What is wrong abd how to fix it ?
MODIFIED :
I understood WHERE the error :
<div class="row m-0 p-0" v-show="forumCategories.length && isPageLoaded" style="border: 2px dotted red;">
if to remove 2nd condition && isPageLoaded in a line above I see content.
But looks like that var isPageLoaded is not reactive and I do not see why?
If is declared with ref and is declared in return of setup method.
But looks like as I modify it in loadForums method it does not work in template...
Thanks!
isPageLoaded is losing its reactivity because loadForums() is changing its type from ref to Boolean:
isPageLoaded = true // ❌ no longer a ref
isPageLoaded is a ref, so your code has to access it through its value property. It's probably best to use const instead of let here to avoid this mistake:
const isPageLoaded = ref(false)
isPageLoaded.value = true // ✅

Vue 3 access child component from slots

I am currently working on a custom validation and would like to, if possible, access a child components and call a method in there.
Form wrapper
<template>
<form #submit.prevent="handleSubmit">
<slot></slot>
</form>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
setup(props, { slots }) {
const validate = (): boolean => {
if (slots.default) {
slots.default().forEach((vNode) => {
if (vNode.props && vNode.props.rules) {
if (vNode.component) {
vNode.component.emit('validate');
}
}
});
}
return false;
};
const handleSubmit = (ev: any): void => {
validate();
};
return {
handleSubmit,
};
},
});
</script>
When I call slot.default() I get proper list of child components and can see their props. However, vNode.component is always null
My code is based from this example but it is for vue 2.
If someone can help me that would be great, or is this even possible to do.
I found another solution, inspired by quasar framework.
Form component provide() bind and unbind function.
bind() push validate function to an array and store in Form component.
Input component inject the bind and unbind function from parent Form component.
run bind() with self validate() function and uid
Form listen submit event from submit button.
run through all those validate() array, if no problem then emit('submit')
Form Component
import {
defineComponent,
onBeforeUnmount,
onMounted,
reactive,
toRefs,
provide
} from "vue";
export default defineComponent({
name: "Form",
emits: ["submit"],
setup(props, { emit }) {
const state = reactive({
validateComponents: []
});
provide("form", {
bind,
unbind
});
onMounted(() => {
state.form.addEventListener("submit", onSubmit);
});
onBeforeUnmount(() => {
state.form.removeEventListener("submit", onSubmit);
});
function bind(component) {
state.validateComponents.push(component);
}
function unbind(uid) {
const index = state.validateComponents.findIndex(c => c.uid === uid);
if (index > -1) {
state.validateComponents.splice(index, 1);
}
}
function validate() {
let valid = true;
for (const component of state.validateComponents) {
const result = component.validate();
if (!result) {
valid = false;
}
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
}
}
});
Input Component
import { defineComponent } from "vue";
export default defineComponent({
name: "Input",
props: {
rules: {
default: () => [],
type: Array
},
modelValue: {
default: null,
type: String
}
}
setup(props) {
const form = inject("form");
const uid = getCurrentInstance().uid;
onMounted(() => {
form.bind({ validate, uid });
});
onBeforeUnmount(() => {
form.unbind(uid);
});
function validate() {
// validate logic here
let result = true;
props.rules.forEach(rule => {
const value = rule(props.modelValue);
if(!value) result = value;
})
return result;
}
}
});
Usage
<template>
<form #submit="onSubmit">
<!-- rules function -->
<input :rules="[(v) => true]">
<button label="submit form" type="submit">
</form>
</template>
In the link you provided, Linus mentions using $on and $off to do this. These have been removed in Vue 3, but you could use the recommended mitt library.
One way would be to dispatch a submit event to the child components and have them emit a validate event when they receive a submit. But maybe you don't have access to add this to the child components?
JSFiddle Example
<div id="app">
<form-component>
<one></one>
<two></two>
<three></three>
</form-component>
</div>
const emitter = mitt();
const ChildComponent = {
setup(props, { emit }) {
emitter.on('submit', () => {
console.log('Child submit event handler!');
if (props && props.rules) {
emit('validate');
}
});
},
};
function makeChild(name) {
return {
...ChildComponent,
template: `<input value="${name}" />`,
};
}
const formComponent = {
template: `
<form #submit.prevent="handleSubmit">
<slot></slot>
<button type="submit">Submit</button>
</form>
`,
setup() {
const handleSubmit = () => emitter.emit('submit');
return { handleSubmit };
},
};
const app = Vue.createApp({
components: {
formComponent,
one: makeChild('one'),
two: makeChild('two'),
three: makeChild('three'),
}
});
app.mount('#app');

Why dynamic component is not working in vue3?

Here is a working Vue2 example:
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Redone under Vue3 option does not work. No errors occur, but the component does not appear.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
Has anyone studied the vue2 beta version? Help me please. Sorry for the clumsy language, I use Google translator.
Leave everything in the template as in Vue2
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
Change only in "setup" using defineAsyncComponent
You can learn more about defineAsyncComponent here
https://labs.thisdot.co/blog/async-components-in-vue-3
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")) : '')
const onClick = () => {
isShow.value = true;
}
Try this
import DynamicComponent from './DynamicComponent.vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? DynamicComponent : '')
});
...
return {
state,
...
}
}
}
The issue with this seems to be to do with the way we register components when we use the setup script - see the official docs for more info. I've found that you need to register the component globally in order to reference it by string in the template.
For example, for the below Vue component:
<template>
<component :is="item.type" :item="item"></component>
</template>
<script setup lang="ts">
// Where item.type contains the string 'MyComponent'
const props = defineProps<{
item: object
}>()
</script>
We need to register the component in the main.ts, as such:
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
var app = createApp(App);
app.component('MyComponent', MyComponent)
app.mount('#app')
Using 'watch' everything works.
<template>
<component :is="componentPath"/>
</template>
<script lang="ts">
import {defineComponent, ref, watch, SetupContext} from "vue";
export default defineComponent({
props: {
path: {type: String, required: true}
},
setup(props: { path: string }, context: SetupContext) {
const componentPath = ref("");
watch(
() => props.path,
newPath => {
if (newPath !== "")
import("#/" + newPath + ".vue").then(val => {
componentPath.value = val.default;
context.emit("loaded", true);
});
else {
componentPath.value = "";
context.emit("loaded", false);
}
}
);
return {componentPath};
}
});
</script>

vue.js Data Pre-Fetching Problems

I'm building an app following guide https://ssr.vuejs.org/en/data.html.
So i have structure:
server.js
const express = require('express');
const server = express();
const fs = require('fs');
const path = require('path');
const bundle = require('./dist/server.bundle.js');
const renderer = require('vue-server-renderer').createRenderer({
template: fs.readFileSync('./index.html', 'utf-8')
});
server.get('*', (req, res) => {
bundle.default({url: req.url}).then((app) => {
const context = {
title: app.$options.router.history.current.meta.title
};
renderer.renderToString(app, context, function (err, html) {
console.log(html)
if (err) {
if (err.code === 404) {
res.status(404).end('Page not found')
} else {
res.status(500).end('Internal Server Error')
}
} else if (context.title === '404') {
res.status(404).end(html)
} else {
res.end(html)
}
});
}, (err) => {
res.status(404).end('Page not found')
});
});
server.listen(8080);
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex);
import axios from 'axios';
export function createStore() {
return new Vuex.Store({
state: {
articles: [
]
},
actions: {
fetchArticlesList({commit}, params) {
return axios({
method: 'post',
url: 'http://test.local/api/get-articles',
data: {
start: params.start,
limit: params.limit,
language: params.language
}
})
.then((res) => {
commit('setArticles', res.data.articles);
});
},
},
mutations: {
setArticles(state, articles) {
state.articles = articles;
}
}
})
}
router.js
import BlogEn from '../components/pages/BlogEn.vue';
import Vue from 'vue';
import Router from 'vue-router';
export function createRouter() {
return new Router({
mode: 'history',
routes: [
{
path: '/en/blog',
name: 'blogEn',
component: BlogEn,
meta: {
title: 'Blog',
language: 'en'
}
},
});
}
main.js
import Vue from 'vue'
import App from './App.vue'
import {createRouter} from './router/router.js'
import {createStore} from './store/store.js'
import {sync} from 'vuex-router-sync'
export function createApp() {
const router = createRouter();
const store = createStore();
sync(store, router);
const app = new Vue({
router,
store,
render: h => h(App)
});
return {app, router, store};
}
entry-server.js
import {createApp} from './main.js';
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject({ code: 404 })
}
Promise.all(matchedComponents.map(Component => {
// This code not from manual because i want load this in my content-component
if (Component.components['content-component'].asyncData) {
return Component.components['content-component'].asyncData({
store,
route: router.currentRoute
})
}
// This code from manual
// if (Component.asyncData) {
// return Component.asyncData({
// store,
// route: router.currentRoute
// })
// }
})).then(() => {
context.state = store.state
resolve(app)
}).catch(reject)
}, reject)
})
}
entry-client.js
import Vue from 'vue'
import {createApp} from './main.js';
const {app, router, store} = createApp();
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to)
const prevMatched = router.getMatchedComponents(from)
let diffed = false
const activated = matched.filter((c, i) => {
return diffed || (diffed = (prevMatched[i] !== c))
})
if (!activated.length) {
return next()
}
Promise.all(activated.map(c => {
if (c.asyncData) {
return c.asyncData({ store, route: to })
}
})).then(() => {
next()
}).catch(next)
})
app.$mount('#app')
});
Components
BlogEn.vue
<template>
<div>
<header-component></header-component>
<div class="content" id="content">
<content-component></content-component>
<div class="buffer"></div>
</div>
<footer-component></footer-component>
</div>
</template>
<script>
import Header from '../blanks/Header.vue';
import Content from '../pages/content/blog/Content.vue';
import Footer from '../blanks/Footer.vue';
export default {
data() {
return {
};
},
components: {
'header-component': Header,
'breadcrumbs-component' : Breadcrumbs,
'content-component' : Content,
'footer-component': Footer
},
};
</script>
Content.vue
<template>
<section class="blog">
<div v-for="item in articles">
<p>{{ item.title }}</p>
</div>
</section>
</template>
<script>
export default {
data() {
let obj = {
};
return obj;
},
asyncData({store, route}) {
let params = {
start: 0,
limit: 2,
language: 'ru'
};
return store.dispatch('fetchArticlesList', params);
},
computed: {
articles () {
return this.$store.state.articles;
}
}
};
</script>
When i load page /en/blog
My DOM in browser looks like
<div id="app">
<div id="content" class="content">
<!-- There is should be loop content -->
<div class="buffer"></div>
</div>
<footer></footer>
</div>
But! When i look at source code page and html that server sends to me its OK.
<div id="app">
<div id="content" class="content">
<section class="blog">
<div><p>Article Title</p></div>
<div><p>Article Title 2</p></div>
</section>
<div class="buffer"></div>
</div>
<footer></footer>
</div>
Thats not all. I have other pages in my app that i dont show here. When i move at any page and go to "/en/blog" after that DOM is ok.
What's wrong here?