Search code examples
javascriptvue.jsvuejs2vue-componentvuejs-slots

How to use conditional rendering with Vue.js slots?


How can I use the result of a v-if to render two different components in Vue? If it matters, I'm also trying to do this while rendering a TreeView using v-for. Here's what I tried so far.

TreeView.vue:

<template>
    <span>
        <ul v-show="isOpen" v-if="isFolder">
            <p
                v-for="(child, index) in item.children"
                v-bind:item="child"
                v-bind:key="index"
                <span v-if="!child.isFolder">
                    <slot v-bind:individualBookmark="child"></slot>
                </span>
                <span v-else> 
                    <div v-bind:class="{bold: isFolder}" v-on:click="toggle" v-on:dblclick="makeFolder" v-on:contextmenu="rightClick">
                        {{ child.name }}
                        <span v-if="isFolder">[{{ isOpen ? '-' : '+' }}]</span>
                    </div>
                </span>
            </p>
        </ul>
    </span>
</template>

<script>
export default {
  name: "TreeView",
  props: {
    item: Object
  },
  components: {
  },
  data: function() {
    return {
      isOpen: false
    }
  },
  computed: {
    isFolder: function() {
      return this.item.children.length > 0;
      //return this.item.children && this.item.children.length;
    }
  },
  methods: {
    toggle: function() {
      if (this.isFolder) {
        this.isOpen = !this.isOpen;
      }
    },
    makeFolder: function() {
      if (!this.isFolder) {
        this.$emit("make-folder", this.item);
        this.isOpen = true;
      }
    },
    rightClick: function() {
      alert("Right click action")
    }
  }
}
</script>

App.vue (the Bookmark tag is a custom component I built that does not have the isFolder computed property):

<template>
   <div id="app">
      <TreeView v-slot:default="slotProps"
                v-bind:item="treeData"
                v-on:make-folder="makeFolder"
                v-on:add-item="addItem">
        <Bookmark v-bind="slotProps.individualBookmark"></Bookmark>
      </TreeView>
   </div>
</template>

<script>
import Bookmark from './components/Bookmark.vue'
import TreeView from './components/TreeView.vue'
import Vue from 'vue'

export default {
    name: 'App',
    components: {
        Bookmark,
        TreeView
    },

    data: function() {
        return {
            treeData : {
                    name: "My Tree",
                    children: [
                      { name: "hello" },
                      { name: "wat" },
                      {
                        name: "child folder 1",
                        children: [
                          {
                            name: "child folder",
                            children: [{ name: "hello" }, { name: "wat" }]
                          },
                          { name: "hello" },
                          { name: "wat" },
                          {
                            name: "child folder",
                            children: [{ name: "hello" }, { name: "wat" }]
                          }
                        ]
                      }
                    ]
            }
          }
    },
    methods: {
        makeFolder: function(item) {
            Vue.set(item, "children", []);
            this.addItem(item);
        },
        addItem: function(item) {
            item.children.push({
              name: "new stuff"
            });
        }
    }
}
</script>

When I run this, for some reason, the v-if="!child.isFolder" in TreeView.vue always evaluates to true so it renders the Bookmark component. Even in the case when the v-if should be evaluating to false like when given the "child folder 1" child object of treeData, it still evaluates to true. I have a feeling the problem is to do with how I'm using <slot></slot> in TreeView.vue but I'm not sure. Why is the v-if always evaluating to true and never false?

The problem may also have to do with how I wrote the isFolder property.


Solution

  • Add a computed property called computedChildren inside the Treeview component which maps the item prop by adding isFolder property :

      computed: {
        isFolder: function() {
          return this.item.children.length > 0;
          //return this.item.children && this.item.children.length;
        },
       computedChildren(){
          return this.item.children.map(child=>{
               child.isFolder=child.children?child.children.length>0:false //this add the property isFolder
    
              return child
            })
        }
      },
    
    

    Then loop through it as follows :

    <p
     v-for="(child, index) in computedChildren"
    ...
    

    For replacing the slot you could use template element :

       <TreeView 
                    v-bind:item="treeData"
                    v-on:make-folder="makeFolder"
                    v-on:add-item="addItem">
         <template v-slot:default="slotProps">
            <Bookmark v-bind="slotProps.individualBookmark"></Bookmark>
        </template>
          </TreeView>