Manage focusing multiple TextInputs in react-native (more than 10+) - javascript

In my React Native application, there will be many TextInputs, and when the user presses the return button, I want react to focus on the next TextInput. Previous stackoverflow threads recommend using refs, but only have examples for 2 TextInputs. What would be the most efficient way to create lets say 30+ refs and handle focusing?
React Native 0.59 w/ regular old js.

I'm using this plugin
https://github.com/gcanti/tcomb-form-native
It allow me to pass inputs as Array, Thus it can be easy manipulate the array and set the focus dynamically.
You can do the following, First install the library
npm install tcomb-form-native
Then import the library and set your input
var t = require('tcomb-form-native');
var Person = t.struct({
name: t.String, // a required string
surname: t.maybe(t.String), // an optional string
age: t.Number, // a required number
rememberMe: t.Boolean // a boolean
});
Now you can customize fields options through an array. Note I just add a key called "next" this will allow me to loop through the array and focus on the next input dynamically in componentDidMount
let options = {
fields: {
name: {
returnKeyType: "next",
next: "surname"
},
surname: {
returnKeyType: "next",
next: "age"
},
age: {
returnKeyType: "next",
next: "rememberMe"
},
rememberMe: {
returnKeyType: "done",
next: "rememberMe"
}
}
};
Now here is the magic, We just need to loop on the array and dynamically add the focus function for each input
Object.keys(options.fields).forEach(function(key) {
if ("next" in options.fields[key]) {
if (key == options.fields[key]["next"]) {
//set the blur in case the input is the last input
options.fields[key].onSubmitEditing = () => {
self.refs["form"].getComponent(key).refs.input.blur();
};
}else{
//set the next focus option
options.fields[key].onSubmitEditing = () => {
self.refs["form"].getComponent(options.fields[key]["next"]).refs.input.focus();
};
}
}
})
self.setState({
options: options
});
and then in the render section you should be able to pass fields and options
<Form
ref="form"
type={Person}
options={this.state.options}
value={this.state.value}
/>
This should work dynamically, without worry about how much inputs do you have.
Hopefully this answer your question!

// i have a form handler class which does it this is its code.
import React, { Component } from "react";
import { getKeyboardType, isInputValid, isSecureTextEntry } from "./Utils";
class Form extends Component {
childReferences = [];
childDetails = [];
/* go through each input and check validation based on its type */
checkValidation = () => {
let isValid = true;
for (let i = 0; i < this.childReferences.length; i++) {
if (
this.childReferences[i].getValue &&
!isInputValid(
this.childReferences[i].getValue(),
this.childDetails[i].type
)
) {
this.childReferences[i].setError &&
this.childReferences[i].setError(true);
isValid = false;
}
}
return isValid;
};
/* collecting user entered values from all inputs */
getValues = () => {
let data = {};
this.childReferences.forEach((item, index) => {
data[this.childDetails[index].identifier] = item.getValue();
});
return data;
};
onSubmitForm = () => {
return this.checkValidation() ? this.getValues() : undefined;
};
refCollector = ref => this.childReferences.push(ref);
collectChildDetails = childProps =>
this.childDetails.push({
identifier: childProps.identifier,
type: childProps.type
});
/* handling onSubmit of each input when user moves to next input from keyboard */
onSubmitEditing = index => ev => {
if (
index < this.childReferences.length - 1 &&
this.childReferences[index + 1].setFocus
) {
this.childReferences[index + 1].setFocus();
}
};
render() {
const wrappedChildrens = [];
React.Children.map(this.props.children, (child, index) => {
if (!child) {
return;
}
/* holding details of input in an array for later user */
this.collectChildDetails(child.props);
/* cloning children and injecting some new props on them */
wrappedChildrens.push(
React.cloneElement(child, {
key: child.props.identifier || `${child.props.type}_${index}`,
ref: this.refCollector,
onSubmitEditing: this.onSubmitEditing(index),
returnKeyType:
index < this.props.children.length - 1 ? "next" : "done",
keyboardType: getKeyboardType(child.props),
secureTextEntry: isSecureTextEntry(child.props)
})
);
});
return wrappedChildrens;
}
}
export default Form;
// this is how its used
<Form ref={ref => (this.formHandler = ref)}>
<MaterialInput
label="Emial address"
error="Enter a valid email"
rightImage={Images.forwardArrow}
type={INPUT_TYPES.EMAIL}
identifier="email"
blurOnSubmit={false}
/>
<MaterialInput
label="Password"
error="Password length must be greater then six"
rightImage={Images.forwardArrow}
type={INPUT_TYPES.PASSWORD}
identifier="password"
/>
</Form>

Related

List items with inputs - how to add list object to an array when input has value?

I have created a list of objects and each list item has input within:
{flowers_data?.map((flower) => {
return (
<>
<div className={classes.Nested_Flower_Container} key={flower.id}>
<div className={classes.Nested_Flower_Name}>
{flower.name}
</div>
<div className={classes.Nested_Flower_Input} style={{ marginRight: '0.2em' }}>
<TextField
id="Amount"
label="Amount"
variant="outlined"
size="small"
type="number"
onChange={(e) => {
setAmount(e.target.value);
handleAddList(e.target.value, flower);
}}
className={classes_2.root}
/>
</div>
</div>
</>)
})}
How can I add an object that has a value in input to an array? I tried to do this using a function that I created, but each time I change one element's target.value and move on to the next item to change its input value, there is only one element in the array with the latest target.value. And after modifying the inputs, when I try to output the values outside that function with e.g. a button, the add_flowers_tab array is empty.
handleAddList function:
let temp_flower: Flower;
let add_flowers_tab: Flower[] = [];
const handleAddList = (targetValue: string, flower: Flower) => {
temp_flower = {
"id": flower.id,
"name": flower.name,
"price": flower.price,
"amount": Number(targetValue),
"creation_date": flower.creation_date
}
if (targetValue === '') {
/* Delete flower when input is empty */
add_flowers_tab.forEach(tabFlower => {
if (tabFlower.id === temp_flower.id) {
const indexOfDelete = add_flowers_tab.indexOf(tabFlower);
add_flowers_tab.splice(indexOfDelete, 1);
}
})
}
if (targetValue !== '') {
/* Add flower to tab when input has value */
if (add_flowers_tab.length > 0) {
/* When input changes, delete flower with old input value and add the new one */
add_flowers_tab.forEach(tabFlower => {
if (tabFlower.id === temp_flower.id) {
const indexOfDelete = add_flowers_tab.indexOf(tabFlower);
add_flowers_tab.splice(indexOfDelete, 1);
add_flowers_tab.push(temp_flower);
}
})
}
else {
/* Add new flower as a first element in the array */
add_flowers_tab.push(temp_flower);
}
/*
Displays an array with only the most recently added temp_flower, even though
several inputs in list have values
*/
console.log(add_flowers_tab);
}
}
Here is the minimum solution for generating a list on Inputs that each update a single element in a list on state.
export const ManyInputs = ({inputs}) => {
const [list, setList] = useState(inputs);
const setIndividual = (value, id) =>
// Update a single field in the object in the list that matches the given id
setList(list.map(l => l.id === id ? ({...l, value}) : l));
return list.map(i =>
// Send the id back to help match the object
<TextField onChange={(e) => setIndividual(e.target.value, i.id)} />
);
};
You can have one React state, which has an array of objects that you enter through your "mapped" component.
const [tmpAmount, setTmpAmount] = React.useState<{flower:Flower,amount:number}[]>([])
// fill the temporary state with generated flowers only once
React.useEffect(()=>{
setTmpAmount(flowers_data.map((flwr) =>{flower:flwr, amount:0}))
},[])
and replace the onChange()
onChange={(e) => {
// find existing flower to edit the amount of it
let indexToChange = flowers_data.findIndex((element) => element.id === flower.id)
// update and replace the array variable with updated object
setAmount2((prev) =>
[
...prev.slice(0, indexToChange),
{
...prev[indexToChange],
amount: e.target.value,
},
...prev.slice(indexToChange + 1),
]);
}
You can also check if the array changes and print it:
React.useEffect(()=>console.log(tmpAmount),[tmpAmount])

JSX select element does not seem to auto-select (implement 'selected') on option. Am I missing something?

I have a <select> tag which I use to create my own custom <SelectField> component as follows:
export default function SelectField(props) {
/*
PARAMS:
- fieldname (String)
- fieldID (String)
- options (Array([Object, Object...]) [{"option_value": "option_name"}, ...])
- init_value (String "option_value")
*/
const generate_options = () => {
// Function for handling SelectField options
let element_list = [];
element_list.push(
<SelectOption key={0} option_value="" option_text="None" />
);
var count = 1;
if (!Array.isArray(props.options)) {
for (var [value, name] of Object.entries(props.options)) {
element_list.push(
<SelectOption key={count} option_value={value} option_text={name} />
);
count += 1;
}
} else {
props.options.forEach((subject) => {
element_list.push(subject.to_option());
});
}
return element_list;
};
const nameToString = () => {
// Converts props.fieldname into a properly formatted name
if(props.fieldname.indexOf("_")){
var full_field_name = `${props.fieldname.split("_").join(" ")}`;
return `${full_field_name[0].toUpperCase() + full_field_name.slice(1)}`;
};
return `${props.fieldname[0].toUpperCase() + props.fieldname.slice(1)}`;
};
return (
<div className="form-group">
<label htmlFor={props.fieldname}>
{nameToString()}:
</label>
<select
name={`${props.fieldname}`}
id={`${props.fieldID}`}
className="countries"
defaultValue={props?.init_value ? `${props.init_value}` : ""}
>
{generate_options()}
</select>
</div>
);
}
(PS. Don't mind the className - it really SHOULDN'T relevant. But who knows with a beginner like me... DS). Now, I want to use that field to create a <select> tag for Subjects within my web-app (powered by Django), and I created a sub-component for it that looks like this:
export function SubjectSelectField(props) {
const [subjects, setSubjects] = useState([]);
useEffect(() => {
const getSubjects = async () => {
let reqObj = new RequestHandler("/courses/subjects/");
const data = await reqObj.sendRequest();
setSubjects(
data.map((item) => {
return new Subject(item);
})
);
};
getSubjects();
}, []);
console.log({ subjects });
return <SelectField options={subjects} {...props} />;
}
When rendering the page with this component, I get the following console.logs:
{
"subjects": [
{
"id": 6,
"name": "Art"
},
{
"id": 4,
"name": "Biology"
},
{
"id": 5,
"name": "Chemistry"
},
{
"id": 3,
"name": "Geography"
},
{
"id": 2,
"name": "Language"
},
{
"id": 1,
"name": "Mathmatics"
},
{
"id": 7,
"name": "Physics"
},
{
"id": 8,
"name": "Social Studies"
}
]
}
For reference; this is my custom Subject class (ES6):
export class Subject {
constructor({ id, name }) {
this.id = id;
this.name = name;
}
to_option() {
return (
<SelectOption
key={this.id}
option_value={this.id}
option_text={this.name}
/>
);
};
}
And the <SelectOption> component:
export function SelectOption(props) {
return <option value={`${props.option_value}`} >{`${props.option_text}`}</option>;
}
So, the output I expect is for the <SelectField> to automatically assign the selected attribute to the option that has the value of the <SelectField>'s init_value prop.
I use the <SelectField> for another type of field I call <CountrySelectField> and that works just fine. The country I expect to be pre-selected is properly selected as per its init_value.
That component looks like this:
export function CountrySelectField(props) {
return (
<SelectField
init_value={props?.student?.country ? `${props.student.country}` : ""}
{...props}
/>
);
}
As I mentioned, if I pass a value to this component's init_value prop, it executes just as expected and renders with correct option having selected attribute set on it.
The <SubjectSelectField> doesn't render the way I expect - with correct <option> having a selected attribute set.
Why does it not work?
EDIT:
Changed the Subject class to a React functional component:
export function Subject(props){
const id = props.subject.id;
const name = props.subject.name;
console.log(id, name, props.subject);
if(props.option){
return(
<SelectOption
key={id}
option_value={id}
option_text={name}
/>
);
}
return(
<h1>{name} - {id}</h1>
);
}
In order for other things to work, I changed these things too:
export function SubjectSelectField(props) {
// ...
setSubjects(
data.map((item) => {
return <Subject subject={item} />;
})
);
// ...
The option still won't be auto-selected.. I have other fields that works with mostly the same logic (SelectField is used in other places as well and works), but I have clearly messed something up here.
PS.
If you have time to language-police, you definitely have time to look into the question too. Don't you, #halfer? Jeez..
DS.
I am relieved to have solved this problem on my own, and the problem was that I never implemented a check on the SubjectSelecField to render the <select> field ONLY when the Subjects are loaded form the server.
E.g. like this:
export function SubjectSelectField(props) {
const [subjects, setSubjects] = useState([]);
// Here I use 'loaded' as a check variable when rendering.
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const getSubjects = async () => {
let reqObj = new RequestHandler("/courses/subjects/");
try{
const data = await reqObj.sendRequest();
setSubjects(
data.map((item) => {
return <Subject subject={item} />;
})
);
// If the Subjects load properly, set 'loaded' to true.
setLoaded(true);
}
catch(error){
console.log(`Something went wrong when trying load Subjects from server!`);
console.error(error);
// Else we make sure its set to false.
setLoaded(false);
}
};
getSubjects();
}, []);
console.log({ subjects });
// Here, before rendering, we check for the 'loaded' variable
// to make sure the <select> tag doesn't try to load without
// the <option> tags. Otherwise, the 'defaultValue' prop won't work.
if(loaded) return <SelectField init_value={props.init_value} options={subjects} {...props} />;
return <span>Loading Subject list...</span>;
}
Thank you people for trying to look into it at least :)

Why mapping filtered array of objects is not rendered using react?

I am new to programming. I want to display the list of filtered users in a dropdown.
Below is what I am trying to do,
when user types '#' in the input field I get the string after '#' and use that string to filter the one that matches from user_list.
But this doesn't show up in the dropdown.
Say, user types '#', but it doesn't show the dropdown with user list.
Could someone help me fix this? thanks.
class UserMention extends react.PureComponent {
constructor(props) {
super(props);
this.state = {
text='',
user_mention=false,
};
this.user='';
}
get_user = s => s.includes('#') && s.substr(s.lastIndexOf('#')
+ 1).split(' ')[0];
user_list = [
{name: 'user1'},
{name: 'first_user'},
{name: 'second_user'},
];
handle_input_change = (event) => {
let is_user_mention;
if (event.target.value.endsWith('#')) {
is_user_mention = true;
} else {
is_user_mention = false;
}
this.setState({
is_user_mention: is_user_mention,
[event.target.name]: event.target.value,
});
this.user = this.get_user(event.targe.value);
}
render = () => {
const user_mention_name = get_user(this.state.text);
console.log("filtered_users", filtered_users);
return {
<input
required
name="text"
value={this.state.text}
onChange={this.handle_input_change}
type="text"/>
{this.user &&
<div>
{this.user_list.filter(user =>
user.name.indexOf(this.user)
!== -1).map((user,index) => (
<div key={index}>{user.name}
</div>
))
}
</div>
}
);};}
It seems like this.state.user_mention is always false. In handle_input_change you set is_user_mention, but in render you check user_mention. Could this possibly be the issue?
Since user_mention is false, the expression will short-circuit and not render the list.

Trigger change-event inside react-component not working

I have a simple react-component which contains of a form. In this form the user has an search-box where to find users.
In order to have a valid form, there must be at least 3 users but not more than 6.
Therefore I've added a hidden field (hidden by surrounding display:none; div) which contains the number of already added users.
This one will show the error-message in case the current number is invalid.
The following code is simplified but shows the main issue I have.
In my render-call I have:
render():JSX.Element {
return (
<form>
<ValidatableInput
input={<Input type="number" value={this.state.users.length.toString()} min={3} max={6} getRef={(el: HTMLInputElement) => this.testElement = el} onChange={() => alert("changed...")} />}
minValueMessage={"Currently there are " + this.state.users.length + " users. A minimum of 3 is needed"}
hidden={true} />
<UserSearch onUserSelected={this.handleUserSelected} />
// ...
</form>
);
}
whereas UserSearch is the component to search for users. This is mainly an autocomplete-input which triggers the handleUserSelected:
private handleUserSelected = (selectedElement: IUser) : void => {
// get a copy of the current state
const newState = { ...this.state };
// add the user
newState.users.push(selectedElement);
// thats what I've tried so far
this.testElement.dispatchEvent(new Event("change"));
this.testElement.dispatchEvent(new Event("change", {bubbles: true}));
this.testElement.onchange(new Event("change"));
this.testElement.onchange(new Event("change"), {bubbles: true});
this.setState(newState, () => // here I want to do the triggering as the input has now the new count as value set)
}
However, the console does not show changed at all.
How can I call/trigger the change-event manually when something other changed on the component?
This is the ValidatableInput-component:
export class ValidatableInput extends React.Component<IValidatableInputProps, IValidatableInputState> {
render(): JSX.Element {
const { labelText, input, requiredMessage, maxLengthMessage, minValueMessage, hidden } = this.props;
input.props.onInvalid = this.handleInvalid;
input.props.onChange = this.handleChange;
if (hidden) {
// set height to 0
}
return (
<FormGroup row >
<Label for={input.props.name} md={3}>{!hidden && labelText}</Label>
<Col md={9}>
<div>
{input}
{this.state.validityState.valueMissing && <div className={classNames("invalid-feedback")}>{requiredMessage || "This field is required"}</div>}
{this.state.validityState.tooLong && <div className={classNames("invalid-feedback")}>{maxLengthMessage || "The field shoud not be longer than " + input.props.maxLength}</div>}
{this.state.validityState.rangeOverflow && <div className={classNames("invalid-feedback")}>{maxLengthMessage || "There are too many items. Maximum allowed: " + input.props.max}</div>}
{this.state.validityState.rangeUnderflow && <div className={classNames("invalid-feedback")}>{minValueMessage || "There are too less items. Minimum needed: " + input.props.min}</div>}
</div>
</Col>
</FormGroup>
);
}
private handleInvalid = (ev: React.FormEvent<HTMLInputElement>): any => {
const input = ev.target as HTMLInputElement;
this.setState({ validityState: input.validity });
}
private handleChange = (ev: React.FormEvent<HTMLInputElement>): any => {
const input = ev.target as HTMLInputElement;
this.setState({ validityState: input.validity });
}
}
Maybe keep it simpler and remove the refs and eventHandling boilerplate.
Your business logic relies on this.state.users.length, right?
So use it on your favor:
handleUserSelected = (selectedElement: IUser) : void => {
this.setState({
users: [
...this.state.users,
selectedElement,
],
})
}
render() {
const { users } = this.state
const isInvalid = users.length < 3 || users.length > 6
return (
<form>
{isInvalid && <span>Your invalid message</span>}
<UserSearch ... />
</form>
)
}

Can I combine these two array into one object using React and Javascript?

While working on my application I found out that the way I have it set up now is not really ideal. I am currently saving the values of two dynamicly created input fields in two seperate arrays. Now as I understand this is not really smart to do since both input fields have the same index and actually belong together.
Now I was wondering how I can change my code so I can save both values in the same Object.
This is my code for my dynamic form:
class MediaInput extends React.Component {
render() {
const linkName = `link${this.props.index}`;
const contentName = `content${this.props.index}`;
return (
<div>
<ControlLabel>Media (optional)</ControlLabel>
<input
onChange={(event) => this.props.handleChangeUrl(event, this.props.index)}
name={ linkName }
value={ this.props.mediaUrls[this.props.index]}
className="form-control"
placeholder="Add your media url. We accept YouTube, Vimeo and SoundCloud links"
type="text"
/>
<input
name={ contentName }
onChange={(event) => this.props.handleChangeContent(event, this.props.index)}
value={ this.props.mediaContents[this.props.index]}
className="form-control"
placeholder="Add your media content"
type="text"
/>
</div>
);
}
}
This is my current state:
this.state ={
mediaFields: [],
content: [],
mediaUrls: [ null, null, null ],
mediaContents: ['', '', ''],
};
These are my functions:
// Add/remove media fields
add() {
event.preventDefault();
const mediaFields = this.state.mediaFields.concat(MediaInput);
if (i < 3) {
this.setState({ mediaFields });
i++
} else {
Bert.alert('Only 3 media links are allowed', 'danger');
}
}
remove() {
event.preventDefault();
const lastElement = this.state.mediaFields.pop();
const mediaFields = this.state.mediaFields;
this.setState({ mediaFields });
i--
}
// Handle change media fields
handleChangeUrl(e, index) {
// Shallow copy of array
const mediaUrls = this.state.mediaUrls.slice();
let url = e.target.value
if (!/^https?:\/\//i.test(url)) {
url = 'http://' + url;
}
mediaUrls[index] = url;
this.setState({ mediaUrls});
}
handleChangeContent(e, index) {
// Shallow copy of array
const mediaContents = this.state.mediaContents.slice();
mediaContents[index] = e.target.value;
this.setState({ mediaContents });
}
And this is the part of the form where I add the input fields:
[...]
const mediaFields = this.state.mediaFields.map((Element, index) => {
return <Element key={ index } index={ index } mediaUrls={this.state.mediaUrls} mediaContents={this.state.mediaContents} handleChangeUrl={this.handleChangeUrl} handleChangeContent={this.handleChangeContent} />
})
[...]
<div>
{ mediaFields }
<Button onClick={ () => this.add() }>Add media field</Button>
<Button onClick={ () => this.remove() }>Remove media field</Button>
</div>
As you can see I have almost the same function and I understand that this is not great, so I want to learn how I can do it better.
I want to save both the values in the same Object. Like so:
const mediaContent = [ link1: content1, link2: content2, link3: content3 ]
Hope someone can push me in the right direction.
In Javascript, an array can only be indexed by a number (it is not like PHP).
That being said, you can have both values, like you want, in an object (json).
So, you would have something like:
const mediaContent = { link1: content1, link2: content2, link3: content3 }
Then, you just need to treat your object like an array. An object doesn't have the methods pop and concat, but it can be easily achieved using Object.Assign for adding, and Object.keys + map reducing functions to remove (actually treating your object again as array).
Extra tip: You can remove the i variable and use Object.keys(this.state.mediaFields).length instead.
add() {
event.preventDefault();
const mediaFields = Object.assign(this.state.mediaFields, MediaInput);
if (i < 3) {
this.setState({ mediaFields });
i++;
} else {
Bert.alert('Only 3 media links are allowed', 'danger');
}
}
remove() {
event.preventDefault();
const mediaFields = Object.keys(this.state.mediaFields)
.map(k => this.state.mediaFields[k])
.pop()
.reduce((items, curr) => ({
...items,
curr
}), {});
this.setState({ mediaFields });
i--
}
Be aware that keeping the order for adding and removing might not work properly.

Categories

Resources