Skip to content

With v-model.trim on custom component, trimmed value does not flow back to the component #5847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ThomHurks opened this issue Jun 8, 2017 · 13 comments

Comments

@ThomHurks
Copy link

Version

2.3.4

Reproduction link

https://jsfiddle.net/p8dvkqmb/32/

Steps to reproduce

Originally, I had a textarea with v-model.trim on it. This worked great! However, I added extra fancy controls to the textarea and decided to make a custom component so I can reuse and share it easily.

After this, I noticed two odd behaviours:

  • As the title says, if you type "a" and unfocus/focus the textarea in the custom component, the whitespace is still there, it's not trimmed. With my old plain textarea, an unfocus/focus clears the extreaneous whitespace.
  • In the parent component, the variable that holds your text is trimmed correctly. So it seems that v-model.trim on a custom component does trim the value that comes with the "input" event from within the component, but the trimmed value does not flow back to the child component through the value prop.
  • Another strange behaviour is that when you type "ab" and remove the "b", the extra whitespace is instantly trimmed. This can be annoying when typing and using backspace when you make a typo. The "plain" textarea does not have this behaviour when using v-model.trim on it.

What is expected?

When implementing a custom component containing a textarea that supports v-model as the docs describe and using v-model.trim on it the behaviour of the textarea should match that of using v-model.trim on a plain textarea.

What is actually happening?

The textarea within the custom component has different behaviour than a plain textarea. Specifically, the trimmed value is seen by the parent component that uses v-model.trim on the child component, but the trimmed value does not flow back to the child component. You can see that after unfocus/focus the whitespace is still there, while with a plain textarea it will be trimmed.

Additionally, when you type "ab" and remove the "b", the remaining whitespace is instantly removed, which is annoying behaviour when typing and using backspace to correct a typo for example, while the original plain textarea does not have this behaviour.

@yyx990803
Copy link
Member

This is indeed some special handling that is only present in native v-model.

The basic idea is that when you type the whitespace, the parent value gets updated to the trimmed value. This triggers an update from parent to child. However because the textarea in the child is still in focus, the update is blocked. Then when you blur the input, no update happens because the parent value is already trimmed so the system thinks nothing has changed.

You can easily simulate the native behavior with an extra @blur="$forceUpdate()" on the textarea in your custom component.

@ThomHurks
Copy link
Author

@yyx990803 Thanks for your answer! The @blur="$forceUpdate()" does indeed fix my issue of the whitespace still showing in the custom component. The part where "a(some whitespace here)b" -> remove "b" -> instantly trims all whitespace so only "a" remains happens is still there, but I can live with that.

@tegola
Copy link

tegola commented Mar 2, 2018

@ThomHurks did you find a solution for the backspace issue?

@ThomHurks
Copy link
Author

@tegola I did not, the quirk is there in my application to this day and no one has noticed. I think users don't often make a typo directly after a space, but more often make a typo in the middle of a word. If you mistype the first letter of a word (after a space) and then delete it, the space before it will be deleted too. I think you can still fix it, and I might try this myself, by just not using v-model on such components but by "rolling your own" and making it a prop that syncs with some custom code for trimming it before sending out the input event.

@tegola
Copy link

tegola commented Mar 2, 2018

@ThomHurks unfortunately one of our users noticed, that's why I'm looking for a fix.

Actually, my component doesn't event use v-model.trim. I wanted every textbox to be trimmed without specifying it in every binding, so I added a trim prop to the component.

Here's a stripped down version:

<template>
  <input type="text" :value="value" @input="onInput" @change="onChange">
</template>

<script>
export default {
  props: {
    value: [String, Number],
    trim: {
      type: Boolean,
      default: true
    }
  },

  methods: {
    onInput(event) {
      const value = event.target.value;

      this.$emit('input', this.trim ? value.trim() : value);
    },

    onChange(event) {
      const value = event.target.value;

      this.$emit('change', this.trim ? value.trim() : value);
    }
  }
}
</script>

Its behavior, though, looks identical to v-model.trim: backspace-ing the last char also removes any space before it.

@yyx990803 I know this is an old thread, but maybe there's something we're missing here?

@ThomHurks
Copy link
Author

Yeah, it's a bit weird isn't it. As @yyx990803 says:

However because the textarea in the child is still in focus, the update is blocked.

But this doesn't seem to be the case when deleting characters; when you delete a character and some whitespace was present before the character, then the whitespace is trimmed immediately in the input control even if the input is still in focus. I don't completely understand the mechanics of that.

Anyway, what does seem to solve it is don't update on the input event but only on the change event. I'm still using v-model, so in my component I now have this:

model: {
            prop: 'myCustomValueName',
            event: 'change'
        }

and

:value="myCustomValueName" v-on:change="$emit('change', $event.target.value)" v-on:blur="$forceUpdate()"

and then on the parent I use:

v-model.trim="someText"

and then I have the following behaviour:

  • The value in the parent is always trimmed
  • The value in the child is trimmed after blur.
  • When deleting a character, any preceding whitespace is preserved because it doesn't send an event to the parent immediately.

However, the value in the parent does lag behind, so if you display the value somewhere else too there's no 1:1 correspondence immediately with what you're typing. In my case that's no objection though.

@tegola does that solution work for you too?

@tegola
Copy link

tegola commented Mar 5, 2018

@ThomHurks changing v-model configuration does indeed help:

model: {
  event: 'change'
}

I would have loved to keep the input event at least with trim: false, so with something like this:

model: {
  event: this.trim ? 'change' : 'input'
}

Alas, this doesn't work. I'll keep just the change event, but the fact that normal inputs handle backspaces just fine bothers me.

Thanks for your help!

@afwn90cj93201nixr2e1re
Copy link

afwn90cj93201nixr2e1re commented Nov 22, 2019

@tegola @ThomHurks so, i have done it with @input, with replicate v-model code, but only for input tag(изображение),
so, if you wanna create another one you should replicate another models stuff.

Check gifs:
Instant/Delayed(default) value updates:
GIF

Instant/Delayed(default) value updates with local computed setter condition:
GIF

With numbers:

GIF
GIF

Src: https://gist.github.com/afwn90cj93201nixr2e1re/205ad636556c3af9d5009bf1753d6778

@afwn90cj93201nixr2e1re
Copy link

Also as you can see i go away on $forceUpdate calling. So, everything works fine, that's the first right v-model based custom component for inputs,

If someone can, create some article and describe this code, so, for future users and frameworks owners, coz now every article writing only about watch/props/model:{} but no one said about how we should do it right.

@m4heshd
Copy link

m4heshd commented Jun 10, 2021

Is there a way to use forceUpdate() in composition API?

@lobo-tuerto
Copy link

@m4heshd Yes.

<script setup lang="ts">
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()

instance?.proxy?.$forceUpdate()
</script>

@m4heshd
Copy link

m4heshd commented Oct 7, 2022

@lobo-tuerto oh gosh. A year later 😁. Yes I figured that out by just printing out the current instant. But as I remember, dev team said not to rely on that and to never use getCurrentInstance() in production other than for testing.

@lobo-tuerto
Copy link

@m4heshd Yeah, you should rely mostly on :key attributes if possible. Or invalidating a computed property I guess.

Here is a nice article about forcing Vue to re-render: https://michaelnthiessen.com/force-re-render/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants