Vue js import components dynamically - javascript

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
)
})
}

Related

vue.js dynamic component renders twice with itself as child component

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

Make shared property reactive in Vue Composition API composable by declaring variable outside of exported function

I am using the composition api plugin for vue2 (https://github.com/vuejs/composition-api) to reuse composables in my app.
I have two components that reuse my modalTrigger.js composable, where I'd like to declare some sort of shared state (instead of using a bloated vuex state management).
So in my components I do something like:
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'SearchButton',
setup(props, context) {
const { getModalOpenState, setModalOpenState } = modalTrigger();
return {
getModalOpenState,
setModalOpenState,
};
},
};
And in my modalTrigger I have code like:
import { computed, ref, onMounted } from '#vue/composition-api';
let modalOpen = false; // needs to be outside to be accessed from multiple components
export default function () {
modalOpen = ref(false);
const getModalOpenState = computed(() => modalOpen.value);
const setModalOpenState = (state) => {
console.log('changing state from: ', modalOpen.value, ' to: ', state);
modalOpen.value = state;
};
onMounted(() => {
console.log('init trigger');
});
return {
getModalOpenState,
setModalOpenState,
};
}
This works, but only because I declare the modalOpen variable outside of the function.
If I use this:
export default function () {
const modalOpen = ref(false); // <------
const getModalOpenState = computed(() => modalOpen.value);
...
It is not reactive because the modalTrigger is instantiated twice, both with it's own reactive property.
I don't know if that is really the way to go, it seems, that I am doing something wrong.
I also tried declaring the ref outside:
const modalOpen = ref(false);
export default function () {
const getModalOpenState = computed(() => modalOpen.value);
But this would throw an error:
Uncaught Error: [vue-composition-api] must call Vue.use(plugin) before using any function.
So what would be the correct way to achieve this?
I somehow expected Vue to be aware of the existing modalTrigger instance and handling duplicate variable creation itself...
Well, anyway, thanks a lot in advance for any hints and tipps.
Cheers
Edit:
The complete header.vue file:
<template>
<header ref="rootElement" :class="rootClasses">
<button #click="setModalOpenState(true)">SET TRUE</button>
<slot />
</header>
</template>
<script>
import { onMounted, computed } from '#vue/composition-api';
import subNavigation from '../../../../composables/subNavigation';
import mobileNavigation from '../../../../composables/mobileNavigation';
import search from '../../../../composables/searchButton';
import { stickyNavigation } from '../../../../composables/stickyNav';
import metaNavigation from '../../../../composables/metaNavigation';
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'Header',
setup(props, context) {
const { rootElement, rootClasses } = stickyNavigation(props, context);
mobileNavigation();
subNavigation();
search();
metaNavigation();
const { getModalOpenState, setModalOpenState } = modalTrigger();
onMounted(() => {
console.log('Header: getModalOpenState: ', getModalOpenState.value);
setModalOpenState(true);
console.log('Header: getModalOpenStat: ', getModalOpenState.value);
});
return {
rootClasses,
rootElement,
getModalOpenState,
setModalOpenState,
};
},
};
</script>
The composition API is setup somewhere else where there are Vue components mounted a bit differently than you normally would.
So I can't really share the whole code,but it has this inside:
import Vue from 'vue';
import CompositionApi from '#vue/composition-api';
Vue.use(CompositionApi)
The composition API and every other composable works just fine...

How to dynamically import CKEditor in Vue.js 3 only on client-side?

I am trying to include CKEditor 5 in my Vue.js 3 app but am struggling with including it only on client-side. I am using server-side rendering which cannot handle window that CKEditor uses, so it must only load only if the browser requests it, and not Node.js.
In the setup() method I can test for IsBrowser like this:
const IsBrowser = typeof window !== 'undefined';
How can I perform import and initialise a component only if IsBrowser is true?
I have to do the following code to make CKEditor-5 work:
<CKEditor v-if="IsBrowser" id="PostContent" class="ck-content" contenteditable="true" :editor="CKEditorInline" ></CKEditor>
<script>
import CKEditor from '#ckeditor/ckeditor5-vue/dist/ckeditor'
import CKEditorInline from '#ckeditor/ckeditor5-editor-inline/src/inlineeditor';
export default {
name: "ComponentCreate",
components: {
CKEditor: CKEditor.component
},
data() {
return {
CKEditorInline: CKEditorInline,
</script>
TLDR
Working solution (explanation is below):
<CKEditor v-if="IsBrowser && CKEditorInline"
id="PostContent"
class="ck-content"
contenteditable="true"
:editor="CKEditorInline"
></CKEditor>
<script>
import { ref, defineAsyncComponent } from 'vue';
export default {
name: "ComponentCreate",
components: {
CKEditor: defineAsyncComponent(() => {
return import('#ckeditor/ckeditor5-vue/dist/ckeditor')
.then(module => module.component)
})
},
setup() {
const IsBrowser = typeof window !== 'undefined';
let CKEditorInline = ref(null);
if (IsBrowser) {
import('#ckeditor/ckeditor5-editor-inline/src/inlineeditor')
.then(e => CKEditorInline.value = e.default)
}
return { IsBrowser, CKEditorInline }
},
};
</script>
There are two challenges here:
Conditionally load the <CKEditor> component
Conditionally load the CKEditorInline module's export
Conditionally Load <CKEditor> component
Use defineAsyncComponent to lazy load and register the component. It only loads and registers if the template actually renders it. So only when the v-if is true.
components: {
CKEditor: defineAsyncComponent(() => {
return import('#ckeditor/ckeditor5-vue/dist/ckeditor')
.then(module => module.component)
})
},
Extra challenge, not the module but the component property is needed in your case
Conditionally load CKEditorInline module export
For this dynamic module, we want the default export
let CKEditorInline = ref(null);
if (IsBrowser) {
import('#ckeditor/ckeditor5-editor-inline/src/inlineeditor')
.then(e => CKEditorInline.value = e.default)
}
Change the v-if condition
<CKEditor v-if="IsBrowser && CKEditorInline" :editor="CKEditorInline"></CKEditor>

`IonRouterOutlet` does not deep render in Ionic-React snapshot testing

I'm trying to do a snapshot of a component wrapped by Ionic React router and perform deep rendering. But IonicRouterOutlet doesn't seem to deep render in my case. Any suggestions how to fix this issue and perform a deep rendering successfully?
Here's my sample code:
import React from 'react'
import { render } from '#testing-library/react'
import { IonRouterOutlet } from '#ionic/react'
import { IonReactRouter } from '#ionic/react-router'
jest.mock('#capacitor/core', () => ({
Plugins: {
StatusBar: {
setStyle: jest.fn()
},
Storage: {
set: jest.fn(),
get: () => ""
}
},
StatusBarStyle: {
Light: "LIGHT"
},
}));
describe('component', () => {
let component
beforeEach(async () => {
component = render(
<IonReactRouter>
<IonRouterOutlet>
<div />
</IonRouterOutlet>
</IonReactRouter>
)
})
describe('snapshot', () => {
it('should match snapshot', () => {
const { asFragment } = component
expect(asFragment()).toMatchSnapshot()
})
})
})
Here's my snapshot output:
exports[`Login component snapshot should match snapshot 1`] = `
<DocumentFragment>
<ion-router-outlet />
</DocumentFragment>
`;
As you can see, the <div /> is not generated in snapshot output. So how to fix this problem?
I have found this documentation which i think would be helpful https://github.com/crossroads/app.goodchat.hk/wiki/Testing-Routing
Basically, as you have found using IonReactRouter & IonRouterOutlet causes the JSDOM to output <ion-router-outlet /> and not the full tree.
The workaround is to use React Router's Router and MemoryRouter directly, depending on what you want to access e.g history.location.pathname. IonReactRouter uses React Router under the hood so the behaviour and JSDOM output should match.

Vue3 Creating Component Instances Programmatically on button click

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)

Categories

Resources