Why dynamic component is not working in vue3? - vue.js

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>

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>

How to unload the script so that every time the component is mounted, it is loaded again

How to unload the script so that every time the component is mounted, it is loaded again.
<template>
<div id="root">
<component :is="App" />
</div>
</template>
<script setup>
import { defineAsyncComponent, ref, onMounted, onUnmounted } from 'vue';
const App = ref(null);
onMounted(() => {
App.value = defineAsyncComponent(() => import('editor/App'));
});
onUnmounted(() => {
App.value = null;
});
</script>
Code like in my example does not do this. The component is unmounted, but the file is not reloaded during subsequent mounting.
This is the code from the config:
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
configureWebpack: {
plugins: [
new ModuleFederationPlugin({
name: 'consumer',
filename: 'remoteEntry.js',
remotes: {
editor: process.env.FRONTEND_EDITOR_URI,
},
}),
],
},
};
Any thoughts?

vue-test-utils: Unable to detect method call when using #click attribute on child component

Vue-test-utils is unable to detect method call when using #click attribute on child component, but is able to detect it when using the #click attribute on a native HTML-element, e.g. a button. Let me demonstrate:
This works:
// Test.vue
<template>
<form #submit.prevent>
<button name="button" type="button" #click="click">Test</button>
</form>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Test',
setup() {
const click = () => {
console.log('Click')
}
return { click }
}
})
</script>
// Test.spec.js
import { mount } from '#vue/test-utils'
import Test from './src/components/Test.vue'
describe('Test.vue', () => {
const wrapper = mount(Test)
if ('detects that method was called', () => {
const spy = spyOn(wrapper.vm, 'click')
wrapper.find('button').trigger('click')
expect(wrapper.vm.click).toHaveBeenCalled() // SUCCESS. Called once
})
})
This does NOT work:
// Test.vue
<template>
<form #submit.prevent>
<ButtonItem #click="click" />
</form>
</template>
<script>
import { defineComponent } from 'vue'
import ButtonItem from './src/components/ButtonItem.vue'
export default defineComponent({
name: 'Test',
components: { ButtonItem },
setup() {
const click = () => {
console.log('Click')
}
return { click }
}
})
</script>
// ButtonItem.vue
<template>
<button type="button">Click</button>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ButtonItem',
})
</script>
// Test.spec.js
import { mount } from '#vue/test-utils'
import Test from './src/components/Test.vue'
import ButtonItem from './src/components/ButtonItem.vue'
describe('Test.vue', () => {
const wrapper = mount(Test)
if ('detects that method was called', () => {
const spy = spyOn(wrapper.vm, 'click')
wrapper.findComponent(ButtonItem).trigger('click')
expect(wrapper.vm.click).toHaveBeenCalled() // FAIL. Called 0 times
})
})
This issue stumbles me. I am not sure what I do wrong. I would be grateful if someone could describe the fault and show me the solution. Thanks!
I haven't run your code to test it yet, but why won't you use a more straightforward solution?
Look out for when the component emits click.
describe('Test.vue', () => {
const wrapper = mount(Test)
it('when clicked on ButtonItem it should be called one time', () => {
const button = wrapper.findComponent(ButtonItem)
button.trigger('click')
expect(wrapper.emitted().click).toBeTruthy()
expect(wrapper.emitted().click.length).toBe(1)
})
})

Vue received a Component which was made a reactive object

The problem I need to solve: I am writing a little vue-app based on VueJS3.
I got a lot of different sidebars and I need to prevent the case that more than one sidebar is open at the very same time.
To archive this I am following this article.
Now I got a problem:
Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref. (6)
This is my code:
SlideOvers.vue
<template>
<component :is="component" :component="component" v-if="open"/>
</template>
<script>
export default {
name: 'SlideOvers',
computed: {
component() {
return this.$store.state.slideovers.sidebarComponent
},
open () {
return this.$store.state.slideovers.sidebarOpen
},
},
}
</script>
UserSlideOver.vue
<template>
<div>test</div>
</template>
<script>
export default {
name: 'UserSlideOver',
components: {},
computed: {
open () {
return this.$store.state.slideovers.sidebarOpen
},
component () {
return this.$store.state.slideovers.sidebarComponent
}
},
}
</script>
slideovers.js (vuex-store)
import * as types from '../mutation-types'
const state = {
sidebarOpen: false,
sidebarComponent: null
}
const getters = {
sidebarOpen: state => state.sidebarOpen,
sidebarComponent: state => state.sidebarComponent
}
const actions = {
toggleSidebar ({commit, state}, component) {
commit (types.TOGGLE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
},
closeSidebar ({commit, state}, component) {
commit (types.CLOSE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
}
}
const mutations = {
[types.TOGGLE_SIDEBAR] (state) {
state.sidebarOpen = !state.sidebarOpen
},
[types.CLOSE_SIDEBAR] (state) {
state.sidebarOpen = false
},
[types.SET_SIDEBAR_COMPONENT] (state, component) {
state.sidebarComponent = component
}
}
export default {
state,
getters,
actions,
mutations
}
App.vue
<template>
<SlideOvers/>
<router-view ref="routerView"/>
</template>
<script>
import SlideOvers from "./SlideOvers";
export default {
name: 'app',
components: {SlideOvers},
};
</script>
And this is how I try to toggle one slideover:
<template>
<router-link
v-slot="{ href, navigate }"
to="/">
<a :href="href"
#click="$store.dispatch ('toggleSidebar', userslideover)">
Test
</a>
</router-link>
</template>
<script>
import {defineAsyncComponent} from "vue";
export default {
components: {
},
data() {
return {
userslideover: defineAsyncComponent(() =>
import('../../UserSlideOver')
),
};
},
};
</script>
Following the recommendation of the warning, use markRaw on the value of usersslideover to resolve the warning:
export default {
data() {
return {
userslideover: markRaw(defineAsyncComponent(() => import('../../UserSlideOver.vue') )),
}
}
}
demo
You can use Object.freeze to get rid of the warning.
If you only use shallowRef f.e., the component will only be mounted once and is not usable in a dynamic component.
<script setup>
import InputField from "src/core/components/InputField.vue";
const inputField = Object.freeze(InputField);
const reactiveComponent = ref(undefined);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = undefined;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
</script>
<template>
<component :is="reactiveComponent" />
</template>

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?