Search code examples
javascriptvue.jsfrontendcomponentsvuejs3

Vue3: How do I access a function inside the methods attribute from a component's setup() method?


I have an array containing data of questions that will be displayed. Only one question must be displayed at a time. Once the user selects an answer to a question, they can move to the next one by, either, the click of a button or using the up and down arrow keys.

Here's a gif of the present version: Screen recording gif

The App.vue file:

import { defineComponent, ref } from 'vue';
import QuestionCard from "./components/QuestionCard.vue";

export default defineComponent({
   components: {
      QuestionCard
   },
   setup() {
      const questionArray = ref([
         {
            id: "123",
            question: "Which of these is a colour in the rainbow?",
            options: [
               'brown', 'red', 'black',
            ],
         }, {
            id: "456",
            question: "How many continents does Earth have?",
            options: [
               1, 7, 6, 9
            ],
         }, {
            id: "789",
            question: "Which of these is a prime number?",
            options: [
               7, 4, 44, 
            ],
         },
      ]);

      let currentQuestion = ref(0);

      return {
         questionArray, currentQuestion
      }
   },
   methods: {
      nextQuestion() {
         if (this.currentQuestion < this.questionArray.length - 1) {
            this.currentQuestion++;
         }
      },
      previousQuestion() {
         if (this.currentQuestion > 0) {
            this.currentQuestion--;
         }
      }
   },
   mounted() {
      console.log('Mounted!!')
      window.addEventListener("keyup", (event) => {
         if (event.code == 'ArrowDown') {
            this.nextQuestion();
         }
         else if (event.code == 'ArrowUp') {
            this.previousQuestion();
         }
      });
   },
   
});
</script>


<template>
   <div>
      <div id="top_bar">
         <button @click="previousQuestion">Previous Question (Up key)</button>
         <button @click="nextQuestion">Next Question (Down key)</button>
      </div>

      <div id="question_section">
         <QuestionCard 
            :question="questionArray[currentQuestion].question"
            :answer_options="questionArray[currentQuestion].options"
         ></QuestionCard>
      </div>
   </div>
</template>

I have a component called QuestionCard that displays the questions and the answer options. Just like in the App component, I have added an event listener, in order to detect key presses. Each answer option is given a key code beginning from letter A (ASCII - 65). For example, based on the answer options of the first question in questionArray above, the QuestionCard for the first question must only respond to just 3 key codes:

  • A for brown
  • B for red
  • C for black

On either

  1. pressing one of the keys assigned to an answer option button, or
  2. by clicking it,

I want to record the answer options value and pass it to a method called storeAnswer(). I have achieved goal 1 by calling this method and passing the value when a <button> is clicked. However, I'm not quite sure how to achieve goal 2.

Here's what I have done - inside the setup() method of the QuestionCard, I have added listener to the window to detect key releases. On detection, I check if the key matches any of the assigned keys to the answer options. If so, I call the storeAnswer() method. But, I get an error saying:

Uncaught ReferenceError: storeAnswer is not defined

Here's the code of QuestionCard.vue:

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
   props: {
      name: String,
      question: String,
      answer_options: Array,
   },
   methods: {
      storeAnswer(ans: any) { 
         this.selectedAns = ans;
         console.log("Button clicked: " + ans);
      }
   },
   setup(props) {
      let selectedAns = ref(null);
      
      // loop through array of answer_options and assign a key code for each one so that it can be detected
      const keyOptions: {keyCode: string, value: any}[] = [];
      for (let i=0; i < props.answer_options.length; i++) {
         keyOptions.push(
            {
               keyCode: String.fromCharCode(65+i),
               value: props.answer_options[i]
            }
         );
      }

      window.addEventListener("keyup", (event) => {
         const keyPressed = event.key.toUpperCase();

         const i = keyOptions.findIndex(Element => Element.keyCode == keyPressed);
         if (i > -1) {            
            selectedAns.value = keyOptions[i].value;
            console.log(`Option you selected is: ${keyPressed + ')' + selectedAns.value}`)
            storeAnswer();
         }
      });

      return {
         selectedAns
      }
   },
});
</script>


<template>
   <div>
      <h4>{{ question }}</h4>
      <div>
         <ul>
            <li v-for="(option, index) in answer_options" :key="index">
               <button @click="storeAnswer(option)">
                  {{  String.fromCharCode(65 + index) + ') ' + option }}
               </button>
            </li>
         </ul>
      </div>

      <!-- display the selected answer -->
      <p>
         {{ selectedAns }}
      </p>
   </div>
</template>

Solution

  • I managed to solve my problem thanks to @Estus Flask, who pointed out my misusage of the Vue's Composition API with Options API in my code.

    Here's the working code.

    <script setup>
    import { ref, onMounted } from 'vue';
    import QuestionCard from "./components/QuestionCard.vue";
    
    
    const questionArray = ref([
       {
          id: "123",
          question: "Which of these is a colour in the rainbow?",
          options: [
             'brown', 'red', 'black',
          ],
          selectedAns: null,
       }, {
          id: "456",
          question: "How many continents does Earth have?",
          options: [
             1, 7, 6, 9
          ],
          selectedAns: 6,
       }, {
          id: "789",
          question: "Which of these is a prime number?",
          options: [
             7, 4, 44, 
          ],
          selectedAns: 4,
       },
    ]);
    
    const answerOptions = ref([]);
    const currentQuestion = ref(0);
    
    
    // methods to use in the App component -----------------------------------------------------------------
    function nextQuestion() {
       if (currentQuestion.value < questionArray.value.length - 1) {
          currentQuestion.value++;
          setAnswerOptions();
       }   
    }
    function previousQuestion() {
       if (currentQuestion.value > 0) {
          currentQuestion.value--;
          setAnswerOptions();
       }
    }
    function setAnswerOptions() {
       answerOptions.value.length = 0;
    
       for (let i=0; i < questionArray.value[currentQuestion.value].options.length; i++) {
          answerOptions.value.push(
             {
                key: String.fromCharCode(65+i),
                value: questionArray.value[currentQuestion.value].options[i]                  
             }
          );
       }
    }
    
    
    // lifecycle hooks--------------------------------------------------------------------------------
    onMounted(() => {
       // console.log('App mounted, setting up listeners!!');
    
       setAnswerOptions();
    
       window.addEventListener("keyup", (event) => {
          if (event.code == 'ArrowDown') {
             nextQuestion();
          }
          else if (event.code == 'ArrowUp') {
             previousQuestion();
          }
          
          const i = answerOptions.value.findIndex(element => element.key == event.key.toUpperCase());
          if (i > -1) {
             // console.log(`You have seleced ${answerOptions.value[i].value}, moving to next question`);
             questionArray.value[currentQuestion.value].selectedAns = answerOptions.value[i].value;
             nextQuestion();
          }
       });   
    
       // console.log('listeners all set!')
    });
    </script>
    
    
    <template>
       <div>
          <div id='top_bar'>
             <button @click=" previousQuestion ">Previous Question (Up key)</button >
             <button @click=" nextQuestion ">Next Question (Down key)</button >
          </div>
    
          <div id='question_section'>
             <QuestionCard 
                :id=" questionArray[currentQuestion].id "
                :question=" questionArray[currentQuestion].question "
                :answer_options=" questionArray[currentQuestion].options "
             ></QuestionCard>
             
             <p v-if=" questionArray[currentQuestion].selectedAns != null ">
                {{ 'Your last answer was: ' + questionArray[currentQuestion].selectedAns }}
             </p>
             
             <div id='answer_options_section'>
                <ul>
                   <li v-for=" option in answerOptions ">
                      <button :class=" {'selected-option': option.value == questionArray[currentQuestion].selectedAns } " >
                         {{  option.key + ') ' + option.value }}
                      </button>
                   </li>
                </ul>
             </div>
          </div>
       </div>
    </template>
    

    QuestionCard.vue

    <script setup>
    import { ref } from 'vue'
    
    // define the props used in this component
    const props = defineProps({
       id: String,
       name: String,
       question: String,
       answer_options: Array,
    });
    </script>
    
    
    <template>
       <div>
          <h5>Question ID: {{ id }}</h5>
          <h4>{{ question }}</h4>
       </div>
    </template>