user7179686
user7179686

Reputation:

Access the $refs of a parent from a child - Vue.js

Hopefully this has been answered before - essentially I'm trying to append a block ("CodeBlock.vue") to an element inside App.vue from an onClick event triggered inside a sibling of CodeBlock, and a child of App.vue, ("ButtonSidebar.vue"). I'm a little confused by emitting events and/or using an eventBus Vue instance, so any pointers would be greatly appreciated:

So far I have the following. CodeBlock.vue which will be used as an instance and appended to a div inside App.vue.

CodeBlock.vue:

<template>
  <div :class="type">
    THIS IS A CODE BLOCK!!
  </div>
</template>

<script>
export default {
  name: 'CodeBlock',
  props: [ 'type' ]
}
</script>

App.vue:

<template>
  <div id="app" class="container">
    <ButtonSidebar/>
    <div id="pageBlocks" ref="container"></div>
  </div>
</template>

<script>
import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'

// import { eventBus } from './main'

import AddTitle from './components/modules/AddTitle'
import AddSubTitle from './components/modules/AddSubTitle'
import ButtonSidebar from './components/modules/ButtonSidebar'
import CodeBlock from './components/modules/CodeBlock'

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue)

export default {
  name: 'App',
  components: {
    AddTitle,
    AddSubTitle,
    ButtonSidebar,
    CodeBlock
  }
}
</script>

<style>
#app {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #1f1f1f;
  margin-top: 60px;
}

.no-border {
  border: unset !important;
  border: 0px !important;
}
</style>

ButtonSidebar.vue:

<template>
  <div>
    <b-button class="btn-circle absolute-float-tight text-dark" v-on:click="reveal=!reveal">
      <font-awesome-icon v-if="!reveal" :icon="faPlusIcon" />
      <font-awesome-icon v-if="reveal" :icon="faMinusIcon" />
    </b-button>
    <transition name="custom-classes-transition" enter-active-class="animated bounceInDown" leave-active-class="animated bounceOutRight">
      <div v-if="reveal" class="absolute-float-reveal">
        <b-button class="btn-circle text-dark" v-on:click="addCodeBlock"><font-awesome-icon :icon="faCodeIcon" /></b-button>
      </div>
    </transition>
  </div>
</template>

<script>
import Vue from 'vue'

import FontAwesomeIcon from '@fortawesome/vue-fontawesome'
import faPlus from '@fortawesome/fontawesome-pro-regular/faPlus'
import faMinus from '@fortawesome/fontawesome-pro-regular/faMinus'
import faCode from '@fortawesome/fontawesome-pro-regular/faCode'

import CodeBlock from './CodeBlock'

export default {
  name: 'ButtonSidebar',
  computed: {
    faPlusIcon () {
      return faPlus
    },
    faMinusIcon () {
      return faMinus
    },
    faCodeIcon () {
      return faCode
    }
  },
  components: {
    FontAwesomeIcon,
    CodeBlock
  },
  data () {
    return {
      reveal: false
    }
  },
  props: ['codeBlocks'],
  methods: {
    addCodeBlock () {
      var ComponentClass = Vue.extend(CodeBlock)
      var instance = new ComponentClass({
        propsData: { type: 'primary' }
      })
      instance.$mount()

      this.$el.querySelector('#pageBlocks').appendChild(instance.$el)
    }
  }
}
</script>

<style scoped>

  .absolute-float-tight {
    left: 20px;
    position: absolute;
  }

  .absolute-float-reveal {
    left: 60px;
    position: absolute;
  }

  .btn-circle {
    background-color: transparent;
    border-radius: 50%;
    height: 34px;
    padding: 0;
    width: 34px;
  }

</style>

It's around the this.$el.querySelector('#pageBlocks').appendChild(instance.$el) part that I start to loose the plot a bit...I'm worried that I have to strip everything down and start again perhaps?

Upvotes: 4

Views: 18678

Answers (2)

Fab
Fab

Reputation: 4657

I think you could achieve it in this simple way:

App.vue (template section)

<ButtonSidebar @add="addCodeItem"/>
<div id="pageBlocks">
    <codeBlock v-for="code in arrCodes" :type="code.type"/>
</div>

App.vue (script)

export default {
  data() {
     return {
        arrCodes: []
     }
  },
  methods: {
     addCodeItem(codeType) {
        this.arrCodes.push( { type: codeType } )
     }
  }
}

ButtonSidebar.vue (script section)

addCodeBlock () {
  this.$emit('add', 'yourtype');
}

Upvotes: 1

Christophe
Christophe

Reputation: 192

You should avoid reaching to the DOM as much as possible. The source of truth for the data should be in your components. refs are very useful to integrate other js library that needs a DOM element.

So in your case, assuming codeBlocks are available in your App.vue components, the SidebarButton needs to emit an event when it's clicked so that the parent App.vue can add a new Codeblock:

(I have removed some code not needed for the example. CodeBlock.vue stays the same)

App.vue

<template>
  <div id="app" class="container">
    <ButtonSidebar @add-block="addCodeBlock" />
    <CodeBlock v-for="block in codeBlocks" :type="block.type" />
  </div>
</template>

<script>
import ButtonSidebar from '../ButtonSidebar'
import CodeBlock from '../CodeBlock'

export default {
  name: 'App',

  components: {ButtonSidebar, CodeBlock},

  data() {
      return {
          codeBlocks: []
      }
  },
  methods: {
      addCodeBlock() {
          const newBlock = {type: 'whatever'}
          this.codeBlocks.push(newBlock)
      }
  }
}
</script>

ButtonSideBar.vue

<template>
  <div>
    <b-button class="btn-circle text-dark" v-on:click="addCodeBlock</b-button>
  </div>
</template>

<script>  
export default {
  name: 'ButtonSidebar',

  data () {
    return {
      reveal: false
    }
  },

  methods: {
    addCodeBlock () {
      this.$emit('add-block')
    }
  }
}
</script>

A good pattern to follow in Vue is to lift the state to the parents and passing it down as props whenever you feel like you want to share state between parents and children.

Upvotes: 2

Related Questions