VueJS: Why Trigger 'Input' Event Within 'Input' Event Handler?
Asked Answered
P

1

10

I'm learning VueJS. I'm figuring out their currency validation example code.

Vue.component('currency-input', {
  template: `
    <span>
      $
      <input
        ref="input"
        v-bind:value="value"
        v-on:input="updateValue($event.target.value)">
    </span>
  `,
  props: ['value'],
  methods: {
    // Instead of updating the value directly, this
    // method is used to format and place constraints
    // on the input's value
    updateValue: function (value) {
      var formattedValue = value
        // Remove whitespace on either side
        .trim()
        // Shorten to 2 decimal places
        .slice(
          0,
          value.indexOf('.') === -1
            ? value.length
            : value.indexOf('.') + 3
        )
      // If the value was not already normalized,
      // manually override it to conform
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // Emit the number value through the input event
      this.$emit('input', Number(formattedValue))
    }
  }
})

The $emit call at the bottom of the updateValue function, triggers an input event.

When I comment it out, the real time currency validation no longer works. So I realize it has a purpose.

But why trigger an input event inside an input event?

You'd think the input event would fire again, causing the updateValue handler to fire again, causing a stack overflow due to recursive calls.

I understand VueJS's much simpler $emit example code. It's just like Jquery's trigger function.

vm.$on('test', function (msg) {
  console.log(msg)
})
vm.$emit('test', 'hi')
// -> "hi"

But in the currency validation example, I do not understand why $emit is used the way it's used, and why it works the way it works.

Can somebody explain?

Pyknic answered 15/5, 2017 at 14:29 Comment(1)
The emit in this case is to communicate the event up to the parent scope.Anal
F
16

The Emit call here is to allow you to hook into the event in parent contexts. The Input event is also used by the v-model directive to handle two way binding with components.

v-model='model' is essentially v-bind:value='model' v-on:input='model = $event.target.value' with some added bits to make it play nice. When you remove the this.$emit('input', Number(formattedValue)) You're removing the mechanism that updates the value outside the component.

EDIT: @Jay careful what you wish for sometimes

All elements in HTML have a series of native handlers for the common events; resize, load, unload, etc. These handle what to do when the page changes it's rendering and can be disabled or added onto, since the introduction of JavaScript browsers have used an event pump system that allows multiple functions to be attached to any event which run in sequence when the event is raised. An example being how you can have 3 functions run on resize to handle edge cases such as minimum/maximum size, screen orientation etc.

Form elements generally implement their own base event functions: keydown, keyup, mousedown, mouseup. These base functions invoke events to make our lives easier as developers, these being: input, blur, focus. Some have specialized events as in select elements implementing change, form tags implementing submit.

Input tags on focus capture keyboard input and display the text input cursor to indicate that it's ready to receive input. It adds in handlers for the tab keycode which finds the next available input and shifts focus to that element. The event pump style function system is great here as it allows you to bind to focus and do things like change the background color or border when the input is focused without having to implement the code for capturing input or displaying the cursor yourself.

Input tags also raise the input event when you type in them indicating that the input has changed, telling the browser to change the value and update the display so that the functionality expected by the user is consistent.

In the currency-input example we are adding the updateValue function to work with the native function and process the input value of the event, in the updateValue function we modify the string representation of the value and need someplace to put it. You could simply add a data property to hold the value and bind the input's value property to the data property allowing the currency-input to internally handle the display of the result but that would lock the value behind a private accessor and you would be unable to modify or retrieve the value of the resulting currency formatted value.

Using this.$emit('input', Number(formattedValue)) the updateValue function is acting similar to the native input tag by raising an event that can be captured by the parent context and worked with. You can store it in a value, use it as the basis for a function, or even ignore it completely though that may not help much. This allows you to keep track of the value of the input and modify it as needed or send it to the server, display it, etc.

It also ties into a few directives most pertinently v-model which is syntactic sugar to allow for a value property binding and an input event binding to a data property inside the current context. By providing a value prop and emitting an input event a custom element can act similar to a native form element in the systems of a Vue application. An extremely attractive feature when you want to package and distribute or reuse components.

It's a lot nicer to go:

...
<currency-input v-model='dollarValue'></currency-input>
<input v-model='dollarValue'>
...

Than to have to add in value and input bindings everywhere ergo:

...
<currency-input v-bind:value='dollarValue' v-on:input='updateDollarValue($event.target.value)'></currency-input>
<input v-bind:value='dollarValue' v-on:input='updateDollarValue($event.target.value)'>
...

Now that my weird rambling is done, I hope this helped with understanding some of the patterns and reasoning behind the currency-input example.

Folderol answered 15/5, 2017 at 14:33 Comment(11)
So hooking up the updateValue handler prevents the input element's native input handling? But why? The code doesn't use the 'prevent' modifier.Pyknic
@Pyknic No it doesn't block the native handling but the native handling is just to update the displayed value. You're adding the handler alongside the native handling. Custom web components don't have native handling for any events, you have to implement that yourself.Folderol
If the native handling simply updates the displayed value and it isn't blocked, then why must there be an $emit statement to make it execute anyway? The documentation states that $emit triggers an event on 'this' instance. Where in the documentation does it explain how it works for triggering parent events?Pyknic
@Pyknic The native handling for the <input> is to display the updated value. The native handling for the input event on the <currency-input> is undefined. Since it's not defined you have to define when it's invoked and what happens when it is. this.$emit Invokes the event and allows it to be caught with an event listener. <currency-input v-on:input='handler'></currency-input> allows you to attach event handling to the component. If you'd like I can update my answer with more detailed information on the event pump but comments are too short to handle it.Folderol
Sure, edit your comment with all the details you can. I greatly appreciate the effort!Pyknic
Thanks for updating your post with loads more detail. I'll be sure to soak it all up!Pyknic
Dude, you wrote 'synaptic sugar'. :PPyknic
@Pyknic No problem. Lol I blame the speed I typed it. I'll fix that.Folderol
I have found a much clearer example of the $emit function's use. The counter example clearly shows the $emit function triggering an event in the parent context. My example has no such event in the parent context. You'd think the $emit in the currency validator would be useless. But it's not, because it makes 'NaN' appear for non digit characters. I'm learning more, but still asking myself what's going on with the currency validator's $emit. Why are they even using an emit? They could set NaN on the statement above it.Pyknic
I've also learned that if I comment out the $emit statement, the Vue context's price property is not 2-way-bound. The documentation states: "So for a component to work with v-model, it should (1) accept a value prop and (2) emit an input event with the new value." So the $emit function is for making v-model work. I'm learning at geometric rate here!Pyknic
@Pyknic Yeah, Like I mentioned v-model='data' is the similar as adding v-bind:value='data' v-on:input='data = $event.target.value' There is some optimization that happens behind the scenes but you'd get the results with those two directives instead of v-model.Folderol

© 2022 - 2024 — McMap. All rights reserved.