I'm trying to figure out how keydown and change events work together in Vue.js (Vue 3). The setup is pretty simple. I have a text input in a component, and I want to be able to enter a number in the text input field. From there, I want to be able to use the arrow keys (up and down) to increment or decrement the value in the input. When the input loses focus, it should trigger #change and then update the model.
This issue I'm having is that when I update the value in the input using #keydown, it doesn't behave the same way that a keypress for a printable character would. Updating the value on #keydown doesn't cause the #change event to be fired when the control loses focus.
I set up an example in the playground if that helps, but here's the code:
App.vue
<script>
import NumberInput from './NumberInput.vue'
export default {
components: {
NumberInput
},
data() {
return {
number: 100
}
}
}
</script>
<template>
<p>
Model should only be updated when focus leaves the text box.
</p>
<p>
This works fine when you type in a number and focus off the text box.
</p>
<p>
However, when you <b>only</b> use the arrow keys to modify a value, the `#change` event doesn't fire.
</p>
<br />
<div style="padding:10px; border:1px solid #aaaaaa">
<NumberInput v-model="number" />
Number: {{ number }}
</div>
</template>
NumberInput.vue
<script>
export default {
props: ['modelValue'],
data() {
return {
number: this.modelValue
}
},
methods: {
incrementOrDecrementNumber: function(e) {
const isUpKey = (e.key === "ArrowUp");
const isDownKey = (e.key === "ArrowDown");
let x = parseInt(this.number, 10);
if (x == NaN)
x = 0;
if (isUpKey || isDownKey) {
if (isUpKey)
x++;
else
x--;
this.number = x;
e.preventDefault();
}
},
numberChanged: function(e) {
this.$emit("update:modelValue", this.number);
}
}
}
</script>
<template>
<div>
Enter a number (press up or down to modify): <input type="text" v-model="number" #change="numberChanged($event);" #keydown="incrementOrDecrementNumber($event);" />
</div>
</template>
Is it possible to update this.number in the component and also have it fire a #change event when the input loses focus?
Thanks!
The problem that you're running into is not due to "how keydown and change events work together in Vue.js", and actually has nothing to do with Vue.js per se, but rather is a basic underlying issue with HTML and JavaScript. A change event will be triggered in an input field if the user changes the data of an input field by direct interaction with the field in the web page as this will set the element's "dirty" flag to true.
In your code, while the user's key press actions are triggering the changes, the input value is being changed internally, via JavaScript, and this will not trigger a change event, whether it is being changed through Vue.js or via plain vanilla JavaScript.
To trigger the change event, you can change the input value by a specific means, such as by using setAttribute(value,...) on the input element as per this answer or by directly calling a change event.
In your own code, #blur="numberChanged($event)" could work as noted in my comments:
<input
type="text"
v-model="number"
#change="numberChanged($event)"
#blur="numberChanged($event)"
/>
or you could make the input's value your component's "model" as per Vue.js documentation. I would also make the input type a number, so as to give the input up-down arrow key responsiveness natively:
<script>
export default {
props: ["modelValue"],
emits: ["update:modelValue"],
},
};
</script>
<template>
<div>
<p>
Enter a number (press up or down to modify):
<input
type="number"
:value="modelValue"
#input="$emit('update:modelValue', $event.target.value)"
/>
</p>
</div>
</template>
Related
I have two inputs that use the same function to set a new value like this
<b-input
v-model="ownerProps.approver2ExtraCost"
#blur="onClick($event)"
class="inputBuefy"
></b-input>
</div>
<b-input
class="inputBuefy"
#blur="onClick($event)"
v-model="ownerProps.approver3ExtraCost"
></b-input>
</div>
they have different v-models but I'm using the same function to change their values on my methods property.
onClick(e) {
console.log(e.target.value);
e.target.value = "dfg";
}
The thing is, when I change one of the following inputs it gets the previously value that I typed.
for example:
if typed 'ABC' in the first input
replaces it with 'dfg'
But when I go to the next input, and I type something like 'HIJ'
the first input get its previous value = 'ABC'
But it should have remained with 'dfg'
Your problem is that you are setting the value of the element and not changing the value of the bounded variable which you should do.
so when vue "refreshes" it updates the input
do this instead
<b-input
v-model="ownerProps.approver2ExtraCost"
#blur="onClick(2)"
class="inputBuefy"
></b-input>
</div>
<b-input
class="inputBuefy"
#blur="onClick(3)"
v-model="ownerProps.approver3ExtraCost"
></b-input>
</div>
and change your function to
onClick(id) {
if (id === 2) ownerProps.approver2ExtraCost = "dfg";
if (id === 3) ownerProps.approver3ExtraCost = "dfg";
}
you should ideally have those elements in a array instead and send the index of what you want to change
Given the following code below where <v-input> is a custom input element,
<template>
<v-input id="username" type="text" v-model="username" />
</template>
<script>
export default {
data() {
return {
username: "",
};
},
};
</script>
How would one go about modifying the value of the input element through the browser console? I have tried the following code below but it does not bind to the Vue component.
const element = document.querySelector("#username");
element.value = "foobar"
I've also tried emitting custom events but those don't seem to work for me either. Any advice regarding this would be much appreciated.
I figured it out, I need only dispatch a new input event so that Vue.js catches the value. See below for an example.
const inputBox = document.querySelector("#inputBox");
inputBox.value = "hello";
inputBox.dispatchEvent(new Event('input'));
In a textarea, i want to customize the line break action. the default behavior is defined by click on enter, i want to program the line break action to be done when i click on Alt+Enter.
you can use (keydown.alt.enter) and (keydown.enter) events. If your textarea is using [(ngModel)] you can has some like
<textarea #textArea #textAreaModel="ngModel"
[(ngModel)]="value"
(keydown.alt.enter)="altEnter(textArea,textAreaModel)"
(keydown.enter)="$event.preventDefault();enter()">
</textarea>
See that you pass to the function "altEnter" the "textArea" (the HTMLElement) and the "textAreaModel" (the "model")
If you use ReactiveForms you can has some like
<textarea #textArea [formControl]="control"
(keydown.alt.enter)="altEnter(textArea,control)"
(keydown.enter)="$event.preventDefault();enter()">
</textarea>
See that in this case you pass to the function the "textArea" (the HTMLElement) and the formControl "control"
Your functions are like
enter(){
console.log("enter")
}
altEnter(element:any,control:any){
const start=element.selectionStart; //get teh Selection start and end
const end=element.selectionEnd;
let value:string[]=element.value.split('')
value.splice(start,end-start,'\n')
if (control.control)
control.control.setValue(value.join('')) //give value to the ngModel
else
control.setValue(value.join('')) //give value to the formControl
setTimeout(()=>{
element.selectionStart=element.selectionEnd=start+1 //position the cursor
})
}
You can see both cases in this stackblitz
I have a React (15.3.2) component with a text input.
(Everywhere I say "render" here it's actually render or unhide; I've tried both.)
When that input element is blurred, I render a new component with a text input.
I want give the new text input focus.
I've tried componentDidMount, componentWillUpdate, and componentDidUpdate; I've tried named and function refs; I've tried react-dom.
The focusing itself works, e.g., once it's been rendered, if I click in the initial input, focus goes to the new input (this is a bug, but compared to focusing, trivial).
The first input has an onBlur that sets the state used to tell the second input to render or not.
In that blur handler I stop the event as best as I can.
When I tab out of the first element I'm already "past" the newly-rendered element, e.g., the browser tab bar in my current bare design–I guess the new element hasn't been rendered yet?
class SecondInput extends Component {
componentDidUpdate = (prevProps, prevState) => {
if (!this.props.hidden) this._input.focus()
}
render = () =>
<input type="text" hidden={this.props.hidden} ref={(c) => this._input = c}
}
class NewItem extends Component {
state = { itemEntered: false }
itemBlurred = (e) => {
e.preventDefault()
e.stopPropagation()
this.setState({ itemEntered: true })
}
render = () =>
<div>
Item: <input type="text" onBlur={this.itemBlurred} />
<SecondInput hidden={!this.state.itemEntered} />
</div>
}
Any ideas or hints? I have to believe it's something obvious, because surely this happens all the time.
I'm also open to any other form of component hierarchy, e.g., if I need to have a container that wraps all this stuff up somehow that's fine.
React 15.3.2
The problem you are seeing appears to be because there are no more focusable elements on the page when you press tab, so the focus goes to the address bar. For some reason when the focus is on the address bar, just calling this._input.focus() does not grab focus as you would expect.
In order to combat this problem, I have added an empty div, and set the tabIndex property based on whether or not the second input is shown.
Just to make things easier for myself, I made the input focus on mount instead of using the hidden property. This may or may not work in your case, but it seemed to be cleaner since it would keep the input from calling focus on every keypress if it were to be a controlled input.
let Component = React.Component;
class SecondInput extends Component {
componentDidMount(){
this.textInput.focus()
}
render(){
return (
<input type="text" ref={(input) => this.textInput = input} />
)
}
}
class NewItem extends Component {
state = { itemEntered: false }
itemBlurred = (e) => {
//e.preventDefault()
//e.stopPropagation()
this.setState({ itemEntered: true })
}
render = () =>
<div>
Item: <input type="text" onBlur={this.itemBlurred} />
{
this.state.itemEntered ? [
<SecondInput/>
] : []
}
<div tabIndex={this.state.itemEntered ? null : 0}/>
</div>
}
ReactDOM.render(<NewItem />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.0/react-dom.min.js"></script>
In the spec for my app it says (developerified translation): When tabbing to a time element, it should update with the current time before you can change it.
So I have:
<input type="time" ref="myTimeEl" onFocus={this.handleTimeFocus.bind(null, 'myTimeEl')} name="myTimeEl" value={this.model.myTimeEl} id="myTimeEl" onChange={this.changes} />
Also relevant
changes(evt) {
let ch = {};
ch[evt.target.name] = evt.target.value;
this.model.set(ch);
},
handleTimeFocus(elName, event)
{
if (this.model[elName].length === 0) {
let set = {};
set[elName] = moment().format('HH:mm');
this.model.set(set);
}
},
The component will update when the model changes. This works well, except that the input loses focus when tabbing to it (because it gets rerendered).
Please note, if I would use an input type="text" this works out of the box. However I MUST use type="time".
So far I have tried a number of tricks trying to focus back on the element after the re-render but nothing seems to work.
I'm on react 0.14.6
Please help.
For this to work, you would need to:
Add a focusedElement parameter to the components state
In getInitialState(): set this parameter to null
In handleTimeFocus(): set focusElement to 'timeElem` or similar
Add a componentDidUpdate() lifecycle method, where you check if state has focusedElement set, and if so, focus the element - by applying a standard javascript focus() command.
That way, whenever your component updates (this is not needed in initial render), react checks if the element needs focus (by checking state), and if so, gives the element focus.
A solution for savages, but I would rather not
handleTimeFocus(elName, event)
{
if (this.model[elName].length === 0) {
let set = {};
set[elName] = moment().format('HH:mm');
this.model.set(set);
this.forceUpdate(function(){
event.target.select();
});
}
},
try using autoFocus attrribute.
follow the first 3 steps mention by wintvelt.
then in render function check if the element was focused, based on that set the autoFocus attribute to true or false.
example:
render(){
var isTimeFocused = this.state.focusedElement === 'timeElem' ? true : false;
return(
<input type="time" ref="myTimeEl" onFocus={this.handleTimeFocus.bind(null, 'myTimeEl')} name="myTimeEl" value={this.model.myTimeEl} id="myTimeEl" onChange={this.changes} autoFocus={isTimeFocused} />
);
}