Vue3 Creating Component Instances Programmatically on button click - javascript

In vue2 it was be easy:
<template>
<button :class="type"><slot /></button>
</template>
<script>
export default {
name: 'Button',
props: [ 'type' ],
}
</script>
import Button from 'Button.vue'
import Vue from 'vue'
var ComponentClass = Vue.extend(Button)
var instance = new ComponentClass()
instance.$mount() // pass nothing
this.$refs.container.appendChild(instance.$el)
extend + create instance. But in vue3 it's has been deleted. Where are another way?

import {defineComponent,createApp} from 'vue'
buttonView = defineComponent({
extends: Button, data() {
return {
type: "1111"
}
}
})
const div = document.createElement('div');
this.$refs.container.appendChild(div);
createApp(buttonView ).mount(div)

This works for me, using options API, in a method create a component
import { defineComponent } from "vue";
createComponent() {
var component = {
data() {
return {
hello:'world',
}
},
template:`<div>{{hello}}</div>`
}
var instance = defineComponent(component);
}
Use it within your template once you've instantiated it
<component :is="instance" v-if="instance"/>

When I migrated from Element UI to Element Plus I had to pass prefix/suffix icons as components and not as classname strings anymore. But I use single custom icon component and I have to first create a vnode and customize it with props and then pass it in as the
icon prop.
So I created a plugin:
import { createVNode, getCurrentInstance } from 'vue'
export const createComponent = (component, props) => {
try {
if (component?.constructor === String) {
const instance = getCurrentInstance()
return createVNode(instance.appContext.components[component], props)
} else {
return createVNode(component, props)
}
} catch (err) {
console.error('Unable to create VNode', component, props, err)
}
}
export default {
install(APP) {
APP.$createComponent = createComponent
APP.config.globalProperties.$createComponent = createComponent
}
}
And then I can use it like this for globally registered components:
<component :is="$createComponent('my-global-component', { myProp1: 'myValue1', myProp2: 'myValue2' })" />
And for locally imported components:
<component :is="$createComponent(MyComponent, { foo: 'bar' }) />
Or
data() {
return {
customComponent: $createComponent(MyComponent, { foo: 'bar' })
}
}
<template>
<component :is="customComponent" />
<MyOtherComponent :customizedComponent="customComponent" />
</template>

Try out to extend the Button component and then append it root element $el to the referenced container:
import Button from 'Button.vue'
const CompB = {
extends: Button
}
this.$refs.container.appendChild(CompB.$el)

Related

Vue 3 Composition api pass data boject to child

I want to pass reactive data object to child, but app shows blank page without error message whatsoever. I want to use composition api.
Parent:
<template>
<Landscape :viewData="viewData"/>
</template>
<script>
import { onMounted, onUnmounted, ref, inject } from 'vue';
export default {
name: 'App',
setup() {
const resizeView = ref(false)
const mobileView = ref(false)
const viewData = reactive({resizeView, mobileView})
viewData.resizeView.value = false
viewData.mobileView.value = false
// lets do sth to change viewData
return {
viewData
}
},
components: {
Landscape
}
}
</script>
Child:
<template>resize- {{viewData.resizeView}} mob {{viewData.mobileView}}
</template>
<script>
export default {
name: 'Header',
props: {
viewData: Object,
},
setup() {
return {
}
}
}
</script>
everything works, when in parent, data object is passed directy like this
<Landscape :viewData="{resizeView: false, mobileView: false}"/>
According to Vue docs about reactive objects:
The reactive conversion is "deep"—it affects all nested properties.
So you don't need to wrap every variable as a ref in reactive object (unless you want to unwrap ref variable). Check Vue docs for more info about reactivity API in Vue.
I provided some basic usage of ref and reactive with your Landscape component. Paste this in your App.vue:
<template>
<button #Click="changeResize" type="button">Change ref values</button>
<Landscape :viewData="viewData" />
<br />
<br />
<button #Click="changeReactiveSize" type="button">
Change reactive values
</button>
<Landscape :viewData="otherViewData" />
</template>
<script>
import { onMounted, onUnmounted, ref, inject } from 'vue';
export default {
name: 'App',
setup() {
const resizeView = ref(false);
const mobileView = ref(false);
const viewData = {
resizeView,
mobileView,
};
const changeResize = () => {
viewData.resizeView.value = !viewData.resizeView.value;
viewData.mobileView.value = !viewData.mobileView.value;
};
const otherViewData = reactive({
mobileView: false,
resizeView: false,
});
const changeReactiveSize = () => {
otherViewData.resizeView = !otherViewData.resizeView;
otherViewData.mobileView = !otherViewData.mobileView;
};
return {
viewData,
otherViewData,
changeResize,
changeReactiveSize,
};
},
components: {
Landscape
}
}
You can also check this code example on stackblitz.

Reactive property is not propagating to component in Vue 3 app

I have a Vue 3 app. I am trying to setup a store for state management. In this app, I have the following files:
app.vue
component.vue
main.js
store.js
These files include the following:
store.js
import { reactive } from 'vue';
const myStore = reactive({
selectedItem: null
});
export default myStore;
main.js
import { createApp } from 'vue';
import App from './app.vue';
import myStore from './store';
const myApp = createApp(App);
myApp.config.globalProperties.$store = myStore;
myApp.mount('#app');
component.vue
<template>
<div>
<div v-if="item">You have selected an item</div>
<div v-else>Please select an item</div>
<button class="btn btn-primary" #click="generateItem">Generate Item</button>
</div>
</template>
<script>
export default {
props: {
item: Object
},
watch: {
item: function(newValue, oldValue) {
alert('The item was updated.');
}
},
methods: {
generateItem() {
const item = {
id:0,
name: 'Some random name'
};
this.$emit('itemSelected', item);
}
}
}
</script>
app.vue
<template>
<component :item="selectedItem" #item-selected="onItemSelected" />
</template>
<script>
import Component form './component.vue';
export default {
components: {
'component': Component
},
data() {
return {
...this.$store
}
},
methods: {
onItemSelected(item) {
console.log('onItemSelected: ');
console.log(item);
this.$store.selectedItem = item;
}
}
}
</script>
The idea is that the app manages state via a reactive object. The object is passed into the component via a property. The component can then update the value of the object when a user clicks the "Generate Item" button.
I can see that the selectedValue is successfully passed down as a property. I have confirmed this by manually setting selectedValue to a dummy value to test. I can also see that the onItemSelected event handler works as expected. This means that events are successfully flowing up. However, when the selectedItem is updated in the event handler, the updated value is not getting passed back down to the component.
What am I doing wrong?
$store.selectedItem stops being reactive here, because it's read once in data:
data() {
return {
...this.$store
}
}
In order for it to stay reactive, it should be either converted to a ref:
data() {
return {
selectedItem: toRef(this.$store, 'selectedItem')
}
}
Or be a computed:
computed: {
selectedItem() {
return this.$store.selectedItem
}
}

Vue js import components dynamically

I have the following parent component which has to render a list of dynamic children components:
<template>
<div>
<div v-for="(componentName, index) in supportedComponents" :key="index">
<component v-bind:is="componentName"></component>
</div>
</div>
</template>
<script>
const Component1 = () => import("/components/Component1.vue");
const Component2 = () => import("/components/Component2.vue");
export default {
name: "parentComponent",
components: {
Component1,
Component2
},
props: {
supportedComponents: {
type: Array,
required: true
}
}
};
</script>
The supportedComponents property is a list of component names which I want to render in the parent conponent.
In order to use the children components in the parent I have to import them and register them.
But the only way to do this is to hard code the import paths of the components:
const Component1 = () => import("/components/Component1.vue");
const Component2 = () => import("/components/Component2.vue");
And then register them like this:
components: {
Component1,
Component2
}
I want to keep my parentComponent as generic as possible. This means I have to find a way to avoid hard coded components paths on import statements and registering. I want to inject into the parentComponent what children components it should import and render.
Is this possible in Vue? If yes, then how?
You can load the components inside the created lifecycle and register them according to your array property:
<template>
<div>
<div v-for="(componentName, index) in supportedComponents" :key="index">
<component :is="componentName"></component>
</div>
</div>
</template>
<script>
export default {
name: "parentComponent",
components: {},
props: {
supportedComponents: {
type: Array,
required: true
}
},
created () {
for(let c=0; c<this.supportedComponents.length; c++) {
let componentName = this.supportedComponents[c];
this.$options.components[componentName] = () => import('./' + componentName + '.vue');
}
}
};
</script>
Works pretty well
Here's a working code, just make sure you have some string inside your dynamic import otherwise you'll get "module not found"
<component :is="current" />
export default {  data () {
    return {
      componentToDisplay: null
    }
  },
  computed: {
    current () {
      if (this.componentToDisplay) {
        return () => import('#/components/notices/' + this.componentToDisplay)
      }
      return () => import('#/components/notices/LoadingNotice.vue')
    }
  },
  mounted () {
    this.componentToDisplay = 'Notice' + this.$route.query.id + '.vue'
  }
}
Resolving dynamic webpack import() at runtime
You can dynamically set the path of your import() function to load different components depending on component state.
<template>
<component :is="myComponent" />
</template>
<script>
export default {
props: {
component: String,
},
data() {
return {
myComponent: '',
};
},
computed: {
loader() {
return () => import(`../components/${this.component}`);
},
},
created() {
this.loader().then(res => {
// components can be defined as a function that returns a promise;
this.myComponent = () => this.loader();
},
},
}
</script>
Note: JavaScript is compiled by your browser right before it runs. This has nothing to do with how webpack imports are resolved.
I think we need some plugin that can have code and every time it should load automatically. This solution is working for me.
import { App, defineAsyncComponent } from 'vue'
const componentList = ['Button', 'Card']
export const registerComponents = async (app: App): void => {
// import.meta.globEager('../components/Base/*.vue')
componentList.forEach(async (component) => {
const asyncComponent = defineAsyncComponent(
() => import(`../components/Base/${component}.vue`)
)
app.component(component, asyncComponent)
})
}
you can also try glob that also work pretty well but I have checked it for this solution but check this out worth reading
Dynamic import
[Update]
I tried same with import.meta.globEage and it works only issue its little bit lazy loaded you may feel it loading slow but isn't noticeable much.
import { App, defineAsyncComponent } from 'vue'
export const registerComponents = async (app: App): void => {
Object.keys(import.meta.globEager('../components/Base/*.vue')).forEach(
async (component) => {
const asyncComponent = defineAsyncComponent(
() => import(/* #vite-ignore */ component)
)
app.component(
(component && component.split('/').pop()?.split('.')[0]) || '',asyncComponent
)
})
}

How do you unit test a Vue.js functional component with a render function that returns any array of elements?

In Vue.js, a functional component can return multiple root nodes by using a render function that returns an array of createdElements.
export default {
functional: true,
props: ['cellData'],
render: function (h, context) {
return [
h('td', context.props.cellData.category),
h('td', context.props.cellData.description)
]
}
}
This works great but I'm having trouble trying to create a unit test for such a component. Using shallowMount on the component results in [Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.
import { shallowMount } from '#vue/test-utils'
import Cell from '#/components/Cell'
wrapper = shallowMount(Cell, {
context: {
props: {
cellData {
category: 'foo',
description: 'bar'
}
}
}
});
This github issue suggests that the component needs to be wrapped in a single root node to actually render it, but trying that results in [vue-test-utils]: mount.context can only be used when mounting a functional component
import { shallowMount } from '#vue/test-utils'
import Cell from '#/components/Cell'
wrapper = shallowMount('<div><Cell></div>', {
context: {
props: {
cellData {
category: 'foo',
description: 'bar'
}
}
}
});
So how do I test a functional component that returns multiple root nodes?
You could create a higher order, transparent wrapper component that passes all props and event listeners to the inner Cell component using v-bind="$attrs"[1] and v-on="$listeners"[2]. Then you can use propsData to pass props to the wrapper component ..
import { mount } from '#vue/test-utils'
import Cell from '#/components/Cell'
const WrappedCell = {
components: { Cell },
template: `
<div>
<Cell v-bind="$attrs" v-on="$listeners" />
</div>
`
}
const wrapper = mount(WrappedCell, {
propsData: {
cellData: {
category: 'foo',
description: 'bar'
}
}
});
You can create a fragment_wrapper for wrapping your Components with Fragments (multiple root elements).
//File: fragment_wrapper.js
exports.fragment_wrapper = function(FragmentComponent){
const wrapper = {
components: { FragmentComponent },
props: FragmentComponent.props,
template: `<div><FragmentComponent v-bind="$props" v-on="$listeners"/></div>`
}
return wrapper;
}
Then you can use this to test all your Fragmented Components as follows:
import { mount } from '#vue/test-utils'
import { fragment_wrapper } from './fragment_wrapper'
import Cell from './components/Cell'
describe('Test Cell', () => {
let WrappedCell = fragment_wrapper(Cell);
const wrapper = mount(WrappedCell, {
propsData: {
cellData: {
category: 'foo',
description: 'bar'
}
}
});
it('renders the correct markup', () => {
expect(wrapper.html()).toContain('<td>foo</td>')
});
});

How to programmatically set $route.params when testing a Vuejs component

I have a Vue.js component that has different behavior according to the $route.params or $route.query. Something like:
<template>
<div class="hello">
{{query}}
</div>
</template>
<script>
export default {
name: 'hello',
data () {
return {
}
}
},
computed: {
'query': () => {
return $route.params.query
}
}
}
</script>
How can I write a unit test spec where I can programmatically set the value of $route.params or $route.query ?
Something like this:
import Vue from 'vue'
import Template from '#/components/Template'
describe('Template.vue', () => {
it('should render according to query', () => {
const Constructor = Vue.extend(Script)
const vm = new Constructor()
vm.$route = { // THIS DOES NOT WORK
params: {
id: 1
}
}
vm.$mount()
// Test contents
})
})
Fails with
setting a property that has only a getter

Categories

Resources