0

I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like

job = {
  id: 1,
  uuid: 'a-unique-value',
  content_blocks: [
    {
      id: 5,
      uuid: 'some-unique-value',
      block_type: 'text',
      body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
    },
    {
      id: 9,
      uuid: 'some-other-unique-value',
      block_type: 'text',
      body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
    },
  ]
}

So, I instantiate my sub-components like this

<div v-for="block in job.content_blocks" :key="block.uuid">
    <component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>

(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)

The TextContentBlock goes like this

export default {
    props: {
        data: {
            type: Object,
            required: true
        }
    },
    created: function() {
        if (!this.data.body) {
            this.data.body = {
                it: { content: "" },
                en: { content: "" }
            }
        }
    }
}

The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this

addBlock: function(block_type) {
    this.job.content_blocks = [...this.job.content_blocks, {
        block_type: block_type,
        uuid: magic_uuidv4_generator(),
        order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
    }]
}

The template for TextContentBlock is

    <b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
        <template slot="title">
            {{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
        </template>
        <b-form-textarea v-model="data.body[l].content" rows="6" />
        <div class="small mt-3">
            <code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
        </div>
    </b-tab>

Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.

However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).

Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this

data: function() {
    return {
        block_data: this.data
    }
}

and adjusted every data to be block_data but I get the same behaviour as before.

What exactly am I missing?

Morpheu5
  • 2,610
  • 6
  • 39
  • 72
  • 1
    Can you please provide https://stackoverflow.com/help/mcve. The example is not very usable in current form. – Harshal Patil Jun 27 '18 at 15:34
  • Have you inspected what happened with Vue dev tools? – Andrey Popov Jun 27 '18 at 15:46
  • mess codes you provided. but it seems your codes modify the values of `props=data`. try `data: function() { return { block_data: Object.assign({}, this.data) } }` and what is the output for `addBlock`? it seems not create property=body. – Sphinx Jun 27 '18 at 16:27
  • Andrey: I did and the whole data structure gets the objects constructed correctly. – Morpheu5 Jun 27 '18 at 17:03
  • Sphinx: I tried that, I can then change the data locally in the sub components, but the changes obviously do not propagate up to the main data structure which is the whole point. – Morpheu5 Jun 27 '18 at 17:05
  • @Morpheu5 then you should emit value to parent component like this: [v-model one prop](https://stackoverflow.com/questions/50952232/vue-js-custom-select-component-with-v-model/50952421#50952421) – Sphinx Jun 27 '18 at 17:17
  • @Sphinx so if in the future I end up with nested content blocks, I will need a mechanism to notify the parent, right? Not undoable, but basically requires passing down a reference to the top level component and emitting to that, right? – Morpheu5 Jun 27 '18 at 17:28
  • 1
    @Morpheu5 checked the demo in the answer, let me know if fix your issue. – Sphinx Jun 27 '18 at 17:44

1 Answers1

0

As the OP's comments, the root cause should be how to sync textarea value between child and parent component.

The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)

As Vue Guide said:

v-model is essentially syntax sugar for updating data on user input events, plus special care for some edge cases.

The syntax sugar will be like:

the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"

So adjust your codes to like below demo:

  1. for input, textarea HTMLElement, uses v-bind instead of v-model

  2. then uses $emit to popup input event to parent component

  3. In parent component, uses v-model to sync the latest value.

Vue.config.productionTip = false
Vue.component('child', {
  template: `<div class="child">
        <label>{{value.name}}</label><button @click="changeLabel()">Label +1</button>
        <textarea :value="value.body" @input="changeInput($event)"></textarea>
    </div>`,
  props: ['value'],
  methods: {
    changeInput: function (ev) {
      let newData = Object.assign({}, this.value)
      newData.body = ev.target.value
      this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
      // you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
    },
    changeLabel: function () {
      let newData = Object.assign({}, this.value)
      newData.name += ' 1'
      this.$emit('input', newData)
    }
  }
});

var vm = new Vue({
  el: '#app',
  data: () => ({
    options: [
      {id: 0, name: 'Apple', body: 'Puss in Boots'},
      {id: 1, name: 'Banana', body: ''}
    ]
  }),
})
.child {
  border: 1px solid green;
}
<script src="https://unpkg.com/vue@2.5.16/dist/vue.js"></script>
<div id="app">
  <span> Current: {{options}}</span>
  <hr>
  <div v-for="(item, index) in options" :key="index">
    <child v-model="options[index]"></child>
  </div>
</div>
tony19
  • 125,647
  • 18
  • 229
  • 307
Sphinx
  • 10,519
  • 2
  • 27
  • 45
  • Hey, your answer pointed me in the right direction, so I'm accepting it, but not before I published my solution somewhere so I can update my question to reference how I actually pulled this off – which is not very different from your approach, it's just I had a few additional kinks to work out. – Morpheu5 Jul 04 '18 at 09:38