React Synthetic Events vs Native Javascript Events? - javascript

I have a hard time understanding react synthetic events.
The way I debug with native javascript events is something like this
window.addEventListener('mousemove',
function(event){
console.log(event);
})
If I move my mouse, and check the console, I can see the events properties such as target and other useful properties such as innerHTML, nextElementSibling, lastChild etc.
I have a simple react app below. Its an input field with a submit button. If something is entered in the input when submitted, an alert pops up. Otherwise nothing
// React Synthetic Events
class AddOption extends React.Component {
handleAddOption(e){
e.preventDefault();
console.log(e);
const option = e.target.elements.option.value; // < HOW WOULD I FIND THIS IN THE DEBUGGER?
if (option){
alert("something was entered in");
}
}
render() {
return (
<div>
<form onSubmit={this.handleAddOption}>
<input type="text" name="option" />
<button>React Submit</button>
</form>
</div>
)
}
}
ReactDOM.render(<AddOption />, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"><!-- reactapp -->
What I don't get is if I inspect the page it doesn't give that useful information like I get with native events.
The issue is I referenced in my reactapp (following a tutorial) to use e.target.element.options.value. I have no idea how someone would find that out in the debugger. I don't see a reference to anything dubbed element in any of the long nested chain of prototype properties under synthetic event's target. I tried CTRL+F but I don't think chrome supports searching nested prototypes
Am I not understanding something about things happening in the virtual DOM / react in general?

per li357 comment, adding e.persist() right before the console.log(e) statement in original post shows this in the debugger. You can see the native javascript event's properties include target, element and the defined option from react

You can use e.nativeEvent to get access to the native event and from there on to the elements.option.value property:
// React Synthetic Events
class AddOption extends React.Component {
handleAddOption(e){
e.preventDefault();
console.log(e);
const option = e.target.elements.option.value; // < HOW WOULD I FIND THIS IN THE DEBUGGER?
// > YOU CAN USE e.nativeEvent TO GET ACCESS TO THE NATIVE EVENT:
console.log(e.nativeEvent);
console.log(e.nativeEvent.target.elements.option.value); // < HERE IS YOUR VALUE
if (option){
alert("something was entered in");
}
}
render() {
return (
<div>
<form onSubmit={this.handleAddOption}>
<input type="text" name="option" defaultValue={"Toy"}/>
<button>React Submit</button>
</form>
</div>
)
}
}
ReactDOM.render(<AddOption />, document.getElementById('root'));

Related

Add event listener to element in React from external script

I am working on a React app, but I can't change the actual React code. The only thing I can do: I can add an external script to the footer of page. The script I want to insert into the app is the following:
$('input').keyup(function(event) {
// skip for arrow keys
if(event.which >= 37 && event.which <= 40) return;
// format number
$(this).val(function(index, value) {
return value
.replace(/\D/g, "")
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<p>Imagine a big react app with some input fields and the above script loaded in the footer using a script tag.</p>
<input />
It adds decimal separators to all input elements. I've taken it from https://codepen.io/kdivya/pen/oxVeWz. However, it doesn't work that way, because when the script is loaded, the DOM hasn't been constructed yet. Thus, according to the dev tools, the event listener isn't added to the input elements.
I researched a lot, but all related answers use some React lifecycle stuff, but afaik I can't utilize that from an external script. I also tried adding the event listener within $(document).ready(...), but it didn't work either. I'm open to all ideas (with or without JQuery, it can also use React I just don't know how to get into React structures from "outside").
The main issue is that event if you bound event handlers to the input elements, you have no idea when React will rerender, creating new input elements that don't have your event handlers bound to them. What I would do is use jQuery event delegation to bind to the body, but only listen for events originating from input elements:
$('body').on('keyup', 'input', function(event) {
// skip for arrow keys
if(event.which >= 37 && event.which <= 40) return;
// format number
$(this).val(function(index, value) {
return value
.replace(/\D/g, "")
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
});
});
// Example class component
class Thingy extends React.Component {
render() {
return (
<input />
);
}
}
// Render it
ReactDOM.render(
<Thingy />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"/>
But, you may need to manually trigger the change event or something, so that React will "know" that you changed the input's value.
Something like:
$('body').on('keyup', 'input', function(event) {
// ...
$(this).trigger('change');
});

Change focus to next input field on "Enter" key with VueJS & Quasar

I have a form that works strictly with a Barcode code that simulates an Enter event at the end of the read. (No keyboard and mouse). I'm having a hard time sending the focus to the next element (input sometimes a button). I prepared a playground for you so you can checkout my code. At some point this worked before a quasar dress-up and now it isn't. I refuse to think this is a quasar issue and more like a "I suck" problem lol.
The process is simple in theory. Wait for the input field to read the entire barcode before it fires the focus event. My best guess is to use the change event. When I tried the input or keydown event, it registered other stuff and fired other functions on every digit.. Big no-no, especially when making api calls.
Here is my sendFocus method.
sendFocus: function(e) {
document.addEventListener("keydown", function(e) {
var input = e.target.nodeName.toLowerCase() === "input";
var form = e.target.form;
if (e.key === "Enter" && input) {
var index = Array.prototype.indexOf.call(form, e.target);
form.elements[index + 1].focus();
}
});
}
And the link to the codepen. Thanks in advance
With #change (or native DOM onchange) event, any associated handler will get invoked as many times as you hit a key on the keyboard, and in your case, you are literally attaching a new handler every time a key is pressed (you seem to be good with Javascript and jQuery, so I hope the codepen was just for mere illustration... Unless I'm missing something?).
But anyway, in Vue (for this particular purpose), we usually want #keydown.enter. Have a read on Event Handling with Key Codes.
That being said, there are several approaches to achieving this "jump-to-next-field" behavior, but please consider the following example (which should also be applicable to Quasar's q-input).
new Vue({
el: '#app',
methods: {
focusNext(e) {
const inputs = Array.from(e.target.form.querySelectorAll('input[type="text"]'));
const index = inputs.indexOf(e.target);
if (index < inputs.length) {
inputs[index + 1].focus();
}
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11"></script>
<div id="app">
<form>
<div>
<label>Field #1</label>
<input type="text" #keydown.enter="focusNext" />
</div>
<div>
<label>Field #2</label>
<input type="text" #keydown.enter="focusNext" />
</div>
<div>
<label>Field #3</label>
<input type="text" />
</div>
</form>
</div>

Autofocus input on mount (Vue)- iOS

I would like to trigger the input box to focus (and thus keyboard to pop up) when a Vue component appears.
It does not to work on iOS.
I tried using Vue's example directive (here), and HTML5 autoFocus but neither worked.
I created this example in a sandbox (https://codesandbox.io/s/lw321wqkm).
FWIW, I do not believe it is a JS limitation, as I've seen example work (such as React Native Web using autoFocus- see example)
Parent component
<template>
<div>
<TextComp v-if='showText' />
<button #click='showText = !showText'> Show/hide input </button>
</div>
</template>
...
Child component
<template>
<div>
<input ref='focusMe' type='text'/>
</div>
</template>
<script>
export default {
name: 'TextComp',
mounted () {
this.$refs.focusMe.focus()
}
}
</script>
I hope you must have found a solution by now.
But I just wanted to add this for other new users like me.
mounted () {
this.$refs.focusMe.focus() // sometime this doesn't work.
}
Try adding this instead.
this.$nextTick(() => this.$refs.focusMe.focus())
For more info check this
Edit: 14/06/2022
Prashant's answer also helped me understand the nextTick in more depth.
nextTick allows you to execute code after you have changed some data and Vue.js has updated the virtual DOM based on your data change, but before the browser has rendered that change on the page.
You can create a fake input field and focus it during the click event.
<template>
<div>
<TextComp v-if='showText' />
<button #click='onShowText'> Show/hide input </button>
<input type="text" ref="dummykeyboard" style="opacity:0">
</div>
</template>
<script>
export default {
methods:{
onShowText() {
this.showText = !this.showText;
this.$refs.dummykeyboard.focus();
}
}
}
</script>
I would suggest triggering a click event on that field instead of focus
this.showText = !this.showText;
this.$nextTick(function () {
this.$refs.dummykeyboard.click();
})
I would go with this solution and it works for me. Just add set timeout and put focus method inside it
<template>
<div>
<input ref='focusMe' type='text'/>
</div>
</template>
<script>
export default {
name: 'TextComp',
mounted () {
setTimeout(() => {
this.$refs.focusMe.focus();
}, 500);
}
}
</script>
EDIT:
Ideally you can use $nextTick to wait until the DOM fully loaded, but sometimes it didn't work. So using setTimeout is just quick workaround to me.
https://v2.vuejs.org/v2/guide/custom-directive.html
In vue's official guide it says auto focus does not work in mobile safari. When the page loads, that element gains focus (note: autofocus doesn’t work on mobile Safari).
In iOS mobile safari, focus() only works when responding to a user interaction, like a click event. See: Mobile Safari: Javascript focus() method on inputfield only works with click?
My guess why it does not work in Vue:
In your Vue example, when you click on the button, it merely inserts a watcher into a batcher queue. A watcher has the information about what needs to update. You can see it as an update action, or update event. And later (almost immediately, at the next tick), Vue reads it (watcher) from the queue, and update the virtual dom / dom subsequently. However, this means, your code focus() is not "inside" a click event handler, rather, it is executed after the click event handler finishes.
I don't know the internal implementation of React though, so cannot explain why it works in the React example.
Still focus doesn't works for you???
Here is the solution :
setTimeout(() => {
this.$refs["input-0"].focus();
}, 1000);
The trick is to use setTimeout.

Detect synthetic click in React during testing with Jest/Enzyme

I am building an app with React. I am hiding a file input element (<input type="file"/>) "behind" a react-bootstrap Button to be able to control styling. So, when the button is clicked I turn around and fire a synthetic click event on the text input element, as shown below.
class OpenFileButton extends React.Component {
...
clickHandler() {
this.refs['input'].click();
}
render() {
return (
<ButtonGroup>
<div>
<input type="file" onChange={this.props.someCallback}
ref="input" style={{display: 'none'}}/>
<Button onClick={this.clickHandler}>Open File</Button>
</div>
</ButtonGroup>
);
}
}
I want to be able to test this with Jest/Enzyme. However, while I can simulate a click event on the button, I haven't figured out how to detect a synthetic click event on the file input element.
I have tried using Jest/Enzyme to mock the click method on the input element.
const component = mount(<OpenFileButton/>);
const fileInput = component.find('input');
const button = component.find('Button');
fileInput.click = jest.fn();
button.simulate('click');
expect(fileInput.click).toHaveBeenCalled();
However, mocking the click method this way does not work. I also can't add an onClick attribute, i.e. fileInput.props().onClick = jest.fn() does not work.
This question is about detecting synthetic click events in the code itself, not in the test code, and so is not relevant.
So, how can I detect a (synthetic) click event on a DOM element using Jest/Enzyme?
<input /> or this.refs.input is an instance of HTMLInputElement.
Then you can test if HTMLInputElement.prototype.click is called .
Using sinon you will have :
import sinon from 'sinon';
import {mount} from 'enzyme';
const clickInputSpy = sinon.spy(HTMLInputElement.prototype, 'click')
const component = mount(<OpenFileButton/>);
const button = component.find('Button');
button.simulate('click');
expect(clickInputSpy.called).toBeTruthy();
clickInputSpy.restore();
const clickInputSpy = sinon.spy(HTMLInputElement.prototype, 'click');
console.log(
'Is <input /> clicked ? ', clickInputSpy.called
);
document.querySelector('input').click();
console.log(
'Is <input /> clicked ? ', clickInputSpy.called
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sinon.js/1.15.4/sinon.min.js"></script>
<input />
The solution here involves spying on the click method of the particular file input element that I'm interested in. I can thus check to see if this file-input-element-click-spy was called after a click is simulated on the button element, as follows:
const openFileButtonWrapper = mount(<OpenFileButton/>);
const buttonWrapper = openFileButtonWrapper.find(Button);
const fileInputWrapper = openFileButtonWrapper.find('input [type="file"]');
const fileInput = fileInputWrapper.get(0);
const clickInputSpy = spyOn(fileInput, 'click');
buttonWrapper.simulate('click');
expect(clickInputSpy).toHaveBeenCalled();
The answer by #AbdennourTOUMI used Sinon's spy method which reminded me that Jest uses some Jasmine functionality, including its spyOn method, that isn't obvious from the Jest documentation. So, even though that other answer ended up spying on _all_ input elements, which is not ideal, it did send me in the right direction, so thank you, Adbennour.

Update value of an element in React component from iOS UIWebView

I am trying to transfer data from native iOS app to a React (not react-native) web app running in a UIWebView.
I get a string from native viewcontroller and need to "inject" it into the value of an <input> element inside a React component class. I gave the element an id like so:
render() {
<div>
...
<input id="iccid" type="text" name="iccid" pattern="\d*" onChange={this.iccidChanged.bind(this)} autoFocus />
...
</div>
}
Then using JavaScriptCore from iOS (Swift code) I run:
self.context = self.webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as? JSContext
self.context!.evaluateScript("document.getElementById('iccid').value='\(barcodeValue)';")
This seems work fine and I can see the value updated in the DOM inside the webview, problem is that my React onChange function (that validates the value and changes component state) does not fire as React does not interpret this as a change (probably because of DOM vs virtual DOM handling).
What's the correct way/best practice to update the element value from iOS AND get the React component to behave like user has typed it in?
I'm supposing that you want to trigger an event when a barcode is scanned. Your iOS app probably doesn't need to know where the event should be dispatched (i.e. which input field and how). It should be enough just to signal the web view that a barcode has been scanned.
You can dispatch a custom DOM event to window and have some React component listen to that event.
window.dispatchEvent(new CustomEvent('barcodescan', {
detail: {
value: '(barcodeValue)'
}
}))
You can then create a non-rendering component that will listen to the barcode event:
const BarcodeListener = React.createClass({
propTypes: {
onBarcodeScan: React.PropTypes.func.isRequired
},
componentDidMount () {
window.addEventListener('barcodescan', this.handleBarcodeScan)
},
componentWillUnmount () {
window.removeEventListener('barcodescan', this.handleBarcodeScan)
},
render () {
return null
},
handleBarcodeScan (e) {
this.props.onBarcodeScan(e)
}
})
Then, in your form, you can render the input alongside with the barcode event listener:
render () {
return (
<div>
...
<input ... />
<BarcodeListener onBarcodeScan={this.handleBarcodeScan} />
</div>
)
}
Then handle the barcode scan event separately.

Categories