David Folkner
David Folkner

Reputation: 1199

VUE 2+ accessing child methods

I have a situation where in VUE I need to access a child function from within the child as well as from the parent. I have a working example, but there must be a better way to access the child function from the parent.

The example has a couple of event listeners that should expand when clicked or keypressed. I want to use the child.do_something method from the parent with the global EventListener.

Is there a better way to use the child than this?

// CHILD is a component that needs access to its own click method
var child = {
  props: ['item'],

  data: function() {
    return {
      isActive: false
    }
  },

  template: `
    <div class="key" :class="{playing: isActive}" v-on:click='do_something'>
      <kbd class="noselect">{{ item.kbd }}</kbd>
    </div>
  `,

  methods: {
    do_something: function(event) {
      this.isActive = !this.isActive
      // DO OTHER STUFF too
    },
  },
}


//PARENT also need to access do_something for the right object when a key is pressed
var parent = new Vue({
  el: '#parent',
  data: {
    keysList: [{
        keyCode: "65",
        kbd: "A"
      },
      {
        keyCode: "83",
        kbd: "S"
      },
    ],
  },
  components: {
    'child': child,
  },
  methods: {
    keystroke: function(keyCode) {
      //FIND THE CHILD AND EXECUTE...THIS IS THE TERRIBLE PART
      const child = this.$children.find(child => {
        return child.$vnode.data.key === keyCode.toString()
      });
      child.do_something()
    }
  },
  created: function() {
    window.addEventListener('keydown', (e) => this.keystroke(e.keyCode));
  }
})
.keys {
  display: flex;
  flex: 1;
  min-height: 100vh;
  align-items: center;
  justify-content: center;
}

.key {
  margin: 1rem;
  transition: all .07s ease;
  color: white;
  background: rgba(0, 0, 0, 0.77);
}

.playing {
  transform: scale(1.1);
  border-color: #ffc600;
  box-shadow: 0 0 1rem #ffc600;
}

kbd {
  display: block;
  font-size: 4rem;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<div class="keys" id='parent'>
  <child v-for="(item,index) in keysList" :item="item" :key="item.keyCode"></child>
</div>

Upvotes: 2

Views: 586

Answers (2)

Karthikeyan
Karthikeyan

Reputation: 302

You can achieve this by adding one more property active to the items in your parent data keysList & update the active property for the matching keystroke . As you are passing the item as prop to your child, you can use the item.active from the props. You don't need the isActive data in your child.

Parent:

    var parent = new Vue({
  el: '#parent',
  data: {
    keysList: [{
        keyCode: "65",
        kbd: "A",
        active: false
      },
      {
        keyCode: "83",
        kbd: "S",
        active: false
      }
    ],
  },
  components: {
    'child': child,
  },
  methods: {
    keystroke: function(keyCode) {
      this.keysList.forEach(key => {key.active = key.keyCode === keyCode.toString()});
    }
  },
  created: function() {
    window.addEventListener('keydown', (e) => this.keystroke(e.keyCode));
  }
})

<div class="keys" id='parent'>
  <child v-for="(item,index) in keysList" :item="item" :key="item.keyCode" :isActive="item.active"></child>
</div>

Child:

    var child = {
  props: ['item'],

  template: `
    <div class="key" :class="{playing: item.active}" v-on:click='do_something'>
      <kbd class="noselect">{{ item.kbd }}</kbd>
    </div>
  `,

  methods: {
    do_something: function(event) {
       // DO OTHER STUFF too
    },
  },
}

This way you are managing the state just at one place and you don't need to access your child from your parent

Upvotes: 2

Bert
Bert

Reputation: 82439

You could use a ref and then reference the particular child you need by index. This would allow you to avoid using the internal values.

<child v-for="(item,index) in keysList" :item="item" :key="item.keyCode" ref="children"></child>

And in keystroke:

const index = this.keysList.findIndex(k => k.keyCode == evt.keyCode)
this.$refs.children[index].do_something()

Here your code modified.

// CHILD is a component that needs access to its own click method
var child = {
  props: ['item'],

  data: function() {
    return {
      isActive: false
    }
  },

  template: `
    <div class="key" :class="{playing: isActive}" v-on:click='do_something'>
      <kbd class="noselect">{{ item.kbd }}</kbd>
    </div>
  `,

  methods: {
    do_something: function(event) {
      this.isActive = !this.isActive
      // DO OTHER STUFF too
    },
  },
}


//PARENT also need to access do_something for the right object when a key is pressed
var parent = new Vue({
  el: '#parent',
  data: {
    keysList: [{
        keyCode: "65",
        kbd: "A"
      },
      {
        keyCode: "83",
        kbd: "S"
      },
    ],
  },
  components: {
    'child': child,
  },
  methods: {
    keystroke: function(evt) {
      const index = this.keysList.findIndex(k => k.keyCode == evt.keyCode)
      this.$refs.children[index].do_something()
    }
  },
  created: function() {
    window.addEventListener('keydown', this.keystroke);
  }
})
.keys {
  display: flex;
  flex: 1;
  min-height: 100vh;
  align-items: center;
  justify-content: center;
}

.key {
  margin: 1rem;
  transition: all .07s ease;
  color: white;
  background: rgba(0, 0, 0, 0.77);
}

.playing {
  transform: scale(1.1);
  border-color: #ffc600;
  box-shadow: 0 0 1rem #ffc600;
}

kbd {
  display: block;
  font-size: 4rem;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<div class="keys" id='parent'>
  <child v-for="(item,index) in keysList" :item="item" :key="item.keyCode" ref="children"></child>
</div>

Or a slightly more direct approach using a different ref for each child.

<child v-for="(item,index) in keysList" :item="item" :key="item.keyCode" :ref="item.keyCode"></child>

And in keystroke:

this.$refs[evt.keyCode][0].do_something()

The unfortunate part here is that because the ref is set as part of a loop, each ref is an array of one element. If I think of a way around that I'll edit it in.

And here is that working.

// CHILD is a component that needs access to its own click method
var child = {
  props: ['item'],

  data: function() {
    return {
      isActive: false
    }
  },

  template: `
    <div class="key" :class="{playing: isActive}" v-on:click='do_something'>
      <kbd class="noselect">{{ item.kbd }}</kbd>
    </div>
  `,

  methods: {
    do_something: function(event) {
      this.isActive = !this.isActive
      // DO OTHER STUFF too
    },
  },
}


//PARENT also need to access do_something for the right object when a key is pressed
var parent = new Vue({
  el: '#parent',
  data: {
    keysList: [{
        keyCode: "65",
        kbd: "A"
      },
      {
        keyCode: "83",
        kbd: "S"
      },
    ],
  },
  components: {
    'child': child,
  },
  methods: {
    keystroke: function(evt) {
      this.$refs[evt.keyCode][0].do_something()
    }
  },
  created: function() {
    window.addEventListener('keydown', this.keystroke);
  },
})
.keys {
  display: flex;
  flex: 1;
  min-height: 100vh;
  align-items: center;
  justify-content: center;
}

.key {
  margin: 1rem;
  transition: all .07s ease;
  color: white;
  background: rgba(0, 0, 0, 0.77);
}

.playing {
  transform: scale(1.1);
  border-color: #ffc600;
  box-shadow: 0 0 1rem #ffc600;
}

kbd {
  display: block;
  font-size: 4rem;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<div class="keys" id='parent'>
  <child v-for="(item,index) in keysList" :item="item" :key="item.keyCode" :ref="item.keyCode"></child>
</div>

Upvotes: 3

Related Questions