Dynamic Component Question / Help

I am using / loading a dynamic component into a modal and that seems to be fine. But all the code is on the page calling the modal.

What I would like to do is on the layout page (NUXT app) use a side drawer that will open and show any content I need. But then how would I use

<component :is="currentComponent"></component>

Currently currentComponent get / set via Vuex but as stated for the modal I have to import the component ahead of time / or lazy load until its called on the page thats calling it.

ScheduleMgt.vue has to import the ScheduleForm.vue then display it when called.

components: {
    ScheduleForm: () => import('~/components/schedule/ScheduleForm')
  },

But in the drawer in the layout its on a whole other level. Any component right now I can dispatch an action / mutation to open close the drawer since thats a simple display = true / false which is coded in the layout. Its now saying open it with this content, close, open with different content.

But if I want to use all 50 forms for the site in a side drawer do I need to do the exact same thing like lazy load 50+ form in the layout.vue?

layout file:

components: {
    ScheduleForm: () => import('~/components/schedule/ScheduleForm'),
    ThisForm: () => import('~/components/thisForm'),
    ThatForm: () => import('~/components/thatForm'),
    OtherForm: () => import('~/components/otherForm'),
  },

Seems not so ‘dynamic’ in the sense I still have to type out 50+ possible imports. or am I missing something?

Is there a way to replicate the hard coded import into a function where one could pass a name and path then that wound return the same result as coding it like above?

Example Idea
Button 1

@click="loadComponent('someForm', '~/path/to/SomeForm')"

Button 2

@click="loadComponent('coolForm', '~/path/to/very/coolForm')"

Method:

loadComponent(name, path) {
     const payload = { name, path }
     this.$store.commit('setComponent', payload)
}

Then that data is now set in Vuex for the drawer to know what component to load.

Layout page would need to get the requested component and then put it into the drawer based on the 2 values in vuex as passed from the loadComponent (name, path)

name: () => import(path)

Thanks, and hope that makes sense.

Dave

What I managed to do so far is set a default side component but it only works if hard coded.

I have a computed property:

drawerComponent() {
      const sideComponent = this.$store.getters['drawerStore/getSideComponent']
      console.log(sideComponent , 'sideComponent is') // '~/components/navigation/rightNav.vue'
      return () => import(sideComponent) // says '~/components/navigation/rightNav.vue' not found

     // OR BELOW NOT BOTH RETURNS

      return () => import('~/components/navigation/rightNav.vue') // imports no problem
    }

The error page shows exactly the path saying it cant be found so it is spitting out the value its finding which is correct but why does it only work when hard coded?

Exact error is:

[Vue warn]: Failed to resolve async component: () => __webpack_require__("./components/navigation lazy recursive")(path)                                           19:21:38
Reason: Error: Cannot find module '~/components/navigation/rightNav.vue'

Any ideas anyone?

import() statements cannot be dynamic, they can only use static strings. <- Edit this is wrong, based on using a different framework.

However, you can pass components around and store them just like any other data and display them using the <component :is="..."/> Dynamic Component syntax.

Here’s an example where the modal component is stored on the $root.modalComponent property (which is a quick hack to make it easily accessible to every component) - it would be better to use refs and events or a store or something: Codesandbox

Thanks for the reply but the link simply shows " Loading Sandbox Fetching git repository…" and that’s it after a few mins of waiting. So aIl can not comment on what you were trying to show.

I think I understand what you are saying about the modal, but its not the actual modal I am referring to / in this case a side drawer.

I need / would like to ckick 1 link and it open a form in the side drawer, click another link / button and it open a different form in the drawer. Its the content inside the drawer thats changing. Similar to a modal. Its easy to open the modal from anywhere since the state is saved in Vuex any componet can call i from anywhere to open close. It needs to say open but with userForm or feedBackForm…

I can simply call a mutation to change the state.drawerComponet = userForm but on the layout page I am back to having to hardcode

layout page which holds the drawer

components: {
// every possible drawerComponent would need to be registered here on the same page as the html below
    userForm: () => import('~/components/userForm'),
.... other 50+ components that could possible be the content of the side drawer
  },

and the HTML:

<v-navigation-drawer
	v-model="rightDrawer"
	temporary
	:right="right"
	app
	light>

	<component :is="drawerComponent"></component>
</v-navigation-drawer>

I came across this and they say it worked but passed as props vs what I am using Vuex so I dont understand why as a prop it would work but as a Vuext state / string it wont?

3rd last comment on the topic

And as stated this is a Nuxt app so to better give you an idea there is the admin layout view which is the master container you can say and has the html code for the side drawer, then inside that is the main content which is the dashboard page / component, and the dashboard has ~15 widgets / self contained components that each have their own purpose. Just think of a general admin panel. So the idea is that each of this mini widgets can via Vuex say open the sideDrawer and load the userForm so user can edit their into. Or contactForm to edit the contact information. So there could be endless buttons / links to open any of possibly 100+ different components. So the user does not need to navigate to individual pages to add / edit update. You can do it all on the 1 dashboard page.

So I could like i said put the many many import components as normal into the layout page and simply use the standard:

<v-navigation-drawer
	v-model="rightDrawer"
	temporary
	:right="right"
	app
	light>

	<component :is="drawerComponent"></component>
</v-navigation-drawer>

to put the drawerComponent to show as a state in Vuex, the dashboard updates on change, opens drawer and presto there is the component as needed.

Its the fact of having to actually type out all 100+ imports which is what I am wondering is there a way around? If not I will hard code it, just seems there has to be or should be a way to dynamically load the component on demand on the fly without the 100+ hardcoded import statements.

Thats the question to sum it all up.

Thanks again

Dave

I’ve fixed the link to my codesandbox, that’s pretty much what I was trying to show.

I’ve also changed it to to expose a showModal method on the $root instance:

// template
    <Modal v-if="modalContent" :content="modalContent"/>
// script
  ...
  data() { return {
      modalContent: null,
  }},
  methods: {
    showModal(name) {
      this.modalContent = name ? () => import("./components/" + name) : null;
    }
  }

Then any button which needs to show a specific component in the sidebar ‘modal’ can reference it by filename alone:

    <button @click="$root.showModal('This')">This modal...</button>
    <button @click="$root.showModal('Other')">Other modal...</button>

Ok the sandbox worked this time, thanks.

I see what you have done. Thanks for the time and effort.

<Modal v-if="modalContent" :content="modalContent"/>

I mean I have to swap it to drawer but its all the same idea in the end, its loading content into a set area which is the idea.

Just the idea of keeping the state / name / path whatever i want to call it, in Vuex I am trying to wrap my head around your:

showModal(name) {
      this.modalContent = name ? () => import("./components/" + name) : null;
}

Some reason I may need to open the drawer again to the state it was in for some reason, having it in Vuex will allow me to do so. Not sure if I will ever need to but never know.

Late here now but I will get starting on testing it out tomorrow and post results.

Thanks again for your time and insight!

Dave

Glad I could help.

One thing though - you still have to write all of your 100+ components…

If each of these components just lets you edit a different subset of your data, could you dynamically generate a general purpose component which lets you edit arbitrary fields in your db?

e.g., instead of

<button @click="$root.showModal('editProfile')">Edit your profile</button>
<button @click="$root.showModal('editAddress')">Edit your address</button>
...
...

… and then writing 100s of “edit****” components. How about something a bit more dynamic:

<button @click="editProfile">Edit your profile</button>
<button @click="editAddress">Edit your address</button>

...

methods: {
  editProfile() {
    this.$root.setEditFields([
      {type: 'string', field: 'profile.name', label: 'Name'},
      {type: 'number', field: 'profile.age', label: 'Age', min: 13, max: 100},
    ]);
  },
  editAddress() {
    this.$root.setEditFields([
      {type: 'string', field: 'address.address', label: 'Address'},
      {type: 'string', field: 'address.suburb', label: 'Suburb'},
      {type: 'string', field: 'address.city', label: 'City'},
    ]);
  },

Then you create a general purpose EditFields component with something like:

<div v-for="field in fieldList" :key="field.field">
  <MyTextfield v-if="field.type=='string'" v-model="field.value" :label="field.label"/>
  <MySlider v-else-if="field.type=='number'" v-model="field.value" :label="field.label" :min="field.min" :max="field.max" />
  ...
</div>

You’d have to make the component do some pre-processing to fetch the current values for each field from the store, and then on submission the form can send all the new field data to the store.

Just a thought!

No worries, 90% of the components are forms and already made.

I hate for-each loop for inputs since I find forms so ugly to begin with, I try to make them much more user friendly. Legends, expanding / collapsing help sections, tool tips / some fields only show based on selections of other fields, some are chained fields like country / state / city so it would be impossible to get the end result with a set standard object / array of fields to end up visually with the look needed.

But thanks all the same for the advise :slight_smile:

Just a quick follow up as I managed to get it working with your help.

All done via Vuex so no $root stuff but same idea.
Still need to clean it up into a single commit but for testing it works.

Basically a button / link anywhere on the site can @click call a method to open modal / drawer

openModal() {
  const payload = {
    dialog: true,
    content: () => import('~/components/dashboard/schedule/ScheduleForm')
  }
  this.$store.commit('dialogStore/setContent', payload)
},

I made a custom component wrapper for the dialog like your example and placed it in the main page / layout in Nuxt and computed properties for dialog (true / false for visibility)

<!-- START DIALOG SECTION -->
    <custom-dialog v-if="dialog" />
<!-- START DIALOG SECTION -->

So in the end I can now now click a button / link anywhere and on demand on the fly lazy load the modal / drawer with any component.

Not sure if the

content: () => import('~/components/dashboard/schedule/ScheduleForm')

setup is the correct way to do so, any thoughts on that? No errors or warnings and it works, but that does not mean it is ideal…

Still a WIP but gets me closer.

Looks like you’ve got it spot-on!

Vuex is much better, I was only using $root to demonstrate the principle - I couldn’t be bothered setting up Vuex in my demo!

Glad I could help.

To make it even easier and avoid having to create lots of openModal() event handlers, you could create a prototype method:

Vue.prototype.$openModal = function(path) {
  const payload = {
    dialog: true,
    content: () => import(path)
  }
  this.$store.commit('dialogStore/setContent', payload)
}

Then you can replace the click handler with just @click="$openModal('~/components/dashboard/schedule/ScheduleForm')"

Or, just commit the path to the store directly with @click="$store.commit('dialogStore/setContent', '~/components/dashboard/schedule/ScheduleForm')", put the import() statement within the modal component wrapper and change the logic for the dialog = true/false computed property.

But that depends on your style preference and whether you’re happy with more logic inside the template and less in the script, in return for less typing. I’m not trying to encourage bad habits, just demonstrating different way of letting Vue make things easier :wink:

The prototype seems much easier and streamlined so I will give that a shot later this evening.

Thanks and good looking out!

Dave

I have this as the prototype as you had written, just added the import Vue:

import Vue from 'vue'

Vue.prototype.$openModal = function(path) {
  const payload = {
    dialog: true,
    content: () => import(path)
  }
  this.$store.commit('dialogStore/setContent', payload)
}

But this happens now:

Critical dependency: the request of a dependency is an expression

Console shows same error as yesterday with:

commons.app.js:12758 [Vue warn]: Failed to resolve async component: function content() {
      return __webpack_require__("./plugins lazy recursive")(path);
    }
Reason: Error: Cannot find module '~/components/dashboard/schedule/ScheduleForm'

And calling it from the button like so:

@click="$openModal('~/components/dashboard/schedule/ScheduleForm')"

Any ideas?
The modal opens but no longer the component / content being requested is imported into it.

It doesn’t like import(path) being an expression when used with lazy loading, it needs the import to be a fixed string like you had it previously. This is what I wrote in the first line of my very first reply above, but then I changed that when I got it working in my codesandbox (but that wasn’t using lazy loading).

You have to go back to the way you were doing it previously with fixed strings:

  const payload = {
    dialog: true,
    content: () => import('~/components/dashboard/schedule/ScheduleForm')
  }
  this.$store.commit('dialogStore/setContent', payload)
},

Sorry about that.

No worries. In theory the prototype idea sounded good. But there will be other config settings to pass to the dialog so a simple click and load just the path would not have worked for the site in the end.

As I understand it, technically this is a limitation of Webpack/Babel because it needs to know about every file when it compiles.

v8 implementation allows for this: https://v8.dev/features/dynamic-import#dynamic

Example: https://next.plnkr.co/edit/t4pYWx1b2tbmTFHHak4p?p=preview&preview

Thanks, that was what I was going for.
And v8?
No clue what that is so if I have to ask then I should probably not be using it.

But the article is far beyond my scope of things.
Your example is easy enough to follow though.

v8 is Google’s implementation of a JavaScript engine. In a nutshell (and I’m greatly simplifying), JS is a generally considered an interpreted language because an interpreter runs it “on the fly” rather than being a compiled language such as C++. v8 is an interpreter which is built with C++, it’s the “engine” that runs our JS in Chrome and Node.js for example.

Anyway, there’s many layers to the architecture which we work upon. If you’re interested this is a good read on it: https://dev.to/areknawo/javascript-from-the-inside-out-353k