0
votes

I haven't quite seen other questions which try to answer what I'm doing and I'm not sure if it's just that I'm not searching for the correct verbage so I'll explain what's going on.

I'm trying to develop an interface where the user can drag or input an image with vue. I also have a component which I've made called Card which is a bit of vue and bulma for a card-like object. The Card component has two props, title and content. Very straight forward to use when what you want to fill in the title and content properties are pre-loaded text or image data.

However, what I want to do is place another vue component inside the Card (a PictureInput component from vue-picture-input on npm). I'm trying to do this by passing it as the content prop. I have read the vue documentation on reactive props and dynamic components, but neither really talk about this use case.

Code:

Card.vue:

    <template>
      <div class="card is-size-7-mobile is-fluid">
        <div class="card-header">
          <a class="card-header-title">
            <span class="is-info">{{this.title}}</span>
          </a>
          <span v-if="this.isExpanable">
            <a class="card-header-icon card-toggle">
              <span class="icon" @click="arrowClick">
                <span v-if="!this.open">
                  <font-awesome-icon :icon="['fas', 'angle-down']"></font-awesome-icon>
                </span>
                <span v-else>
                  <font-awesome-icon :icon="['fas', 'angle-up']"></font-awesome-icon>
                </span>
              </span>
            </a>
          </span>
        </div>
          <slide-up-down :active="this.open" :duration="400" v-if="this.isExpanable">
            <div v-if="this.open" class="card-content is-size-7-mobile">
              <div class="content">{{this.content}}</div>
            </div>
          </slide-up-down>
          <div v-else>
            <div class="content">{{this.content}}</div>
          </div>
        </div>
    </template>

    <script>
    export default {
      data() {
        return {
          open: false
        };
      },
      props: {
        title: "",
        content: null,
        isExpanable: false
      },
      methods: {
        arrowClick(event) {
          this.open = !this.open;
        }
      }
    };
    </script>

    <style scoped>
    .card {
      border-radius: 1em;
      background-color: transparent !important;
      border-color: transparent !important;
    }
    .card-header {
      box-shadow: transparent;
    }
    </style>

ImageInput.vue:

    <template>
    <div class="content">
        <Card :title="makeCardTitle()" :content="makeCardContent()"/>
    </div>
    </template>

    <script>
    import PictureInput from 'vue-picture-input';
    import Card from './Card';
    import Vue from 'vue';

    export default {
        name: 'ImageInput',
        components: {
            PictureInput,
            Card
        },
        props: {
            idx:{
                type:Number,
                default: 0
            }
        },
        data() {
            return {
                pictureComponent: "PictureInput"
            }
        },
        methods: {
            makeCardTitle: function(){
                return "page ".concat(String(this.idx))
            },
            makeCardContent: function(){
                var PictureInputClass = Vue.extend(PictureInput);
                var pictureInputInstance = new PictureInputClass();
                return pictureInputInstance
            }
        }
    }
    </script>

    <style scoped>

    </style>

As you can see with the ImageInput.vue code I've tried to dynamically construct the PictureInput object and pass it as the prop content to Card but it doesn't work, I get two errors one being a "Cyclic object value" in Card and the other being that the PictureInputClass object doesn't have a toJSON method, which I'm guessing some part of the vue framework expects. I've also tried just doing: <Card :title="makeCardTitle()" :content="PictureInput"/>, but I end up getting errors about how the PictureInput isn't defined but is referenced during render. Maybe I can change content to a named <slot></slot> but I'm not sure if that's what I want either.

The reason I'm going through all of this is that I really appreciate how Vue is designed to allow components to be re-used and re-integrated into each other when creating an interface. I obviously could just take (most of) the template from Card and re-configure it for this case but I wanted to see if there was an easier way to get the PictureInput component to be a child of the Card component.

1
Slots seem more appropriate here. - Eric Guan

1 Answers

1
votes

I would use slots to place the PictureInput component into the Card component. I removed some HTML for a simplified example.

Card.vue

<template>
  <div class="card is-size-7-mobile is-fluid">
        <div class="card-header">
            <a class="card-header-title">

                <!-- Don't need to use "this" keyword inside the template,
                it is implied that the context is already "this" -->
                <span class="is-info" @click="isExpanable = !isExpanable">{{title}}</span>
            </a>
        </div>
        <div v-if='isExpanable'>
            <slot/> <!-- Slot where the <PictureInput/> will be inserted -->
        </div>
    </div>
</template>

<script>
export default {
  data() {
    return {
      isExpanable: false, // internal state to manage the open/close toggle
    }
  },
  props: {
    title: String, // Define the data type
  }
};
</script>

<style scoped>
.card {
  border-radius: 1em;
  background-color: transparent !important;
  border-color: transparent !important;
}
.card-header {
  box-shadow: transparent;
}
</style>

ImageInput.vue

<template>
<div class="content">
    <Card :title="makeCardTitle">
        <PictureInput/>
    </Card>
</div>
</template>

<script>
import PictureInput from 'vue-picture-input';
import Card from './Card';

export default {
    name: 'ImageInput',
    components: {
        PictureInput,
        Card
    },
    props: {
        idx:{
            type:Number,
            default: 0
        }
    },
    computed: {
        // Should be a computed property since we are creating data from internal data "idx"
        makeCardTitle: function(){
            return "page ".concat(String(this.idx));
        }
    }
}
</script>

Hopefully this guides you in the right direction. Also, you shouldn't have to instantiate with Vue.extend(PictureInput). Usually, all you need to do is add it to the components option key.