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.
Related
I'm struggling with getting a modal to appear onClick(). I have a function within a component that adds players to an existing list when clicking on Add Player. The button is rendered separately in a renderAddButton() function, which passes onAddButtonClick() as a prop.
I would like for the user to be able to input the player's name in a form within a modal before it is added to the list, right now the code outputs a Player + index as the name of the player.
function onAddButtonClick() {
setItems((prev) => {
const newItems = [...prev];
newItems.push({
name: `Player ${newItems.length + 1}`,
teamId: currentTeam[0].teamId
});
playersStore.push({
name: `Player ${newItems.length + 1}`,
teamId: currentTeam[0].teamId
});
return newItems;
});
}
I have this form which I want to represent in the modal:
export const PlayerForm = () => {
return (
<div>
<form>
<input type='string' id='playerId' name='playerName' defaultValue='0' />
<input
type='number'
id='playerGoals'
name='totalGoals'
defaultValue='0'
min='1'
max='5'
/>
<input
type='number'
id='playerGoals'
name='playerGoalPercentage'
defaultValue='0'
min='1'
max='5'
/>
</form>
</div>
);
};
How do I trigger the modal from inside onAddButtonClick()?
I implement modals using the react-bootstrap framework.
From the component that I want to display the modal from, I will create a handler that will govern the component's ability to show the modal based on the bool I set in state. Typically from the parent component this show handler would look like this:
setShow = () => {
this.setState({ show: !this.state.show });
};
As seen in the example this handles a state attribute called show which is what dictates whether or not the modal gets to display in app.
Below is the implementation of the modal I would use as a child component to the parent component where it would reside and where I would pass the state attribute which I called show that dictates with true or false whether or not to display the modal:
<ExampleModal
show={this.state.show}
setShow={this.setShow}
activeRecord={this.state.activeRecord}
activePrimaryAccountId={this.state.activePrimaryAccountId}
userAccessRole={this.props.userAccessRole}
/>
I pass the necessary details that the modal needs to display as props that I get from the the parent component's state attributes. The most important being the show attributes to include the setShow function which I use in the child component (the modal itself) to update state in the parent component to close the modal when the time comes also.
In the ExampleModal component I start off with declaring state with the following attributes already loaded from props:
this.state = {
show: this.props.show,
...
}
I then use a handler that takes advantage of the setShow function passed down to the child component in props as shown:
handleClose = () => this.props.setShow(false);
In the modal component there is a button that uses this handler in its onClick() synthetic event to trigger the closing of the modal after it has rendered to the browser.
Conversely in the parent component, your button will use the onClick() synthetic event to trigger a call that would be implemented something like this in the button to open the modal:
onClick={this.setShow(true)}
I reuse that process in all of my modals in React.js, hope that helps. The trick here is using componentDidUpdate() or useEffect() (if you're using React Hooks) effectively to make sure you have the right data loaded in state in the parent component so that you can pass it into the props of the child component at the right time. The <ExampleModal /> I gave you should give you enough of a clue.
I have a parent component which contains a child, which in turn contains a md-dialog component. I open the dialog from the component with this:
ParentComponent.vue
<template>
<div>
<ConfirmDialog
:dialogState="isDialogShown"
#closeDialog="value => isDialogShown = false"
/>
<button #click="handleOpenModal()">open modal</button>
</div>
</template>
<script>
export default {
name: 'ParentComponent',
data() {
return { isDialogShown: false }
},
methods: {
handleOpenModal() {
this.isDialogShown = true;
}
}
}
</script>
Button click opens this dialog:
ConfirmDialog.vue
<template>
<md-dialog :md-active.sync="localDialogState">
markup
</md-dialog>
</template>
<script>
export default {
name: "ConfirmDialog",
props: ["dialogState"],
computed: {
localDialogState: {
get() {
return this.$props.dialogState;
},
set() {
this.handleCloseDialog();
}
}
},
methods: {
handleCloseDialog() {
this.$emit("closeDialog");
}
}
};
</script>
I don't really like this because it uses this localDialogState's setter to perform this side effect which only eventually funnels back down to set localDialogState after setting the prop on the parent, however so far I've had to use the setter so I can capture events like backdrop click or ESC keypress that are meant to close the modal.
NB, I have had to use this "local" version of dialog state because apparently props and computed props are all just kept on this object's instance, so the names collide otherwise. There might also be a better way to do this which I am open to hear suggestions on since the above are my first lines of Vue I have ever written.
What is the canonical way of updating these (.sync'd) props in order to catch close events triggered by md-mialog?
Obviously a dialog cannot be used by only one boolean prop. That way you can't distinguish when a dialog was cancelled and when it was confirmed. In more complex cases you may have several confirm buttons with different actions.
I recommend to use a method like open to open a dialog by using $refs in a parent component and close a dialog inside itself with emitting confirmation events that can be catched in a parent component.
Background:
I have a web app built with React (v. 16.4.2 currently). It will only ever be used on a touch screen. It is composed of a ton of buttons to do things, and since it's all touch, I'm using touchstart/touchend to handle these actions.
Example:
This is a basic example of how I'm using the events. You click a button, it sets this.state.exampleRedirect to true, which in turn, makes the component re-render and then go to the new page (using react-router-dom). This is all working fine.
<button
type='button'
onTouchStart={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
Issue:
I originally used onClick to handle buttons but had issues because my users have fat fingers and not a lot of tech background, and when they'd touch a button, they'd drag their finger over the button and it wouldn't fire the click. OnTouchStart fixes this problem by firing the minute any touch happens (drag, swipe, tap, etc).
The issue is with onTouchStart. A user touches the button, it quickly changes the page (using the router) and re-renders the new page. The app is fast, so this is almost instantaneous, which means that when the new page loads, the user's finger is usually still on the screen, thus firing ANOTHER touch event on whatever they're touching. This is often another routing button, so it just fires through screens until they lift their finger.
I am working around this by putting a delay on enabling buttons on each page load.
// example component
import React, { Component } from 'react';
class ExampleComponent extends Component {
state = { buttonsDisabled: true }
// after 300ms, the buttons are set to enabled (prevents touch events
// from firing when the page first loads
componentWillMount() {
timeoutId = setTimeout(() => {
this.setState({ buttonsDisabled: false });
}, 300);
}
render() {
return (
// button in render method
<button
disabled={this.state.buttonsDisabled}
type='button'
onTouchStart={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
);
}
Is there a better way? Or a way to do what I'm doing, but globally so I don't have to add this jankity code in about 100 components?
Thanks!
Instead of using the onTouchStart event which is fired when a touch point is placed on the touch surface and using a timeout, which is a bit of a hack, you should make use of onTouchEnd since it will be fired when a touch point is removed from the touch surface, thereby ensuring that the mentioned case doesn't happen.
// example component
import React, { Component } from 'react';
class ExampleComponent extends Component {
state = { buttonsDisabled: true }
// after 300ms, the buttons are set to enabled (prevents touch events
// from firing when the page first loads
componentWillMount() {
timeoutId = setTimeout(() => {
this.setState({ buttonsDisabled: false });
}, 300);
}
render() {
return (
// button in render method
<button
disabled={this.state.buttonsDisabled}
type='button'
onTouchEnd={() => this.setState({ exampleRedirect: true })}
className='o-button'>
Open modal
</button>
);
}
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.
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'));