user242315
user242315

Reputation:

Vue: How do I render a dynamic component based on parent data?

I am trying to render components based on the data that is passed to the parent page.

I am trying to use dynamic components to render the correct components based on the JSON object @type and grab the file name in the url (without the extension) of that object to then render the related component and pass the data from Page.vue to the rendered components.

If I set :is="content.@type" (as an example) then the output will render child components with the name Image & Video (removing everything in the URL expect for the filename) to render the correct component.

Is this possible or is there a better approach to this?

Data:

Content: object
  Content [Array]
    0:Object
      @id: "4effb045"
      @type: "http://url/images.json"
      _meta: Object
         name: "TestingImage"
    1:Object
      @id: "4effb045"
      @type: "http://url/video.json"
      _meta: Object
         name: "TestingVideo"

The data comes from a CMS and I'm not able to change the data.

Page.vue

<template>
  <div id="page" v-if="content" class="page-wrapper container" :cid="this.$root.cidItem">

    <div class="row">

      <div class="page__items">

        <component v-for="content in content.content" :is="content.@type" :key="content.id"></component>

      </div>
    </div>

  </div>
</template>

<script>
  import axios from 'axios'
  import Images from "./components/Images"
  import Video from "./components/Video"
  import TextType from "./components/TextType"
  import Card from "./components/Card"

  export default {
    name: 'page',
    props: ['cid'],
    components: {
      Images,
      Video,
      TextType,
      Card
    },

    mounted () {
      axios({
        method: 'GET',
        'url': this.$root.contentDeliveryUrl + this.$root.encodedQuery + this.$root.cidItem + this.$root.store
      }).then(result => {
        // eslint-disable-next-line
        this.content = amp.inlineContent(result.data)[0];
        console.log(this.content)
      }, error => {
        console.error(error)
      })
    },
    data () {
      return {
        content: []
      }
    }

  }
</script>

<style lang="scss">
  @import '../node_modules/bootstrap-css-only/css/bootstrap.min.css';
  .container {
    max-width: 1366px;
  }
</style>

Child Component Example Images.vue

<template>
  <div class="images-wrapper container">
    <div class="row">
      <div class="col">
        <div class="images">
          {{content.imageAltText}}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>

export default {
  name: 'Images'
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
  .images-wrapper {
    &__image {

    }
  }
</style>

Upvotes: 2

Views: 2665

Answers (2)

Yom T.
Yom T.

Reputation: 9190

As mentioned in the comment, you could parse the URL in a method with v-bind:is directive. You've probably figured it out by now, but here's my quick attempt.

const content = {
  content: [{
    '@id': '4effb045',
    '@type': 'http://url/images.json',
    _meta: {
      name: 'TestingImage'
    }
  }, {
    '@id': '4effb046',
    '@type': 'http://url/videos.json',
    _meta: {
      name: 'TestingVideo'
    }
  }]
}

new Vue({
  el: '#app',

  data() {
    return {
      content
    }
  },

  methods: {
    extractName(content) {
      const [name] = /[^\/]*(?=\.\w+$)/.exec(content['@type']);

      return name;
    }
  },

  components: {
    Images: {
      template: '<img src="https://via.placeholder.com/150x45" />'
    },

    Videos: {
      template: '<div>Some video component</div>'
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <component 
    v-for="content in content.content" 
    :is="extractName(content)" 
    :key="content['@id']">
  </component>
</div>

BTW, I would advise against using HTML reserved words as the component names (like in your case <video>) as it may fail to compile. See this related issue on GitHub.

[Vue warn]: Do not use built-in or reserved HTML elements as component id: Dialog

Upvotes: 2

Raphael Parreira
Raphael Parreira

Reputation: 468

Your approach is correct. You can use the is directive to call your components. It is the best way.

As you asked, you need a method in your Page.vue to parse your @type.

methods: {
    parseType: function(type) {
        let typeParts = type.split("/")
        let filenameParts = typeParts[typeParts.length - 1].split('.')
        let parsedType = filenameParts[0]
        return parsedType 
    }
}

Now you can use this function in your template.

<component v-for="content in content.content" :is="parseType(content.@type)" :key="content.id"></component>

Upvotes: 1

Related Questions