Search code examples
typescriptvue.jsvuejs3

Copying an array-read-only-property to an array-property?


I am relatively new to Vue and completely new to TypeScript. I want to pass an array of TaskSolution-Objects (TaskSolution is my custom type) as a property to a component, then I want to delete some TaskSolution-objects from the array and have the gui updated!

Here is my code where I tried to achieve this

//TaskSolution.ts
interface TaskSolution {
    task : string,
    solution: string,
};

export type { TaskSolution };

Here another file:

//ProvideArrayTest.vue
<script lang="ts">
import { defineComponent } from 'vue';
import UseArrayTest from "./UseArrayTest.vue";
import type { TaskSolution } from "./../types/TaskSolution.ts";

export default defineComponent({
  components : { UseArrayTest }
});
</script>

<template>
  <h1>ProvideArrayTest</h1>
  <UseArrayTest :sentences="[ { task: 'not smart', solution: 'stupid'  }, { task: 'not good', solution: 'bad' }]" />
</template>

<style scoped>

</style>

Here another file:

//UseArrayTest.vue
<script lang="ts">

import { ref, defineComponent } from 'vue';
import type { PropType } from "vue";
import type { TaskSolution } from "./../types/TaskSolution.ts";

export default defineComponent({
  // type inference enabled
  props: {
    sentences : {
        required: true,
        type: Array as PropType<TaskSolution[]>
    },
  },
  setup(props) {
    const sentences2 = ref<TaskSolution[]>(props.sentences);
    return { sentences2 };
  },

  methods : {
     deleteSentence(i: number, sentence : TaskSolution) {
        console.log("Delete is executed!");
        if(i==1){
            this.sentences = this.sentences.filter( (item: TaskSolution) => item !== sentence ); 
        }
        else if(i==2){
            this.sentences2 = this.sentences2.filter( (item: TaskSolution) => item !== sentence );
        }
     }
  }
})

</script>

<template>

<h1>sentences array</h1>
  
  <ul>
<li v-for="sentence in sentences">
  Task: {{ sentence.task }} Solution: {{ sentence.solution }} <button @click="deleteSentence(1,sentence)">Delete</button>
</li>
  </ul>
  
<h1>sentences2 array</h1>
 <ul>
<li v-for="sentence in sentences2">
  Task: {{ sentence.task }} Solution: {{ sentence.solution }} <button @click="deleteSentence(2,sentence)">Delete</button>
</li>
  </ul>

</template>

<style scoped>

</style>

When I run npm run dev, for sentences, the task and the solution is corretly display but I can't delete anything. For sentences2, everything works

If I run npm run build, I get the following errors

src/components/UseArrayTest.vue:24:18 - error TS2540: Cannot assign to 'sentences' because it is a read-only property.

24             this.sentences = this.sentences.filter( (item: TaskSolution) => item !== sentence ); //Can't assign to read-only property  

Is there a way to modify the property sentences directly and have the gui updated, not having to copy sentences into sentences2 in the first place?


Solution

  • TypeScript type check indicates the actual problem with what you're trying to do. As already noted in the comments, props should not be mutated. Two-way binding is achieved with a prop+event pair. The parent should own the state and not just pass static object though the prop:

    ...
    data() {
      return { sentences: [...] }
    }
    ...
    <UseArrayTest :sentences="sentences" 
    

    In case of simple reassignment modelValue prop (instead of sentences) and update:modelValue event could be used:

     deleteSentence(sentence: TaskSolution) {
       this.$emit('update:modelValue', this.sentences.filter(...))
     }
    

    This way this:

    <UseArrayTest :modelValue="sentences" @update:modelValue="sentences = $event" />
    

    Can be reduced to v-model syntax sugar:

    <UseArrayTest v-model="sentences" />
    

    It's not efficient to reassign the whole array or object on each modification, and to pass sentence to find an element. For this case this could be done with custom events, and deleteSentence could accept array index to delete:

     deleteSentence(index: number) {
       this.$emit('deleteSentence', index)
     }
    

    Then the state is mutated in the parent:

    <UseArrayTest :sentences="sentences" @deleteSentence="sentences.splice(i, 1)" />