Vue 3 - Component is not loaded or rendering - vue.js

I have following component:
<script setup>
import {defineProps, ref, watch} from "vue";
import ProductsComponent from '#/components/Products.vue'
import OrdersComponent from '#/components/Orders.vue'
import {useTableOrderStore} from "#/store/tableOrder";
const tableOrderStore = useTableOrderStore()
const props = defineProps({
orderID: {
type: Number
},
tableID: {
type: Number
},
tableName: {
type: String
}
})
let orders = ref([])
watch(props, (newProp, oldProp) => {
orders = tableOrderStore.tableOrders
console.log(orders)
})
</script>
<template>
<products-component :tableName="tableName"></products-component>
<orders-component v-for="order in orders" :order="order" :key="order.id"></orders-component>
</template>
And OrdersComponent which is loaded in this component:
<script setup>
import {watch} from "vue";
let props = defineProps({
order: {
type: Array,
required: true
}
})
watch(props, (newProp, oldProp) => {
console.log(newProp)
})
console.log(12333)
</script>
<template>
<div class="row">
{{ order }}
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias, expedita?
</div>
</template>
When the main component is shown, ProductsComponent is loaded and shown, but OrdersComponent is not. What am I doing wrong here?

The component is not rendered because the array orders is still empty and the watcher to update it is not working properly which should be written by returning props from callback and adding other options (immediate and deep):
let orders = ref([])
watch(()=>props, (newProp, oldProp) => {
orders.value = tableOrderStore.tableOrders
console.log(orders.value)
},{
immediate:true,
deep:true //since props are an object
})

Related

Draggable Vue component

I'm trying to implement this vue draggable component within my component. My component works fine without the draggable component, but once draggable is added everything is blank. I'm thinking that it is a configuration issue. Any assistance would be appreciated.
The most recent error I'm getting is
Invalid vnode type when creating vnode: undefined
<template>
<div :class="['image-select']">
<draggable v-model="images">
<div v-for="image in images"
:class="['image-select-image']"
:key="image.id">
<input type="hidden"
:name="fieldName()"
:value="image.id">
<img :src="imageUrl(image,size)" :alt="image.alt">
</div>
</draggable>
</div>
</template>
<script setup>
import {onBeforeMount, ref} from 'vue';
import api from "../modules/api";
import {draggable} from "vuedraggable";
const props = defineProps({
size: {
type: String,
default: 'sm'
},
name: {
type: String,
default: 'images'
},
selectedValues: {
type: Object,
default: null
}
});
const images = ref([]);
onBeforeMount(async () => {
await Promise.all([
setImages()
]);
});
function fieldName(){
return props.name + '[images][]';
}
function imageUrl(image, size){
return image.src.replace(/\/[a-zA-Z0-9_\.-]+(\.[a-zA-Z]+)$/,'/'+size+'$1');
}
function setImages(){
// load images from ids in selectedValues
}
</script>
According to the documentation, you're supposed to import without curly braces {}
import draggable from 'vuedraggable';
Based on how the vuedraggable component is exported, using the curly braces will make the import fail.
See this codesandbox example

Extract modelValue logic to composable

I'm transitioning from Vue 2 to Vue 3 and I'm having trouble with composables.
I have a bunch of components that inherits modelValue. So, for every component that uses modelValue I'm writing this code (example with a radio input component):
<script setup>
import { computed } from 'vue'
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: {
type: [String, null],
required: true
}
})
const computedValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
</script>
<template>
<label class="radio">
<input
v-model="computedValue"
v-bind="$attrs"
type="radio"
>
<slot />
</label>
</template>
Is there a way to reuse the code for the modelValue?
I've just done this while I'm playing with Nuxt v3.
You can create a composable like this:
import { computed } from 'vue'
export function useModel(props, emit) {
return computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
}
<template>
<input type="text" v-model="value" />
</template>
<script setup lang="ts">
const props = defineProps({
modelValue: String,
})
const emit = defineEmits(['update:modelValue'])
const value = useModel(props, emit)
</script>
For completion of #BghinC's perfect answer here the fully typed version:
Composable
File: #/composables/useModelValue.ts
import {computed} from 'vue'
export default function useModelValue<T>(
props: {
modelValue: T
[key: string]: unknown
},
emit: (event: 'update:modelValue', ...args: unknown[]) => void
) {
return computed({
get: () => props.modelValue,
set: (value: T) => emit('update:modelValue', value),
})
}
Usage
<script setup lang="ts">
import useModelValue from '#/composables/useModelValue'
const props = defineProps<{
modelValue: Dog
}>()
const emit = defineEmits(['update:modelValue'])
const dog = useModelValue<Dog>(props, emit)
</script>

Vue-router named views not working as expected

I'm stuck on getting my Vue-router to work as I want. I have two <route-view /> Named Views in my templates. One is the main view for navigation on the top nav bar, which is working fine. The other doesn't work. I have defined my router as such (shortened for clarity):
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
const routes = [
{
path: '/',
name: 'home',
components: { main: HomeView },
},
{
path: '/bpvapp',
name: 'bpvapp',
components: { main: () => import('../views/BpvApp.vue') },
},
{
path: '/bpvapp/projects',
name: 'projects',
components: { bpvapp: HomeView }, // HomeView component for testing purposes
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
My App.vue has the main route defined in the template:
<template>
<div class="main-container">
<navigation-bar
:isLoggedIn="isLoggedIn"
:profileImageUrl="userImage"
:signOut="signOut"
/>
<suspense>
<router-view name="main" />
</suspense>
</div>
</template>
My BpvApp.vue component, which is a CSS Grid layout, uses the second route view (shortened for clarity):
<template>
<div class="bpv--app-container">
<div class="bpv--app-toolbar">TOOLBAR</div>
<div class="bpv--app-sidebar">
<bpv-button
text="Projects"
icon="folder-star-multiple"
styling="info"
class="bpv--bs-1"
:active="activeSection === 'projects'"
to="/bpvapp/projects"
/>
</div>
<div class="bpv--app-content">
<suspense>
<router-view name="bpvapp" />
</suspense>
</div>
<div class="bpv--app-assist">
RIGHT Lorem ipsum dolor sit amet consectetur, adipisicing elit. Laboriosam
placeat deserunt quidem fugiat dicta repellat nobis mollitia! Consectetur
laudantium dolor, odio adipisci at qui, deserunt minus facere rerum,
voluptates maiores.
</div>
<div class="bpv--app-footer">FOOTER</div>
</div>
</template>
The div with class bpv--app-content should show the content from the component I push to the router. You can see the button will push to /bpvapp/projects, which in my router definition should invoke the router with name bpvapp and show the content.
For the sake of completeness, my button component handles the to prop as follows:
...
methods: {
// this.to has the correct value '/bpvapp/projects', I double-checked
buttonClicked(e) {
if (this.to) {
this.$router.push(this.to);
} else if (this.action) {
this.action();
} else {
this.$emit('click', e);
}
},
},
...
What happens now is that I get a blank screen. When I inspect the HTML, the entire structure is not there, except the nav bar.
What am I doing wrong here? I've tried removing the <suspense> tag and loading static content, but that made no difference.
One issue is the two routes are configured as siblings, but the second one should be a nested child route of the first, using the route's children option:
// router.js
const routes = [
{
path: '/',
name: 'home',
components: { main: HomeView },
},
{
path: '/bpvapp',
name: 'bpvapp',
components: { main: () => import('#/views/BpvApp.vue') },
👇
children: [
{
path: 'projects',
name: 'projects',
components: { bpvapp: () => import('#/views/BpvProject.vue') },
},
],
},
]
Note: When there's only one router-view at a particular level, you don't need to name it.
The other issue is you don't need <suspense> for this. <suspense> is intended for async components declared with an async setup() option or within defineAsyncComponent() (which is not appropriate for a route's components option). While the router components are technically loaded asynchronously via the dynamic imports, they're not actually the async components that <suspense> expects.
demo

Invalid route component

When I want to practice a navigation bar with Vue, encountering a mistake about the invalid route component. In this div cannot display anything. As to the hint of the console, I can find no way out.
the App.vue, hereI only show the Home navigation
<template>
<div id="app">
<tab-bar>
<tab-bar-item class="tab-bar-item" path='/home' activeColor="red">
<img slot="item-icon" src="./assets/img/tabbar/home.svg" alt="">
<img slot="item-icon-active" src="./assets/img/tabbar/Home1.svg" alt="">
<div slot="item-text">Home</div>
</tab-bar>
<router-view></router-view>
</div>
</template>
<script>
import TabBar from './components/tabbar/TabBar'
import TabBarItem from './components/tabbar/TabBarItem'
export default {
name: 'App',
components: {
TabBar,
TabBarItem
}
}
</script>
the two vue components:
TabBarItem.vue
<template>
<div class="tab-bar-item" #click="itemClick">
<div v-if="!isActive"><slot name="item-icon"></slot></div>
<div v-else><slot name="item-icon-active"></slot></div>
<div :style="activeStyle" :class="{active: isActive}"><slot name="item-text"></slot></div>
</div>
</template>
<script>
export default {
name: 'TabBarItem',
props: {
path: String,
activeColor: {
type: String,
default: 'red'
}
},
data() {
return {
isActive: true
}
},
methods: {
itemCilck() {
this.$router.replace();
console.log('itemclick')
}
}
}
</script>
TarBar.vue
<template>
<div id="tab-bar">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'TabBar'
}
</script>
index.js
import { createRouter, createWebHistory } from 'vue-router'
const Home = () => import('../views/home/Home')
const Categroy = () => import('../views/category/Category')
const Cart = () => import('../views/cart/Cart')
const Profile = () => import('../views/profile/Profile')
// import Home from '../views/Home.vue'
const routes = [
{
path: '/home',
component: Home
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
the error
vue-router.esm-bundler.js:3266 Error: Invalid route component
at extractComponentsGuards (vue-router.esm-bundler.js:1994)
at eval (vue-router.esm-bundler.js:3107)
the console picture
According to docs they do not omit file types in dynamic imports, try Home.vue instead of just Home:
const Home = () => import('../views/home/Home.vue')

Vue Snapshot testing with RouterLinkStub does not render anchor as expected

I'm trying to run jest-snapshot tests in a Vue application which has a Component with router-link element.
I'm using RouterLinkStub as recommended in Snapshot Testing with Vue-Router
My component template is something like
<template>
<section class="content">
<div>
<router-link to="/" v-slot="{ href, route, navigate }">
<a :href="href" #click="navigate">
Homepage
</a>
</router-link>
</div>
</section>
</template>
The problem is that in snapshot, the router-link renders as empty anchor element (<a />), so if I change the link text the snapshot test stills pass.
This is my snapshot test:
import { RouterLinkStub, shallowMount } from '#vue/test-utils'
import PageNotFound from 'views/PageNotFound.vue'
describe('PageNotFound.vue', () => {
it('renders as expected', () => {
const wrapper = shallowMount(PageNotFound, {
stubs: {
RouterLink: RouterLinkStub
}
})
expect(wrapper.element).toMatchSnapshot()
})
})
Can you help me please?
Thanks
With Vue 3
import { shallowMount, RouterLinkStub } from '#vue/test-utils'
import router from '../../router'
const findComponentRouterLink = () =>
wrapper.findComponent(RouterLinkStub)
const wrapper = shallowMount(Component, {
global: {
plugins: [router],
stubs: {
RouterLink: RouterLinkStub,
},
},
})
findComponentRouterLink().text()).toBe('Link text')