title: Vue - Component abstraction and inheritance
body: <p>I have a Vue application that allows users to create quizzes made up of different types of questions. I have multiple choice questions (both single selection/radio button and multiple selection/checkbox), open answer, attachment answer, so on and so forth.</p>
  <p>I'm looking to create a generic <code>Question</code> component that will display the correct type of input (text editor, radio buttons, file input, etc.) depending on the type of question.</p>
  <p>Let's assume I am using TS and I have a <code>Question</code> interface with a field <code>questionType</code> that easily allows differentiating among the types of questions.</p>
  <p>The solution I've come up with is the following:</p>
  <p>I have an <code>AbstractQuestion</code> component that defines the general template of the questions, i.e. there's a slot for displaying the question text and one for the inputs.</p>
  <p>Then, for each type of question, there's a component, e.g. <code>OpenAnswerQuestion</code>, in whose template there is an <code>AbstractQuestion</code> with the slots filled with the correct template parts. Finally, I expose to the rest of the app a <code>Question</code> component that uses <code>&lt;component :is=&quot;...&quot;&gt;</code> to dynamically display the correct question type.</p>
  <p>AbstractQuestion.vue</p>
  <pre><code>&lt;template&gt;
    &lt;div&gt;
      &lt;slot name=&quot;questionText&quot;&gt;
        &lt;p v-html=&quot;exercise.text&quot; /&gt;
      &lt;/slot&gt;
  
      &lt;!-- contains the input controls to answer the question
      (e.g. checkboxes, textareas...) --&gt;
      &lt;slot
        v-if=&quot;!readOnly || !$slots.readOnlyAnswer?.()&quot;
        name=&quot;submissionControls&quot;
      &gt;&lt;/slot&gt;
  
      &lt;!-- representation of a read-only answer given to the question (e.g. a code fragment for a programming exercise) --&gt;
      &lt;slot v-else name=&quot;readOnlyAnswer&quot;&gt;&lt;/slot&gt;
  
      &lt;slot name=&quot;solution&quot;&gt;&lt;/slot&gt;
    &lt;/div&gt;
  &lt;/template&gt;
  
  &lt;script lang=&quot;ts&quot;&gt;
  import { defineComponent, PropType } from &quot;@vue/runtime-core&quot;;
  export default defineComponent({
    name: &quot;AbstractQuestion&quot;,
    props: {
      question: {
        type: Object as PropType&lt;Question&gt;,
        required: true,
      },
      readOnly: {
        type: Boolean,
        default: false,
      },
    },
  });
  &lt;/script&gt;
  </code></pre>
  <p>Example of an &quot;inheriting&quot; question type:</p>
  <p>OpenAnswerQuestion.vue</p>
  <pre><code>&lt;template&gt;
    &lt;AbstractQuestion v-bind=&quot;$props&quot;&gt;
      &lt;template #submissionControls&gt;
        &lt;TextEditor
          :disabled=&quot;readOnly&quot;
          v-model=&quot;answerTextProxy&quot;
        /&gt;
      &lt;/template&gt;
  
      &lt;template #readOnlyAnswer&gt;
          &lt;p
            v-html=&quot;answerTextProxy&quot;
          /&gt;
      &lt;/template&gt;
    &lt;/AbstractQuestion&gt;
  &lt;/template&gt;
  
  &lt;script lang=&quot;ts&quot;&gt;
  import { defineComponent, PropType } from &quot;@vue/runtime-core&quot;;
  import TextEditor from &quot;@/components/ui/TextEditor.vue&quot;;
  import AbstractExercise from &quot;./AbstractQuestion.vue&quot;;
  export default defineComponent({
    name: &quot;OpenAnswerQuestion&quot;,
    props: {
      question: {
        type: Object as PropType&lt;Question&gt;,
        required: true,
      },
      answer: {
        type: String,
        default: &quot;&quot;,
      },
      readOnly: {
        type: Boolean,
        default: false,
      },
    },
    methods: {},
    computed: {
      answerTextProxy: {
        get() {
          return this.answer;
        },
        set(val: string) {
          this.$emit(&quot;updateAnswerText&quot;, val);
        },
      },
    },
    components: { TextEditor, AbstractQuestion },
  });
  &lt;/script&gt;
  </code></pre>
  <p>And the component actually exposed:
  Question.vue</p>
  <pre><code>&lt;template&gt;
    &lt;component :is=&quot;questionComponentName&quot; v-bind=&quot;$props&quot;&gt;&lt;/component&gt;
  &lt;/template&gt;
  
  &lt;script lang=&quot;ts&quot;&gt;
  import { defineComponent, PropType } from &quot;@vue/runtime-core&quot;;
  export default defineComponent({
    name: &quot;Question&quot;,
    props: {
      question: {
        type: Object as PropType&lt;Question&gt;,
        required: true,
      },
      readOnly: {
        type: Boolean,
        default: false,
      },
    },
    methods: {},
    computed: {
      questionComponentName(): string {
        const componentNames: Record&lt;QuestionType, string&gt; = {
          [QuestionType.OPEN_ANSWER]: &quot;OpenAnswerQuestion&quot;,
          // ... rest of the mapping
        };
        return componentNames[this.question.questionType];
      },
    },
  });
  &lt;/script&gt;
  </code></pre>
  <p>This seems to be working, but it feels kinda hacky. Is there a better way to achieve this specialization/dynamic dispatch/abstraction of components in Vue3?</p>