I want to do this in Vue 3
new ComponentName({
propsData: {
title: 'hello world',
}
}).$mount();
But I'm getting this error: VueComponents_component_name__WEBPACK_IMPORTED_MODULE_1__.default is not a constructor
Currently, we are using the above approach to append VUE components in our legacy app via append
I would like to do the same on VUE 3 but I haven't found the way to do it
Thanks in advance
I found the solution to my answer, mounting a vue component in vue 3 (Outside vue projects) is different than vue 2, this is the approach :
// mount.js
import { createVNode, render } from 'vue'
export const mount = (component, { props, children, element, app } = {}) => {
let el = element
let vNode = createVNode(component, props, children)
if (app && app._context) vNode.appContext = app._context
if (el) render(vNode, el)
else if (typeof document !== 'undefined' ) render(vNode, el = document.createElement('div'))
const destroy = () => {
if (el) render(null, el)
el = null
vNode = null
}
return { vNode, destroy, el }
}
el: DOM element to be appended
vNode: Vue instance
destroy: Destroy the component
This is the way to mount vue 3 components to be appended directly to the DOM, and can be used as below:
// main.js
import { mount } from 'mount.js'
const { el, vNode, destroy } = mount(MyVueComponents,
{
props: {
fields,
labels,
options
},
app: MyVueApp
},
)
$element.html(el);
Hope it Helps, regards!
Just for future visitors to save some time, I was searching for the same answer and found a plug-in that does exactly what Luis explained en his answer at https://github.com/pearofducks/mount-vue-component
Makes it a little simpler to implement.
It is easy to create a new vue3 app and mount to a DOM directly,
const appDef = {
data() {
return {title: 'hello world'};
},
template: '<div>title is: {{title}}</div>',
}
var el = document.createElement('div');//create container for the app
const app = Vue.createApp(appDef);
app.mount(el);//mount to DOM
//el: DOM element to be appended
console.log(el.innerHTML);//title is: hello world
Related
In Vue 2 it was possible to access the innerHTML of a Vue component instance via someInstance.$el.innerHTML. How can the same be achieved in Vue 3?
Let's say you want to to create a Vue component and access its innerHTML. In Vue 2, this could be done like so:
const wrapper = document.createElement('div');
const someComponentInstance = new Vue({
render: h => h(SomeComponent, {
props: {
someProp: 'prop-value-123'
}
})
});
someComponentInstance.$mount(wrapper);
console.log(someComponentInstance.$el.innerHTML);
To achieve the same thing in Vue 3, we have to leverage the createApp() and mount() functions like so:
const wrapper = document.createElement('div');
const someComponentInstance = createApp(SomeComponent, {
someProp: 'prop-value-123'
});
const mounted = someComponentInstance.mount(wrapper); // returns an instance containing `$el.innerHTML`
console.log(mounted.$el.innerHTML);
A word of warning: Make sure your innerHTML is sanitized if it is user generated and you want to reuse it somewhere in your app.
In my component , I have a method which will execute a router.push()
import router from "#/router";
// ...
export default {
// ...
methods: {
closeAlert: function() {
if (this.msgTypeContactForm == "success") {
router.push("/home");
} else {
return;
}
},
// ....
}
}
I want to test it...
I wrote the following specs..
it("should ... go to home page", async () => {
// given
const $route = {
name: "home"
},
options = {
...
mocks: {
$route
}
};
wrapper = mount(ContactForm, options);
const closeBtn = wrapper.find(".v-alert__dismissible");
closeBtn.trigger("click");
await wrapper.vm.$nextTick();
expect(alert.attributes().style).toBe("display: none;")
// router path '/home' to be called ?
});
1 - I get an error
console.error node_modules/#vue/test-utils/dist/vue-test-utils.js:15
[vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property asa read-only value
2 - How I should write the expect() to be sure that this /home route has been called
thanks for feedback
You are doing something that happens to work, but I believe is wrong, and also is causing you problems to test the router. You're importing the router in your component:
import router from "#/router";
Then calling its push right away:
router.push("/home");
I don't know how exactly you're installing the router, but usually you do something like:
new Vue({
router,
store,
i18n,
}).$mount('#app');
To install Vue plugins. I bet you're already doing this (in fact, is this mechanism that expose $route to your component). In the example, a vuex store and a reference to vue-i18n are also being installed.
This will expose a $router member in all your components. Instead of importing the router and calling its push directly, you could call it from this as $router:
this.$router.push("/home");
Now, thise makes testing easier, because you can pass a fake router to your component, when testing, via the mocks property, just as you're doing with $route already:
const push = jest.fn();
const $router = {
push: jest.fn(),
}
...
mocks: {
$route,
$router,
}
And then, in your test, you assert against push having been called:
expect(push).toHaveBeenCalledWith('/the-desired-path');
Assuming that you have setup the pre-requisities correctly and similar to this
Just use
it("should ... go to home page", async () => {
const $route = {
name: "home"
}
...
// router path '/home' to be called ?
expect(wrapper.vm.$route.name).toBe($route.name)
});
I have a basic VueJS application with only one page.
It's not a SPA, and I do not use vue-router.
I would like to implement a button that when clicked executes the window.open() function with content from one of my Vue Components.
Looking at the documentation from window.open() I saw the following statement for URL:
URL accepts a path or URL to an HTML page, image file, or any other resource which is supported by the browser.
Is it possible to pass a component as an argument for window.open()?
I was able to use some insights from an article about Portals in React to create a Vue component which is able to mount its children in a new window, while preserving reactivity! It's as simple as:
<window-portal>
I appear in a new window!
</window-portal>
Try it in this codesandbox!
The code for this component is as follows:
<template>
<div v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
// magic!
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeDestroy() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
The key is the line this.windowRef.document.body.appendChild(this.$el); this line effectively removes the DOM element associated with the Vue component (the top-level <div>) from the parent window and inserts it into the body of the child window. Since this element is the same reference as the one Vue would normally update, just in a different place, everything Just Works - Vue continues to update the element in response to databinding changes, despite it being mounted in a new window. I was actually quite surprised at how simple this was!
You cannot pass a Vue component, because window.open doesn't know about Vue. What you can do, however, is to create a route which displays your component and pass this route's URL to window.open, giving you a new window with your component. Communication between the components in different windows might get tricky though.
For example, if your main vue is declared like so
var app = new Vue({...});
If you only need to render a few pieces of data in the new window, you could just reference the data model from the parent window.
var app1 = window.opener.app;
var title = app.title;
var h1 = document.createElement("H1");
h1.innerHTML = title;
document.body.appendChild(h1);
I ported the Alex contribution to Composition API and works pretty well.
The only annoyance is that the created window ignores size and position, maybe because it is launched from a Chrome application that is fullscreen. Any idea?
<script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch, nextTick} from "vue";
const props = defineProps<{modelValue: boolean;}>();
const emit = defineEmits(["update:modelValue"]);
let windowRef: Window | null = null;
const portal = ref(null);
const copyStyles = (sourceDoc: Document, targetDoc: Document): void => {
// eslint-disable-next-line unicorn/prefer-spread
for(const styleSheet of Array.from(sourceDoc.styleSheets)) {
if(styleSheet.cssRules) {
// for <style> elements
const nwStyleElement = sourceDoc.createElement("style");
// eslint-disable-next-line unicorn/prefer-spread
for(const cssRule of Array.from(styleSheet.cssRules)) {
// write the text of each rule into the body of the style element
nwStyleElement.append(sourceDoc.createTextNode(cssRule.cssText));
}
targetDoc.head.append(nwStyleElement);
}
else if(styleSheet.href) {
// for <link> elements loading CSS from a URL
const nwLinkElement = sourceDoc.createElement("link");
nwLinkElement.rel = "stylesheet";
nwLinkElement.href = styleSheet.href;
targetDoc.head.append(nwLinkElement);
}
}
};
const openPortal = (): void => {
nextTick().then((): void => {
windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
if(!windowRef || !portal.value) return;
windowRef.document.body.append(portal.value);
copyStyles(window.document, windowRef.document);
windowRef.addEventListener("beforeunload", closePortal);
})
.catch((error: Error) => console.error("Cannot instantiate portal", error.message));
};
const closePortal = (): void => {
if(windowRef) {
windowRef.close();
windowRef = null;
emit("update:modelValue", false);
}
};
watch(props, () => {
if(props.modelValue) {
openPortal();
}
else {
closePortal();
}
});
onMounted(() => {
if(props.modelValue) {
openPortal();
}
});
onBeforeUnmount(() => {
if(windowRef) {
closePortal();
}
});
</script>
<template>
<div v-if="props.modelValue" ref="portal">
<slot />
</div>
</template>
I have a problem with VuePaginator , that I can mount it to my Vue app $refs properties. I am doing everyting according to docs, here is my component in the html:
<v-paginator :resource.sync="comments" ref="vpaginator" resource_url="{{route('api.item.comments', $item->pk_i_id)}}"></v-paginator>
The pagination works correctly, but I can't trigger fetchData() from the vuejs code, because paginator is not getting mounted to vm.$refs.vpaginator.
Here is the code that I use:
var app = new Vue({
el: '#comments',
data : {
comments: [],
newComment: {
text: ""
}
},
components: {
VPaginator: VuePaginator
},
methods: {
addComment: function(comment){
var vm = this;
this.$http.post($('meta[name="item-url"]').attr('content'), comment)
.then(function(response){
toastr.success(response.data.result);
comment.text = "";
vm.$.vpaginator.fetchData();
}).catch(function (error) {
if(error.data){
toastr.error(error.data.text[0]);
}
})
},
logRefs: function(){
console.log(this.$refs.vpaginator);
}
}
});
I have created logRefs() function to check the $ref property and it is always undefined.
Since you are using the Version 1 of VueJS, usage is a bit different - check this demo http://jsbin.com/rupogesumo/edit?html,js,output
<v-paginator :resource.sync="comments" v-ref:vpaginator resource_url="{{route('api.item.comments', $item->pk_i_id)}}"></v-paginator>
Docs Reference: https://v1.vuejs.org/api/#v-ref
I have the following test which works great
it('does not render chapter div or error div', () => {
const payLoad = chapter;
const switcher = 'guild';
var vm = getComponent(payLoad, switcher).$mount();
expect(vm.$el.querySelector('#chapter-card')).toBeNull();
expect(vm.$el.querySelector('#error-card')).toBeNull();
});
To do this I wrote a helper method that mounts a component:
const getComponent = (prop1) => {
let vm = new Vue({
template: '<div><compd :payLoad="group" :index="index" "></compd ></div></div>',
components: {
compd,
},
data: {
payLoad: prop1,
},
})
return vm;
}
however, I have a method within my vue component compd. For simplicitys sake, lets call it
add(num,num){
return num+num;
}
I want to be able to write a test case similar to the following:
it('checks the add method works', () => {
expect(compd.add(1,2).toBe(3));
});
I cannot figure out how to do this. Has anyone any suggestions?
The documentation here:
https://v2.vuejs.org/v2/guide/unit-testing.html
Does not cover testing methods.
Source code from vue repo
As you can see the method gets called simply on the instance
const vm = new Vue({
data: {
a: 1
},
methods: {
plus () {
this.a++
}
}
})
vm.plus()
expect(vm.a).toBe(2)
You can also access the method via $options like in this case (vue source code)
const A = Vue.extend({
methods: {
a () {}
}
})
const vm = new A({
methods: {
b () {}
}
})
expect(typeof vm.$options.methods.a).toBe('function')
Update:
To test child components use $children to access the necessary child. Example
var childToTest = vm.$children.find((comp)=>comp.$options.name === 'accordion')` assuming name is set to `accordion`
After that you can
childToTest.plus();
vm.$nextTick(()=>{
expect(childToTest.someData).toBe(someValue)
done(); //call test done callback here
})
If you have a single child component and not a v-for put a ref on it
`
vm.$refs.mycomponent.myMethod()