1
votes

I have an an Array of Objects like this :

    let tree = [
    {
       task: "Some Task",
       spentTime : 2, 
       subTasks: {
          task: "Some Sub Task",
          spentTime: 1,
             subTasks:{
                task:"Some sub sub task",
                spentTime:30
             }
       }
    }
 ]

As you can see here i have this type of tree structure and i am displaying that some kind of nested accordion. So every node has a input box which has 2 way binding with spentTime property ( using v-model ).

Now if i type in any of the node's input. i need to do some operation on these spentTime values and re-populate Or insert different values in the same object.

Here i was thinking doing deep watch. But i think this will cause infinite loop because i am changing the same object and assigning value back and it triggers watch again :)

What i can do if i want to trigger a function on input change and put different values back in the same object.

Thanks!

3
Could you please define the user experience with complete disregard of technical implementation? What should happen to spentTime of parent when spentTime of child changes? And what should happen to spentTime of children when spentTime of parent changes? What's the expected relation between them and which value should be prevalent when they don't add up? - tao
Okay. If i change any spent time i need to update parents spent time. Basically just add up to parents spent time. To calculate this i am using post order tree traversal. is that you asked?) - Rakesh K
Yes. Let's say you hard-coded a parent spent time. It says 12. But its children add up to 2. And you change one of the children and they now add up to 4. Do you want to override the initial 12 with 4? You don't seem to have a technical implementation issue. It's more of a requirements definition issue. As long as you know what should happen, it's easy to do. :). We're getting back to my initial question: "Which value should be prevalent in case of conflicts?" - tao
Yes. exactly. i have a function which traverse the full tree and calculate required values and put it back. But then how do i listen for change ? - Rakesh K
Does "Yes" mean "Yes, 12 should be overridden with 4"? If you think it's relevant, provide the function you have. Eventually, create a minimal reproducible example even if it creates a stack overflow. At least we can see the required logic. - tao

3 Answers

1
votes

tl;dr

The clean solution, based on @djiss suggestion, and which correctly bubbles up to top parent, using $set and watch, is here: https://codesandbox.io/s/peaceful-kilby-yqy9v
What's below is the initial answer/logic, which uses $emit and the task 'key' to move the update in the parent.


In Vue you can't modify the child directly. I mean, you can, but you shouldn't. When you do it, Vue warns you about it informing you the change you just made will be overridden as soon as the parent changes.

The only options are to use state to manage the single source of trouth for your app (Vuex or a simple Vue object), or you call the parent telling it: "Change this particular child with this particular value". And you simply listen to changes coming from parent.

Which is what I did here:

const task = {
  task: "Some Task",
  spentTime: 2,
  subTasks: [{
    task: "Some Sub Task",
    spentTime: 1,
    subTasks: [{
      task: "Some sub sub task",
      spentTime: 30
    }, {
      task: "Some other sub sub task",
      spentTime: 12
    }]
  }]
};

Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('Task', {
  template: `
  <div>
    <h2>{{task.task}} ({{spentTime}})</h2>
    <div v-if="hasTasks">
      <Task v-for="(t, k) in task.subTasks" :key="k" :task="t" @fromChild="fromChild" :tid="k"/>
    </div>
    <input v-else v-model="localTime" type="number" @input="updateParent(localTime)">
  </div>
  `,
  props: {
    task: {
      type: Object,
      required: true
    },
    tid: {
      type: Number,
      default: 0
    }
  },
  data: () => ({
    localTime: 0
  }),
  mounted() {
    this.updateParent(this.spentTime);
  },
  computed: {
    spentTime() {
      return this.hasTasks ? this.subtasksTotal : this.task.spentTime;
    },
    subtasksTotal() {
      return this.task.subTasks.map(t => t.spentTime).reduce(this.sum, 0)
    },
    hasTasks() {
      return !!(this.task.subTasks && this.task.subTasks.length);
    }
  },
  methods: {
    fromChild(time, task) {
      this.task.subTasks[task].spentTime = time;
      this.updateParent(this.spentTime);
    },
    updateParent(time) {
      this.$emit("fromChild", Number(time), this.tid);
      this.localTime = this.spentTime;
    },
    sum: (a, b) => a + b
  },
  watch: {
    "task.spentTime": function() {
      this.localTime = this.task.spentTime;
    }
  }
});
new Vue({
  el: "#app",
  data: () => ({
    task
  }),
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <Task :task="task" :tid="0" />
</div>

It will consume any tree you throw at it, provided it has the same structure. The logic is: show the input if no subtasks or calculate from subtasks otherwise.
Obviously, you can change that to fit your needs.

1
votes

I had similar reactivity issues with Vue.js Try to use Vue.set or this.$set to save any changes to your array :

this.$set(this.someObject, 'b', 2)
  • You can read more about Vue set here.
  • You can read more information about Vue reactivity here.
0
votes

I ran into this headache before, and I did solve/ cheat on it with deep watch and Lodash _.cloneDeep and _.isEqual.

Inside your child component, create your own data componentTask. You will watch your componentTask and your prop. Every time they change, compare them using _.isEqual. When componentTask changes, emit an event to its parent.

SubTask:

<template>
    <div>
        <input type="text" v-model="componentTask.task">
        <input type="number" min="0" v-model.number="componentTask.spentTime">
        <SubTask v-if="task.subTasks" @task-change="handleTaskChange" :task="task.subTasks" />
    </div>
</template>

<script lang="ts">
    import {Vue, Component, Prop, Watch} from 'vue-property-decorator'
    import {Task} from "@/components/Test/Test";
    import _ from "lodash";

    @Component
    export default class SubTask extends Vue {
        @Prop() task!: Task;

        componentTask: Task | undefined = this.task;

        @Watch('task', {deep: true, immediate: true})
        onTaskChange(val: Task, oldVal: Task) {
            if (_.isEqual(this.componentTask, val))
                return;

            this.componentTask = _.cloneDeep(val);
        }

        @Watch('componentTask', {deep: true, immediate: true})
        onComponentTaskChange(val: Task, oldVal: Task) {
            if (_.isEqual(val, this.task))
                return;

            this.$emit("task-change");
        }

        handleTaskChange(subTasks: Task){
            this.componentTask = subTasks;
        }
    }
</script>

Parent class:

<template>
    <div style="margin-top: 400px">
        <h1>Parent Task</h1>
        <br>
        <div style="display: flex;">
            <div style="width: 200px">
                <h4>task</h4>
                <p>{{task.task}}</p>
                <p>{{task.spentTime}}</p>
                <br>
            </div>
            <div style="width: 200px">
                <h4>task.subTasks</h4>
                <p>{{task.subTasks.task}}</p>
                <p>{{task.subTasks.spentTime}}</p>
                <br>
            </div>
            <div style="width: 200px">
                <h4>task.subTasks.subTasks</h4>
                <p>{{task.subTasks.subTasks.task}}</p>
                <p>{{task.subTasks.subTasks.spentTime}}</p>
                <br>
            </div>
        </div>
            <SubTask :task="task" @task-change="handleTaskChange"/>
    </div>
</template>

<script lang="ts">
    import {Vue, Component, Prop} from 'vue-property-decorator'
    import SubTask from "@/components/Test/SubTask.vue";
    import {defaultTask, Task} from "@/components/Test/Test";

    @Component({
        components: {SubTask}
    })
    export default class Test extends Vue {
        task: Task = defaultTask;

        handleTaskChange(task: Task) {
            this.task = task;
        }
    }

</script>

Defined interface:

export interface Task {
    task: string;
    spentTime: number;
    subTasks?: Task;
}

export const defaultTask: Task = {
    task: "Some Task",
    spentTime : 2,
    subTasks: {
        task: "Some Sub Task",
        spentTime: 1,
        subTasks:{
            task:"Some sub sub task",
            spentTime:30
        }
    }
};