javascriptvuejs3vue-router4vue-dynamic-components

Vue3: Nested Routes and Dynamic Layout component


Hi Vue enthusiasts out there, I have been working on an multi-tenant application and stuck at dynamic layout problem.

Requirement: Load tenant specific layout.vue file from public folder and wrap <router-view> around it.

Tried few things like dynamic imports, defineAsyncComponent etc but couldn't get it working.

   // router:
        import store from '../store/index';
        import NestedApp from '../views/NestedApp.vue';
        // const layoutA = () => defineAsyncComponent(import(store.getters.pageLayout('LayoutA')));

        const routes = [
          {
            path: '/:tenant:/:locale',
            name: 'NestedApp',
            component: NestedApp,
            children: [
             {
                path: 'about',
                name: 'About',
                component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
                meta: { layout: () => import(store.getters.pageLayout('LayoutA')) }
              }
            ]
        ]


    // NestedApp.vue:
    <template>
      <div class="NestedApp">
        <navbar/>
        <component :is="layoutWrapper">
          <router-view/>
        </component>
      </div>
    </template>
    <script>
    import Navbar from '../components/Navbar.vue';
    export default {
      name: 'NestedApp',
      components: {
        Navbar,
      },
      computed: {
        layoutWrapper() {
          console.info(`layout: ${this.$route.meta.layout}`);
          return this.$route.meta.layout || 'div';
        }
      }
    }
    
    // LayoutA.vue:
    <template>
      <div class="LayoutA">
          <span>Layout A</span>
          <slot/>
      </div>
  </template>
   

I get following error in browser console: enter image description here


Solution

  • Got a workaround to this problem. Sending component via template string from backend API call and then creating a component out of it via defineComponent and markRaw methods.

    API response:

    "Layouts": {
            "LayoutA": {
                "name": "LayoutAbout",
                "template": "<div class='LayoutA' style='background-color: darkgray'><span>Layout A</span><slot/></div>"
            }
    },

    and then use in App.vue:

        import { defineComponent, markRaw } from 'vue';
    
        export default {
          name: 'App',
          methods: {
            loadLayout(pageLayout) {
              const layout = this.$store.getters.pageLayout(pageLayout);
    
              this.layoutWrapper = layout ? defineComponent(markRaw({...layout})) : 'div'; 
            }
          },
        created() {
          this.loadLayout(this.$route.meta.layout);
        },
        beforeRouteUpdate(to) {
          this.loadLayout(to.meta.layout);
        },
    }
    <template>
      <div class="App">
        <navbar/>
        <component :is="layoutWrapper">
          <router-view/>
        </component>
      </div>
    </template>