skip to Main Content

I am trying to create two-way binding but the problem is that I need to know what exactly changes the model ref: parent or child component.

When using watch it simply catches all changes and I can’t find the way to distinguish the source of change.

<script setup>
// Parent.vue

const doc = ref('');

setTimeout(() => {
    // Update this ref AND trigger watch in Child.vue
    doc.value = 'new value from parent';
}, 2000);
</script>

<template>
    <Child v-model:doc="doc" />
</template>
<script setup>
// Child.vue

const doc = defineModel('doc');

setTimeout(() => {
    // DO NOT TRIGGER WATCH ON THIS ONE!
    // But still update parent `doc` ref
    doc.value = 'new inner value';
}, 3000);

watch(doc, newValue => {
    // Catch `new value from parent` but not `new inner value`
});
</script>

2

Answers


  1. you should introduce the flag to track the source of the change

    <template>
      <Child v-model:doc="doc" />
    </template>
    
    <script setup>
    import { ref } from 'vue';
    
    const doc = ref('');
    
    setTimeout(() => {
      doc.value = 'new value from parent';
    }, 2000);
    </script>
    

    and here is the child.vue

    <script setup>
    import { defineModel, watch, ref } from 'vue';
    
    const doc = defineModel('doc');
    const changeSource = ref('');
    
    watch(doc, (newValue, oldValue) => {
      if (newValue !== oldValue && changeSource.value === 'parent') {
        console.log('Change from parent:', newValue);
      } else if (newValue !== oldValue && changeSource.value === 'child') {
        console.log('Change from child:', newValue);
      }
    });
    </script>
    
    Login or Signup to reply.
  2. Use a flag in the child:

    See on Vue SFC Playground

    <script setup>
    import {watch} from 'vue';
    const doc = defineModel('doc');
    let meChangingDoc = false;
    
    setTimeout(() => {
        // DO NOT TRIGGER WATCH ON THIS ONE!
        // But still update parent `doc` ref
        meChangingDoc = true;
        doc.value = 'new inner value';
    }, 3000);
    
    watch(doc, newValue => {
      if(meChangingDoc) {
        meChangingDoc = false; 
        console.log('child changed doc');
        return;
      }
      console.log('parent changed doc')
      
        // Catch `new value from parent` but not `new inner value`
    });
    </script>
    

    You can also use a generic function:

    See on Vue SFC Playground

    import {watch} from 'vue';
    export default function syncRef(source){
      let syncing = false;
      return new Proxy(source, {
        get(_, prop){
          if(prop === 'watchSource') return function(cb){
            return watch(source, (...args) => {
              if(syncing) {syncing = false; return; }
              cb(...args);
            });
          };
          return Reflect.get(...arguments);
        }, 
        set(_, prop, val){
          if(prop === 'value'){
            source.value = val;
            return true;
          }
          return Reflect.set(...arguments);
        }
      });
    }
    

    Usage:

    <script setup>
    import syncRef from './SyncRef';
    const model = defineModel('doc');
    const doc = syncRef(model);
    
    setTimeout(() => {
        // DO NOT TRIGGER WATCH ON THIS ONE!
        // But still update parent `doc` ref
        doc.value = 'new inner value';
    }, 3000);
    
    doc.watchSource(newValue => {
      console.log('parent changed doc')  
        // Catch `new value from parent` but not `new inner value`
    });
    </script>
    
    <template>{{ doc }}</template>
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search