11

My "Generateur" component is sending props to my "Visionneuse" component. Everything works fine inthe browser, but I have this message in the console:

Set operation on key "texteEnvoye" failed: target is readonly.

I really have no clue why I get this message, because I pass the prop to a ref. Here are my components: "Generateur"

<template>
  <div>
    <h1>Génération d'une carte de voeux</h1>
    <div class="board">
      <Visionneuse :texteEnvoye="texte" :taille="0.5"/>
    </div>
    <textarea
      :style="'width: 60%; resize:none;height: 100px;'"
      v-model="texte"
      placeholder="Écrivez votre texte ici">
    </textarea>
  </div>
  <div>
    <button
      v-if="lien.length == 0"
      id="boutonObtenirLien"
      v-bind:class="{ enCours: lienEnCours }"
      class="btn first"
      @click="obtenirLien">Obtenir le lien</button>
    <p
      v-if="lien.length > 0">
      Votre carte de voeux est accessible au lien suivant:<br/>
      <a :href="lien">{{ lien }}</a>
    </p>
  </div>
</template>

<script>
import Visionneuse from '@/components/Visionneuse.vue';

import axios from 'axios';

import {
  defineComponent, ref,
} from 'vue';

export default defineComponent({
  name: 'Générateur',
  components: {
    Visionneuse,
  },
  setup() {
    const texte = ref('');
    const lienEnCours = ref(false);
    const lien = ref('');

    function obtenirLien() {
      if (lienEnCours.value) {
        console.log('Je suis déjà en train de chercher!');
        return false;
      }
      lienEnCours.value = true;

      axios.post(`${process.env.VUE_APP_API_URL}/textes/creer/`, {
        texte: texte.value,
      },
      {
        headers: {
          'Content-Type': 'application/json',
        },
      })
        .then((response) => {
          console.log(response.data);
          lien.value = `${process.env.VUE_APP_URL}/carte/${response.data}`;
        })
        .catch((error) => {
          console.log(error);
        })
        .then(() => {
          lienEnCours.value = false;
        });
      return true;
    }

    return {
      texte,
      obtenirLien,
      lienEnCours,
      lien,
    };
  },
});

</script>

And the "Visionneuse"

<template>
  <div class="board">
    <canvas
      ref='carte'
      :width="size.w"
      :height="size.h"
      tabindex='0'
      style="border:1px solid #000000;"
    ></canvas>
  </div>
  <div id="texteRemplacement" v-if="petit">
    <p v-for="p in texte.split('\n')" v-bind:key="p">
      {{ p }}
    </p>
  </div>
</template>

<script>

import {
  defineComponent, onMounted, ref, reactive, nextTick, toRefs, watch,
} from 'vue';

export default defineComponent({
  name: 'Visionneuse',
  props: ['texteEnvoye', 'taille'],
  setup(props) {
    const myCanvas = ref(null);
    const carte = ref(null);
    const { texteEnvoye: texte, taille } = toRefs(props);

    const rapport = ref(0);
    const petit = ref((window.innerWidth < 750));

    const size = reactive({
      w: window.innerWidth * taille.value,
      h: window.innerWidth * taille.value,
    });

    function drawText() {
      const fontSize = 0.05 * size.w - 10;
      myCanvas.value.font = `${fontSize}px Adrip`;
      myCanvas.value.textAlign = 'center';
      myCanvas.value.fillStyle = 'lightgrey';
      myCanvas.value.strokeStyle = 'black';
      myCanvas.value.lineWidth = 0.006 * size.w - 10;
      const x = size.w / 2;
      const lineHeight = fontSize;
      const lines = texte.value.split('\n');
      for (let i = 0; i < lines.length; i += 1) {
        myCanvas.value.fillText(
          lines[lines.length - i - 1],
          x,
          (size.h * 0.98) - (i * lineHeight),
        );
        myCanvas.value.strokeText(
          lines[lines.length - i - 1],
          x,
          (size.h * 0.98) - (i * lineHeight),
        );
      }
    }

    function initCarte() {
      const background = new Image();
      background.src = '/img/fond.jpeg';
      background.onload = function () {
        rapport.value = background.naturalWidth / background.naturalHeight;
        size.h = size.w / rapport.value;
        nextTick(() => {
          try {
            myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
          } catch (e) {
            console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
          }
          if (!petit.value) {
            drawText();
          }
        });
      };
    }

    function handleResize() {
      size.w = window.innerWidth * taille.value;
      size.h = size.w / rapport.value;
      petit.value = window.innerWidth < 750;
      initCarte();
    }

    window.addEventListener('resize', handleResize);

    watch(texte, (_, y) => {
      texte.value = y;
      initCarte();
    });

    onMounted(() => {
      const c = carte.value;
      const ctx = c.getContext('2d');
      myCanvas.value = ctx;
      initCarte();
    });

    return {
      myCanvas,
      size,
      texte,
      petit,
      carte,
    };
  },
});

</script>
djcaesar9114
  • 1,880
  • 1
  • 21
  • 41

2 Answers2

17

I know you answered your own question. For the 'why', you aren't supposed to change props in the composition API, because props are used to pass reactive data from parent components to child components. The pattern is: events from child to parent, mutation from parent to child . toRef makes the data reactive, but it doesn't affect whether you can mutate it. So if you go:

const texteEnvoye = toRef(props, 'texteEnvoye');
texteEnvoye.value='foo'; // not allowed - texteEnvoye.value is read-only

if you go:

const texteEnvoye = ref('');
const texteEnvoyeRo = toRef(props,'texteEnvoye'); // react to prop
watch(texteEnvoyeRo, (value) => {
  textEnvoye.value = texteEnvoyeRo.value; // OK, textEnvoye is yours
});

now texteEnvoye is yours, and you can mutate it, and react to changes in textEnvoyeRo.

Community
  • 1
  • 1
Aaron Newman
  • 549
  • 1
  • 5
  • 27
  • Thanks for this point "toRef makes the data reactive, but it doesn't affect whether you can mutate it." And a suggested edit: toRef accepts the first parameter as the object and the second param will be the key of that object. So to make the second line of your answer correct. Change it to ` const texteEnvoyeRo = toRef(props, 'texteEnvoye'); // react to prop` – Raviteja Aug 26 '22 at 13:24
3

OK I found a solution. I separate "texteEnvoye" and "texte" and everything works fine. I don't know if this is a good way to code in composition API but it did the trick:

<template>
  <div class="board">
    <canvas
      ref='carte'
      :width="size.w"
      :height="size.h"
      tabindex='0'
      style="border:1px solid #000000;"
    ></canvas>
  </div>
  <div id="texteRemplacement" v-if="petit">
    <p v-for="p in texte.split('\n')" v-bind:key="p">
      {{ p }}
    </p>
  </div>
</template>

<script>

import {
  defineComponent, onMounted, ref, reactive, nextTick, toRefs, watch,
} from 'vue';

export default defineComponent({
  name: 'Visionneuse',
  props: ['texteEnvoye', 'taille'],
  setup(props) {
    const myCanvas = ref(null);
    const carte = ref(null);
    const { texteEnvoye, taille } = toRefs(props);
    const texte = ref('');

    const rapport = ref(0);
    const petit = ref((window.innerWidth < 750));

    const size = reactive({
      w: window.innerWidth * taille.value,
      h: window.innerWidth * taille.value,
    });

    function drawText() {
      const fontSize = 0.05 * size.w - 10;
      myCanvas.value.font = `${fontSize}px Adrip`;
      myCanvas.value.textAlign = 'center';
      myCanvas.value.fillStyle = 'lightgrey';
      myCanvas.value.strokeStyle = 'black';
      myCanvas.value.lineWidth = 0.006 * size.w - 10;
      const x = size.w / 2;
      const lineHeight = fontSize;
      const lines = texte.value.split('\n');
      for (let i = 0; i < lines.length; i += 1) {
        myCanvas.value.fillText(
          lines[lines.length - i - 1],
          x,
          (size.h * 0.98) - (i * lineHeight),
        );
        myCanvas.value.strokeText(
          lines[lines.length - i - 1],
          x,
          (size.h * 0.98) - (i * lineHeight),
        );
      }
    }

    function initCarte() {
      const background = new Image();
      background.src = '/img/fond.jpeg';
      background.onload = function () {
        rapport.value = background.naturalWidth / background.naturalHeight;
        size.h = size.w / rapport.value;
        nextTick(() => {
          try {
            myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
          } catch (e) {
            console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
          }
          if (!petit.value) {
            drawText();
          }
        });
      };
    }

    function handleResize() {
      size.w = window.innerWidth * taille.value;
      size.h = size.w / rapport.value;
      petit.value = window.innerWidth < 750;
      initCarte();
    }

    window.addEventListener('resize', handleResize);

    watch(texteEnvoye, (x) => {
      texte.value = x;
      initCarte();
    });

    onMounted(() => {
      const c = carte.value;
      const ctx = c.getContext('2d');
      myCanvas.value = ctx;
      initCarte();
    });

    return {
      myCanvas,
      size,
      texte,
      petit,
      carte,
    };
  },
});

</script>
djcaesar9114
  • 1,880
  • 1
  • 21
  • 41