Vue + Vuex: входное значение не установлено магазином, если магазин не изменился

<template>
  <input
    @input="formatValue"
    type="text"
    :value="formattedValue"
  />
</template>

<script type="text/javascript">
  import {formatPhoneNumber} from '~/utils/string';

  export default {
    computed: {
      formattedValue: function(){
        return formatPhoneNumber(this.value)
      },
    },
    methods: {
      formatValue(e) {
        this.$emit('input', formatPhoneNumber(e.target.value))
      }
    },
    props: ['value']
  }
</script>

Пока formatPhoneNumber(value) выдает другое значение, все работает нормально, но как только достигается максимальная длина (начиная с formatPhoneNumber('xx xx xx xx xx whatever') == 'xx xx xx xx xx'), выдаваемое значение совпадает с текущим хранилищем.

Это совершенно нормально, за исключением того, что, как следствие, состояние не изменяется и компонент не перерисовывается, поэтому formattedValue() не вызывается.

Таким образом, я получаю xx xx xx xx xx в магазине, но на входе отображается xx xx xx xx xx whatever, поскольку локальное значение ввода отличается от значения в магазине.

Как я могу избежать этого неожиданного поведения? Перемещение formatPhoneNumber() в хранилище не решило бы мою проблему, поскольку оно по-прежнему предотвратило бы мутацию, и только использование formatPhoneNumber() в formattedValue() привело бы к тому, что я получил бы неформатированное значение в хранилище, что мне тоже не нужно.

Почему Vue input с динамическим набором value все еще управляет локальным состоянием?


person Augustin Riedinger    schedule 10.12.2019    source источник
comment
вызов commit вызывает мутацию наблюдаемого поля в хранилище. Вы можете подтвердить, что событие срабатывает?   -  person Ohgodwhy    schedule 10.12.2019
comment
Да, подтверждаю. Мое действие addCellPhoneNumber(state, value) { console.log('commit called') state.cellPhoneNumber = value }, и commit вызывается, даже если форматирование не происходит на стороне получения компонента.   -  person Augustin Riedinger    schedule 10.12.2019
comment
Вы можете «сбросить» хранилище непосредственно перед отправкой отформатированной версии this.$emit('input', null). Таким образом, два излучения за обновление. Чувствуется немного взломано, должен быть другой способ.   -  person    schedule 13.02.2020


Ответы (3)


Чтобы достичь того, чего вы хотите (я думаю), вы можете изменить свой метод formatValue на

formatValue(e) {
   this.$emit('input', e.target.value = formatPhoneNumber(e.target.value));
}

Так что он устанавливает ввод в форматированное значение номера телефона. Так или иначе, вы будете переопределять то, что производит ввод, поэтому вы можете сделать это и в событии ввода.

person Shoejep    schedule 13.02.2020

Я бы использовал v-модель вместо v-значения, поскольку это дало бы мне полный контроль над тем, что я хочу отобразить в поле ввода.

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

<template>
  <input @input="formatValue" type="text" v-model="inputModel">
</template>

<script type="text/javascript">    
export default {
  data() {
    return {
      inputModel: this.value
    };
  },
  methods: {
    formatValue() {
      this.inputModel = formatPhoneNumber(this.inputModel);
      this.$emit("input", this.inputModel);
    }
  },
  props: ["value"]
};
</script>

Вот рабочий пример, который я создал для проверки.

person Jair Reina    schedule 18.02.2020
comment
Это аккуратный подход. Я никогда не использовал v-модель со значением реквизита без указания геттера/сеттера. Но знаете ли вы, почему если я добавлю строку: --- propValue: {{value}} --- в компонент, передаваемое значение свойства никогда не обновляется? - person Air; 18.02.2020
comment
Он не обновляется, потому что значение реквизита передается от родителя к компоненту, если вы измените его в родительском элементе, вы увидите, что оно обновлено в компоненте. Не рекомендуется изменять реквизиты, переданные компоненту непосредственно из компонента, поскольку они будут переопределены при изменении родительского компонента. - person Jair Reina; 18.02.2020
comment
Аааа, это имеет смысл. Есть ли преимущество в том, чтобы иметь два отдельных значения в родительском элементе («inputToTest» и «returnedFromTest») вместо простого изменения одного значения? ‹test :value=inputToTest @input=val =› inputToTest= val/› {{inputToTest}} - person Air; 18.02.2020
comment
Не совсем, я использовал два отдельных значения в родительском элементе, чтобы лучше объяснить, что происходит. Вы должны быть хороши, чтобы использовать только один. - person Jair Reina; 18.02.2020

Я думаю, что самый простой подход — это простая однострочная модификация родительского события @input, которая очищает значение свойства перед его обновлением.

Вам по-прежнему нужно выдать только одно значение, но прежде чем работать с выданным значением, очистите свойство.


Я предоставил фрагмент ниже (но обратите внимание на дополнительные различия во фрагменте):

Вместо указания значения поля ввода я решил использовать v-model для привязки его к вычисляемому свойству, имеющему метод get и set. Это позволило мне использовать другую логику при доступе и изменении данных (довольно удобно во многих ситуациях).

Разделив эту логику, я смог переместить функциональность из события ввода в метод set и полностью исключить событие ввода.

new Vue({
  el: "#app",
  // props: ['valueProp'],  
  data: {
    valueProp: "" //simulate prop data
  },
  
  computed: {    
    // --Value input element is binded to--
    inputValue:{
      get(){ //when getting the value, return the prop
        return this.valueProp;
      },
      set(val){ //when the value is set, emit value
        this.formatValue(val);
      }
    }
  },

  methods: {

    // --Emit the value to the parent--
    formatValue(val) {      
      this.parentFunction(this.formatPhoneNumber(val)); //simulate emitting the value
      // this.$emit('input', formatPhoneNumber(val));
    },

    // --Simulate parent receiving emit event--
    parentFunction(emittedValue){
      console.log("emitted:" + emittedValue);
      this.valueProp = null;         //first clear it (updates the input field)
      this.valueProp = emittedValue; //then assign it the emitted value
    },

    // --Simulate your format method--
    // THIS LOGIC CAN BE IGNORED. It is just a quick implementation of a naive formatter.
    // The "important" thing is it limits the length, to demonstrate exceeding the limit doesn't get reflected in the input field
    formatPhoneNumber(val){
      var phoneSpaces = [2,4,6,8];  //specify space formatting (space locations)
      var maxLength = 10;           //specify the max length
      val = val.replace(/ /g,'');   //remove existing formatting
      if(val.length > maxLength)    //limits the length to the max length
        val = val.substring(0, maxLength);

      // for the number of desired spaces, check each space location (working backwards) ... if value is longer than space location and space location is not a space ... add a space at the location.
      for(var i = phoneSpaces.length-1; i >= 0; i--){
        if(val.length > phoneSpaces[i] && val[phoneSpaces[i]] != " "){
          val = val.substring(0, phoneSpaces[i]) + " " + val.substring(phoneSpaces[i], val.length);
        }
      }
      return val
    }
  
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

<div id="app">
  <input type="text" v-model="inputValue"/>

  <label style="float: right;">
    Prop Value: <span>{{valueProp}}</span>
  </label>

  <br>

  <label >format (xx xx xx xx xx)</label>
</div>

person Air    schedule 18.02.2020