Vue.js - Как вернуть данные в родительский компонент?

119
07 января 2021, 01:10

У меня есть родительский компонент куда в data() я хочу передать вычисляемое свойство из дочерного компонента.

// ParentComponent.vue
<template>
  <div>
    <ChildComponent :parent="parent"/>
  </div>
</template>
<script>
import ChildComponent from '@/components/ChildComponent';
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent,
  },
  data() {
    return {
      parent: {
        parentData: '',
      }
    };
  },
</script>

В дочернем компоненте вычисляемое свойство это конкатенация строк, полей свойства data дочерного компонента.

<template>
  <div>
    <div class="control">
      <input
        v-model="salarySumm"
        class="input"
        type="text"
        placeholder="Add the estimated salary (1000 - 2000)"
      >
      <div class="select">
        <select v-model="salaryCode">
          <option disabled value>Select currency</option>
          <option
            v-for="(el, index) in currency"
            :key="index"
            :value="el.code"
          >{{ el.title }}</option>
        </select>
      </div>
  </div>
</template>
<script>  
export default {
  name: 'ChildComponent',
  props: ['parent'],
  data() {
    return {
      salarySumm: '',
      salaryCode: '',
    }
  },
  created() {
    // Тут я получаю массив объектов в виде 
    // [{ title: 'string', code: 'string' }]
    this.$store.dispatch('getCurrencies')
  },
  computed: {
    currency() {
      let list = this.$store.getters.currencies.data
      const currencyList = []
      if (list !== undefined) {
        for (const el of list) {
          if (el.code == '(none)' || el.code == null) el.code = ''
          if (el.name !== null) {
            currencyList.push({ title: `${el.name} | ${el.code}`, code: el.code })
          }
        }
      }
      return currencyList
    },
    salary() {
      // Здесь я получаю строку в том виде, в котором хочу передать ее в родительский компонент.
      return this.salarySumm + ' ' + this.salaryCode
    },
</script>

Как мне передать вычисляемое свойство salary в родительский компонент, а точнее в parent.parentData?

Answer 1

В дочерние компоненты вы передаёте свойства, а из дочерних в родительский вызываете события $emit по изменению свойств:

// Родительский компонент.
<template>
  <child-component :someProps="parent" @updateParent="onUpdateSalary" />
</template>
<script>
  export default {
    // Локальные данные компонента.
    data() {
      return {
        parent: {
          parentData: '',
        }
      }
    },
    methods: {
      onUpdateSalary(someData) {
        // Выполняем необходимые действия с `someData`
      }
    }
  }
</script>
// Дочерний компонент.
<template>
  <button type="button" @click="doSomething">Передать родителю</button>
</template>
<script>
  export default {
    // Входящие свойства из родителя.
    props: ['someProps'],
    // Локальные данные компонента.
    data() {
      return {
        amount: '',
        code: '',
      }
    },
    methods: {
      doSomething() {
        this.$emit('updateParent', {
          amount: this.amount,
          code: this.code
        })
      }
    }
  }
</script>

Если нужно передать вычисляемое свойство computed родительскому компоненту, то необходимо использовать наблюдатель watch, которому задаётся функция обработчик вызываемая при каждом изменении вычисляемого (и не только) свойства. Пример ниже.

// Отключим ненужные для примера 
// сообщения в консоли. 
Vue.config.productionTip = false 
Vue.config.devtools = false 
 
Vue.component('parent-component', { 
  template: ` 
        <div> 
            <child-component :someProps="parent" @updateParent="onUpdateSalary" /> 
            <p v-if="parentSalary"> 
                Значение <b>parentSalary</b>, отображаемое в "parent-component": <pre>{{ parentSalary }}</pre> 
            </p> 
        </div>`, 
  data() { 
    return { 
      // Родительское значение. 
      parentSalary: null, 
      parent: { 
        parentData: '... из поля data', 
      } 
    } 
  }, 
  methods: { 
    onUpdateSalary(data) { 
      this.parentSalary = data 
    } 
  } 
}) 
 
Vue.component('child-component', { 
  template: ` 
        <div> 
            <pre>Свойство из родительского компонента: {{ someProps.parentData }}</pre> 
            <div v-if="currencies.length" class="control"> 
                <input type="number" v-model.number="amount" class="input" /> 
                <select v-model="code"> 
                    <option disabled selected>Select currency</option> 
                    <template v-for="(currency, index) in currencies"> 
                        <option :key="index" :value="currency.code">{{ currency.name }}</option> 
                    </template> 
                </select> 
            </div> 
            <div v-else>Нет данных для отображения.</div> 
            <p v-if="salary">Значение <b>salary</b>, отображаемое в "child-component": <b>{{ salary }}</b></p> 
        </div>`, 
  props: ['someProps'], 
  data() { 
    return { 
      amount: '', 
      code: '', 
 
      currencies: [{ 
          "code": "EUR", 
          "name": "Euro" 
        }, 
        { 
          "code": "GBP", 
          "name": "U.K. Pound Sterling" 
        }, 
        { 
          "code": "JPY", 
          "name": "Japanese Yen" 
        } 
      ] 
    } 
  }, 
 
  computed: { 
    salary() { 
      return this.amount + ' ' + this.code 
    } 
  }, 
 
  watch: { 
    // Эта функция запускается при любом изменении значений 
    // в вычисляемом свойстве `salary`. 
    salary(newValue, oldValue) { 
      // Пробрасываем данные родительскому компоненту, 
      // ч/з вызов метода. 
      this.$emit('updateParent', { 
        amount: this.amount, 
        code: this.code 
      }) 
    } 
  } 
}) 
 
new Vue({ 
  el: '#app', 
  data: {} 
})
<div id="app"> 
  <div> 
    <parent-component></parent-component> 
  </div> 
</div> 
 
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10"></script>

  computed: {
    salary() {
      return this.amount + ' ' + this.code
    }
  },
  watch: {
    // Эта функция запускается при любом изменении значений
    // в вычисляемом свойстве `salary`.
    salary(newValue, oldValue) {
      // Пробрасываем данные родительскому компоненту,
      // ч/з вызов метода.
      this.$emit('updateParent', {
        amount: this.amount,
        code: this.code
      })
    }
  }

Судя по коду this.$store.dispatch вы используете vuex. В таком случае, можно всю логику перенести в хранилище. Это освободит от постоянных проверок полученных данных в каждом компоненте.

// Отключим ненужные для примера 
// сообщения в консоли. 
Vue.config.productionTip = false 
Vue.config.devtools = false 
 
// Список доступных мутаций (сеттеры). 
const CASH_INIT = 'CASH_INIT' 
const CASH_SET_AMOUNT = 'CASH_SET_AMOUNT' 
const CASH_SET_CODE = 'CASH_SET_CODE' 
 
const store = new Vuex.Store({ 
  state: { 
    amount: null, 
    code: null, 
    currencies: [] 
  }, 
 
  getters: { 
    amount: state => state.amount, 
    code: state => state.code, 
    currencies: state => state.currencies, 
    salary: state => { 
      if (state.amount && state.code) { 
        return state.amount + ' ' + state.code 
      } 
 
      return null 
    } 
  }, 
 
  // Обработчики мутаций обязаны быть синхронными. 
  mutations: { 
    [CASH_INIT](state, currencies) { 
      state.currencies = currencies 
    }, 
    [CASH_SET_AMOUNT](state, amount) { 
      amount = parseFloat(amount) 
 
      if (!isNaN(amount)) { 
        state.amount = amount 
      } else { 
        state.amount = '' 
        alert('Ошибка в типе данных: `amount`.') 
      } 
    }, 
    [CASH_SET_CODE](state, code) { 
      code = code.toString() 
 
      if (code) { 
        state.code = code 
      } else { 
        state.code = '' 
        alert('Ошибка в типе данных: `code`.') 
      } 
    } 
  }, 
 
  actions: { 
    getCurrencies(context) { 
      try { 
        // В своем проекте используйте `fetch` или axios, например. 
        // const response = await fetch('http://www.floatrates.com/daily/usd.json') 
        // const data = await response.json() 
        // context.commit('CASH_INIT', data) 
 
        context.commit('CASH_INIT', [{ 
          "code": "EUR", 
          "name": "Euro" 
        }, { 
          "code": "GBP", 
          "name": "U.K. Pound Sterling" 
        }, { 
          "code": "JPY", 
          "name": "Japanese Yen" 
        }, { 
          "code": "AUD", 
          "name": "Australian Dollar" 
        }, { 
          "code": "CAD", 
          "name": "Canadian Dollar" 
        }, { 
          "code": "CHF", 
          "name": "Swiss Franc" 
        }, { 
          "code": "UYU", 
          "name": "Uruguayan Peso" 
        }]) 
      } catch (e) { 
        console.log(e) 
      } 
    }, 
 
    calculate(context, { 
      amount, 
      code 
    }) { 
      context.commit('CASH_SET_AMOUNT', amount) 
      context.commit('CASH_SET_CODE', code) 
    }, 
  } 
}) 
 
Vue.component('child-component', { 
  template: ` 
<div> 
  <div v-if="currencies.length" class="control"> 
    <input type="number" v-model.number="amount" class="input" /> 
    <select v-model="code"> 
      <option disabled selected>Select currency</option> 
      <template v-for="(currency, index) in currencies"> 
      <option :key="index" :value="currency.code">{{ currency.name }}</option> 
      </template> 
    </select> 
    <button type="button" @click="send">Отправить данные во vuex</button> 
  </div> 
  <div v-else>Нет данных для отображения.</div> 
</div>`, 
  data() { 
    return { 
      amount: '', 
      code: '', 
    } 
  }, 
 
  computed: { 
    // При компонентном подходе ...mapGetters() 
    ...Vuex.mapGetters(['currencies']), 
  }, 
 
  mounted() { 
    this.getCurrencies() 
  }, 
 
  methods: { 
    // При компонентном подходе ...mapActions(). 
    // Теперь в компоненте доступны методы: 
    // this.getCurrencies() и this.calculate(...) 
    ...Vuex.mapActions(['getCurrencies', 'calculate']), 
 
    send() { 
      this.calculate({ 
        amount: this.amount, 
        code: this.code 
      }) 
    } 
  } 
}) 
 
Vue.component('parent-component', { 
  template: ` 
<div> 
  <child-component /> 
  <p v-if="salary">Значение <b>salary</b>, отображаемое в "parent-component": <b>{{ salary }}</b></p> 
</div>`, 
  data() { 
    return {} 
  }, 
 
  computed: { 
    ...Vuex.mapGetters([ 
      'salary', 
    ]), 
  }, 
}) 
 
new Vue({ 
  el: '#app', 
  store, 
  data: {}, 
 
  // Используя vuex получаем доступ к хранилище 
  // не только в дочерних и родительских компонентах. 
  computed: { 
    ...Vuex.mapGetters([ 
      'salary', 
    ]), 
  }, 
})
<div id="app"> 
  <div> 
    <p v-if="salary">Значение <b>salary</b> в корне приложения: <b>{{ salary }}</b></p> 
    <parent-component></parent-component> 
  </div> 
</div> 
 
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10"></script> 
<script src="https://unpkg.com/vuex@2.0.0"></script>

Посмотрите другие примеры в этих ответах 984023, 975447.

Answer 2

Немного разобравшись с темой хочу дать и свой ответ.

Первый вариант: Но этот вариант имеет один недостаток, если не отображать результат вычисляемого свойства в компоненте, то мы не будем "дергать" его и оно не будет вычисляться. Т.е. этот метод годится только если вы хотите в этом же методе и использовать вычисляемое свойство например так

<div>Salary: {{ salary }}</div>

Я покажу как можно прокинуть данные в родителя с помощью $emit но без якобы использования события. Все дело в том, что $emit и есть наше собственное событие. Вот полезная статья про Пользовательские события.

В дочернем компоненте, я использую computed потому что он всегда пересчитывает данные которые меняются и сам факт пересчета и есть в неком смысле событие. Computed кроме возврата значения может создает некие побочные эффекты, запись данных куда то в данном случает в событие input (см. computed ниже). Останется только прописать пропс в дочернем компоненте и именем value.

// ChildComponent.vue
<script>  
export default {
  name: 'ChildComponent',
  props: ['value'],
  data() {
    return {
      salarySumm: '',
      salaryCode: '',
    }
  },
  created() {
    this.$store.dispatch('getCurrencies')
  },
  computed: {
    currency() {
      let list = this.$store.getters.currencies.data
      const currencyList = []
      if (list !== undefined) {
        for (const el of list) {
          if (el.code == '(none)' || el.code == null) el.code = ''
          if (el.name !== null) {
            currencyList.push({ title: `${el.name} | ${el.code}`, code: el.code })
          }
        }
      }
      return currencyList
    },
    salary() {
      let salary = this.salarySumm + ' ' + this.salaryCode
      this.$emit('input', salary)
      return salary
    },
</script>

и далее в родителе получить эти данные через v-model

// ParentComponent.vue
<template>
  <div>
    <ChildComponent v-model="parent.parentData"/>
  </div>
</template>
<script>
import ChildComponent from '@/components/ChildComponent';
export default {
  name: 'ParentComponent',
  components: {
    ChildComponent,
  },
  data() {
    return {
      parent: {
        parentData: '',
      }
    };
  },
</script>

Второй вариант. Я использую внутренние события для того что бы вызвать метод который создать пользовательское событие $emit

// ParentComponent.vue
// Все то же самое что и в варианте 1
// ChildComponent.vue
// В шаблоне я явно привязываю событие к моему методу который склеит строки и создаст пользовательское событие. Этот метод в варианте 1 был вычисляемым свойством.
div class="control">
  <input
    v-model="salarySumm"
    class="input"
    type="text"
    placeholder="Add the estimated salary (1000 - 2000)"
    v-on:input="salary"
  >
  <div class="select">
    <select v-model="salaryCode" @change="salary()">
      <option disabled value>Select currency</option>
      <option
        v-for="(el, index) in currency"
        :key="index"
        :value="el.code"
      >{{ el.title }}</option>
    </select>
  </div>
</div>
// Вместо вычисляемого свойства computed я создаю метод
methods: {
  salary() {
    this.$emit('input', this.salarySumm + ' ' + this.salaryCode)
  },
}
READ ALSO
Табы с картинкой и ссылкой

Табы с картинкой и ссылкой

Помогите пожалуйста решить вопрос с табамиПри навешивание click на общий div в котором есть картинка и ссылка, переключение таба срабатывает...

110
Изменения значения select

Изменения значения select

Есть форма, в ней сотрудник вводит данные, в конце нажимает на кнопку и создаетсяhtm файл

106
Как добавить класс элементу Vue.js

Как добавить класс элементу Vue.js

я свсем новичек в vue, столкнулся с маленькой проблемой, буду рад любой помощиЯ использую библиотеку vue-scrollto, в документации нашел описаный...

174
как создать canvas с градиентом

как создать canvas с градиентом

Ка сделать так чтобы по ид создавались новые canvas в зависимости от текста то есть Внутри canvas прозрачный цвет только граница с градиентом нужна

136