I have a component proxy which renders components using vue.js build in dynamic component. The instanceName is the component to render. The data is the relevant data for the component to render.
<template>
<component v-if="getInstanceName" :is="getInstanceName" :data="data" />
</template>
<script>
import WidgetTypes from './WidgetTypes'
import { hydrateWhenVisible } from 'vue-lazy-hydration'
const components = Object.keys(WidgetTypes.types).reduce(
(components, typeKey) => {
const type = WidgetTypes.types[typeKey]
components[type] = hydrateWhenVisible(
() =>
import(
/* webpackChunkName: "[request]" */
/* webpackMode: "lazy" */
`./dynamic/${type}/${type}.vue`
),
{ observerOptions: { rootMargin: '100px' } }
)
return components
},
{}
)
export default {
name: 'WidgetComponentProxy',
components: components,
props: {
data: {
type: Object,
required: true,
default: null
}
},
computed: {
getInstanceName() {
return WidgetTypes.getInstanceName(this.data.instance_type)
}
}
}
</script>
All this works like a charm.. except ;-)
When inspecting in vue.js devtools I see my component twice. The component is somehow child to itself. I sometimes get errors trying to render components using this proxy.
Any help appreciated thanks
Related
I am trying to build my own sortable component. I want to pass a list of items to it's default slot. The sortable component should then wrap all passed items with a custom v-draggable component.
<v-sortable handle=".handle">
<template :key="index" v-for="(item, index) in items">
<some-complex-component :item="item"></some-complex-component>
</template>
</v-sortable>
Now withn my v-sortable component I am trying to wrap all given nodes within default slot with a custom v-draggable component.
My v-sortable component looks like this:
import { h } from 'vue';
export default {
name: 'v-sortable',
props: {
handle: {
type: String,
required: false,
default: () => {
return null;
}
},
},
render () {
const draggableItems = this.$slots.default().map(slotItem =>
h('v-draggable', { handle: this.handle }, [slotItem])
)
return draggableItems;
}
}
This works as expected, except that my custom component v-draggable will not be rendered as a vue component. All items will be wrapped in html tags called <v-draggable>.
How would I have to proceed to actually parse the v-draggable component as Vue component?
Try to import it and register and use it directly :
import { h } from 'vue';
import VDraggable from 'path/to/v-draggable'
export default {
name: 'v-sortable',
props: {
handle: {
type: String,
required: false,
default: () => {
return null;
}
},
},
render () {
const draggableItems = this.$slots.default().map(slotItem =>
h(VDraggable, { handle: this.handle }, [slotItem])
)
return draggableItems;
}
}
It's recommended to pass items as prop and use them directly inside the render function :
<v-sortable handle=".handle" :items="items">
</v-sortable>
child component :
import { h } from 'vue';
import VDraggable from 'path/to/v-draggable'
export default {
name: 'v-sortable',
props: {
items:{
type:Array,
default: () =>[]
},
handle: {
type: String,
required: false,
default: () => {
return null;
}
},
},
render () {
const draggableItems = this.items.map(slotItem =>
h(VDraggable, { handle: this.handle }, [item])
)
return draggableItems;
}
}
The component can to be explicitly specified in render function:
h(VDraggable, ...)
Globally registered component that isn't available for import (e.g. from third-party libs) can be resolved from a name with resolveComponent.
I wanted to know how can I add a class to a modal in a navbar components? My navbar is in App.vue and I wanted to create a message that would add the class "is-active" to a modal in my navbar when I click on it. But I can't find the way to do that..
Thank you
Usually when you have a parent -> child relationship you can use events. In this case since you have two components that are not linked (directly) then you have two alternatives.
Using store (it is usually used in cases where your application is of a considerate size)
You can use vuex to have a central place where you will have your global state. A simple example would be:
store/main.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
isModalOpen: false
},
getters: {
isModalOpen => (state) => state.isModalOpen,
},
mutations: {
setIsModalOpen (state, isOpen) {
state.isModalOpen = isOpen;
}
}
})
then you can access the store in your component as such:
<template>
<navbar :class="[isNavBarOpen ? "is-active" : ""]" />
</template>
export default {
computed: {
isNavBarOpen () {
this.$store.getters['isModalOpen']
}
}
}
Event bus (it is usually used in cases where you have a small app and do not need a global state manager)
Read more about EventBus here.
You can create a simple EventBus
services/eventBus.js
import Vue from 'vue';
const export EventBus = new Vue();
then on your component when the modal is open you can do:
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
methods: {
openStore: () => {
// your logic to open modal
EventBus.$emit('modal-open');
}
}
}
then on your App.vue you just listen to this event
App.vue
<template>
<navbar :class="[isModalOpen ? "is-active" : ""]" />
</template>
// # -> is an alias to your root folder. Most projects scafolded by Vue CLI has this by default
import {EventBus} from "#/services/eventBus"
export default {
data() {
return {
isModalOpen: false,
}
},
created() {
EventBus.$on('modal-open', this.onModalOpen);
},
methods: {
onModalOpen() {
this.isModalOpen = true;
}
}
}
The one you will pick depends on our application structure and if you think it is complex enough to use a central state management (vuex).
There might contain some errors in the code but the main idea is there.
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
)
})
}
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>')
});
});
I'm trying to design a store to manage the events of my Vuex application. This far, I have the following.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
};
export default new Vuex.Store({ state, mutations });
I'm going to have a number of list items that are supposed to change the value of the data in the store when clicked. The design of the root component App and the menu bar Navigation is as follows (there will be a bunch of actions in the end so I've collected them in the file actions.js).
<template>
<div id="app">
<navigation></navigation>
</div>
</template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
<template>
<div id="nav-bar">
<ul>
<li onclick="console.log('Clickaroo... ');">Plain JS</li>
<li #click="updateData">Action Vuex</li>
</ul>
</div>
</template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
}
}
</script>
Clicking on the first list item shows the output in the console. However, when clicking on the second one, there's nothing happening, so I'm pretty sure that the event isn't dispatched at all. I also see following error when the page's being rendered:
Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm very new to Vuex so I'm only speculating. Do I need to put in reference to the updateData action in the store, alongside with state and mutations? How do I do that? What/where's the "data option" that the error message talks about? Isn't it my components state and it's properties?
Why the error
You are getting the error, because when you have <li #click="updateData"> in the template, it looks for a method updateData in the vue component which it does not find, so it throws the error. To resolve this, you need to add corresponding methods in the vue component like following:
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
What this.$store.dispatch("updateData") is doing is calling your vuex actions as documented here.
What/where's the "data option"
You don't have any data properties defined, data properties for a vue component can be used, if you want to use that only in that component. If you have data which needs to be accessed across multiple components, you can use vuex state as I believe you are doing.
Following is the way to have data properties for a vue component:
<script>
import { updateData } from "../vuex_app/actions";
export default {
date: {
return {
data1 : 'data 1',
data2 : {
nesteddata: 'data 2'
}
}
}
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
You can use these data properties in the views, have computed properies based on it, or create watchers on it and many more.