4
<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>

As long as the formatPhoneNumber(value) produces a different value, every thing works fine, but once the max length is reached (Since formatPhoneNumber('xx xx xx xx xx whatever') == 'xx xx xx xx xx'), the emitted value is the same as the current store one.

It is totally fine, except that as a consequence, state is not mutated and component is not re-rendered, hence formattedValue() is not called.

So I end up with xx xx xx xx xx in the store, but the input displays xx xx xx xx xx whatever as local input value varies from the store one.

How can I avoid this unexpected behavior? Moving formatPhoneNumber() to the store would not solve my issue since it would still prevent mutation, and only using formatPhoneNumber() in formattedValue() would make me end up with an un-formatted value in the store which is not what I want either.

How come Vue's input with dynamic value set still manages a local state?

Augustin Riedinger
  • 20,909
  • 29
  • 133
  • 206
  • calling `commit` forces a mutation on the observable field in the store. can you confirm that the event is firing? – Ohgodwhy Dec 10 '19 at 09:34
  • Yes, I do confirm. My action is `addCellPhoneNumber(state, value) { console.log('commit called') state.cellPhoneNumber = value },` and `commit` *is* called, even when formatting doesn't happen on the component get side. – Augustin Riedinger Dec 10 '19 at 16:37
  • You could 'reset' the store just before emitting the formatted version, `this.$emit('input', null)`. So two emits per update. Feels a bit hacky, there has to be another way. –  Feb 13 '20 at 20:04

3 Answers3

4

To achieve what you want (I think), you could change your formatValue method to

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

So that it sets the input to the formatted phone number value. One way or another you're going to be overriding what the input produces so you might as well do it on the input event.

Shoejep
  • 4,414
  • 4
  • 22
  • 26
2

I would use a v-model instead of a v-value since that would give me full control over what I want to display in the input field.

In this way, you can format the input value, and then set it back in the model. It would look something like this:

<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>

Here's a working example I created to test this.

tony19
  • 125,647
  • 18
  • 229
  • 307
Jair Reina
  • 2,606
  • 24
  • 19
  • This is a neat approach. I've never used v-model with a prop value without specifying a getter/setter. But do you know why it is, that if I add the line: --- propValue: {{value}} --- to the component, the prop value being passed in is never updated? – Air Feb 18 '20 at 16:31
  • It is not updated because the prop value is passed down from the parent to the component, if you change it in the parent you will see that it is updated in the component. It is not recommended to change the props passed to component directly from the component because they will be overridden when the parent component changes. – Jair Reina Feb 18 '20 at 16:38
  • Ahhh, that makes sense. Is there a benefit to having two separate values in the parent ( 'inputToTest' & 'returnedFromTest' ) instead of just modifying the one value? inputToTest= val"/> {{inputToTest}} – Air Feb 18 '20 at 16:46
  • 1
    Not really, I used two separate values in the parent just to better explain what was going on. You should be good to use one only. – Jair Reina Feb 18 '20 at 18:31
0

I think the easiest approach is a simple one-line modification to the parent's @input event, that clears the prop value before it updates it.

You still only need to emit the one value, but before working with the emitted value, clear the prop.


I've provided a snippet below (but note the additional differences in the snippet):

Instead of specifying the input field value, I opted to use v-model to bind it to a computed property that has a get and set method. This allowed me to use different logic when accessing vs modifying the data (quite handy in many situations).

By separating this logic, I was able to move the functionality from inside the input event to the set method, and eliminate the input event entirely.

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>
Air
  • 498
  • 1
  • 4
  • 18