Search code examples
vue.jscontent-management-systemvuexserver-side-renderingnuxt.js

Nuxt: Mounting dynamic components to the DOM that is not rendered yet


Scenario:
I am using Nuxt in universal mode. The app works with a headless CMS that provides a simple layout of components that should be rendered, along with component names and their props - something like this:

[
  {
    "tag": "div",
    "class": "col",
    "component": "dogs",
    "props": {
      "dogs": [
        {
          "id": 1,
          "name": "Barky"
        },
        {
           "id": 2,
           "name": "Jumpy"
        }
      ]
     }
   },
   {
    "tag": "div",
    "class": "col",
    "component": "cats",
    "props": {
      "cats": [
        {
           "id": 1,
           "name": "Miouwy"
        },
        {
           "id": 2,
           "name": "Fluffy"
        }
      ]
    }
  }
]

As I understand, I have to apply the components to the DOM before Nuxt makes the "snapshot" and delivers it to the client. My plan was to mount the components in the created() lifecycle - which is, only looking by the names, not the way to go.

The main problem:
I want to mount components dynamically to the DOM, which does not exist yet.


As an example of the thing I want to escape from - mounting the components after Nuxt delivered the snapshot - in the mounted() lifecycle.

<template>
  <section class="container">
    <div ref="layout-container" class="row"></div>
  </section>
</template>

<script>
  import { mapGetters } from 'vuex';
  import Vue from 'vue';
  import Dogs from '@/components/Dogs';
  import Cats from '@/components/Cats';

  export default {
    components: {
      Dogs,
      Cats
    },
    fetch({ store }) {
      store.dispatch('landing/updateContent');
    },
    computed: {
      ...mapGetters({
        content: 'landing/content',
      })
    },
    beforeCreate() {
      Vue.component('dogs', Dogs);
      Vue.component('cats', Cats);
    },
    mounted() {
      this.content.forEach((item) => {
        const CompClass = Vue.component(item.component);
        const instance = new CompClass({ propsData: item.props }).$mount();
        this.$refs['layout-container'].appendChild(instance.$el);
      })
    }
  };
</script>

Big thanks for any directions in advance!

EDIT: Repo with this example: https://github.com/jpedryc/nuxt-test-render


Solution

  • SOLUTION
    My main problem was creating a rendered layout and trying to hook it up after the DOM was already delivered to the client. Instead, I should render the component within the virtual Vue DOM - right before the "snapshot" moment.

    And that's what I did eventually - not mount a rendered component:

    <template>
      <section class="container">
        <page-component/> <!-- A component without <template>, but with render() -->
      </section>
    </template>
    

    The PageComponent consists only of:

    import ...
    
    export default {
      components: {
        Dogs,
        Cats,
      },
      beforeCreate() {
        Vue.component('dogs', Dogs);
        Vue.component('cats', Cats);
      },
      render: function (h) {
        return createDynamicLayout(h);
      },
    }
    

    The createDynamicLayout(h) is just a simple function that creates a tree of:

    return h('div', { 'class': 'row' }, [
      h('div', { 'class': 'col' }, h(<somwhere_call_dogs_component>)),
      h('div', { 'class': 'col' }, h(<somwhere_call_cats_component>)),
    ])