I currently have a React tsx page with some input boxes; for example:
<textarea value={this.state.myData!.valueOne}
onChange={(e) => this.handleValueOneChange(e)}/>
<textarea value={this.state.myData!.valueTwo}
onChange={(e) => this.handleValueTwoChange(e)}/>
handleValueOneChange and handleValueTwoChange look almost identical:
handleValueOneChange(event: React.FormEvent<HTMLTextAreaElement>) {
let newState = { ...this.state };
newState.myData!.valueOne = event.currentTarget.value;
this.setState(newState);
}
handleValueTwoChange(event: React.FormEvent<HTMLTextAreaElement>) {
let newState = { ...this.state };
newState.myData!.valueTwo = event.currentTarget.value;
this.setState(newState);
}
Is it possible to have a single function for both events; for example:
handleDataChange(event: React.FormEvent<HTMLTextAreaElement>, valueByRef) {
let newState = { ...this.state };
valueByRef = event.currentTarget.value;
this.setState(newState);
}
I'm a little unclear on what this might look like in TypeScript: is what I'm trying to do possible and, if so, how?
EDIT:
To further complicate matters myData contains a mix of types (which appears to be an issue for TS)
Try something like this
HTML becomes
<textarea value={this.state.myData!.valueOne}
onChange={(e) => this.handleValueChange(e, 'valueOne')}/>
<textarea value={this.state.myData!.valueTwo}
onChange={(e) => this.handleValueChange(e, 'valueTwo')}/>
and logic (I suppose your state has type MyState)
handleDataChange<K extends keyof MyDataType>(event: React.FormEvent<HTMLTextAreaElement>, ref: K) {
let newState = { ...this.state };
newState.myData![ref] = event.currentTarget.value;
this.setState(newState);
}
EDIT:
I fixed the answer according to the comments
EDIT:
If myData contains strings, numbers and booleans you can use type guards and casts
handleDataChange<K extends keyof MyDataType>(event: React.FormEvent<HTMLTextAreaElement>, ref: K) {
let newState = { ...this.state };
let old = newState.myData![ref];
if (typeof old === "number") newState.myData![ref] = parseInt(event.currentTarget.value); /* or parseFloat, this depends on your app */
else if (typeof old === "boolean") newState.myData![ref] = (event.currentTarget.value === "1"); /* use any string -> boolean conversion function */
else newState.myData![ref] = event.currentTarget.value; /* string case */
this.setState(newState);
}
Of course, pass a string back, for example:
<Input onChange={(event)=>this.handleChange(event, 'type1')} />
If you give your text area's a name prop, you can use this name to add the value to state.
handleChange(event: React.FormEvent<HTMLTextAreaElement) {
this.setState({
...this.state.myData,
[event.currentTarget.name]: event.currentTarget.value,
})
}
<textarea value={this.state.myData.one} name="one"
onChange={this.handleChange} />
<textarea value={this.state.myData.two} name="two"
onChange={this.handleChange} />
onChange prop looks cleaner in this case too. No use of an inline arrow function.
Working Code sandbox https://codesandbox.io/s/04rm89oq8w
Related
I set up debounce inside my functional component like this:
const debouncedFunc= debounce(myFunction, 500);
I have the below TextField
<TextField
id="myField"
maxLength={8}
onChange={(e) => debouncedFunc(e.target?.value)}
/>
I have the myFunction like this
function myFunction(val) {
if (val.length === 8) {
console.log(val);
}
}
So this works well. It prints value when a user types eight characters into the field. The problem is that I need to empty the value in this field when a user types eight characters, and debounced function does kick in. Normally, TextField, I can empty the value in the field by e.target.value="". Since I am in the debounce function, I do not have a reference to the e, so I cannot empty it.
Long question short, what is the best way to empty the textfield from a debounce function?
My current and only solution is this, anyone that can think of a better solution please do share
export default function DebounceFeaturedTextfield() {
const debouncedFunc= debounce(myFunction, 500);
let TextFieldRef = "";
function myFunction(val) {
if (val.length === 8) {
TextFieldRef.setInputValue("");
}
}
return (
<TextField
ref={(r) => {
TextFieldRef = r;
}}
id="myField"
maxLength={8}
onChange={(e) => debouncedFunc(e.target?.value)}
/>
);
}
Hi there SO!
I'm currently trying to make a form that generates based on the object supplied and this seem to work at just about anything I throw at it.
That is, until I get to a nested object.
The problem:
Once I hit the if condition (typeof value === "object") I want to have a hidden input (this works).
Then I want to go into that object I just identified, and into all child objects it may contain and generate the input on same criteria as the initial run-through.
function GenericForm(props: any) {
var object = props.object;
return (
<div>
<form>
{Object.entries(object).map(([property, value]) => {
let type: string = "";
if (typeof value === "string") {
type = "text";
} else if (typeof value === "number") {
type = "number";
} else if (typeof value === "boolean") {
type = "checkbox";
} else if (value instanceof Date) {
type = "date";
} else if (typeof value === "object") {
type = "hidden";
}
return [
<label property={property} htmlFor={property}>
{property}
</label>,
<input
type={type}
id={property}
name={property}
defaultValue={value as string}
onChange={(newVal) => {
object[property] = newVal.target.value;
}}
/>,
];
})}
</form>
</div>
);
}
export default GenericForm;
I'm aware that this most likely utilizes some kind of recursion and while I have tried to solve it using recursion, I haven't been able to solve it. the code pasted here is from before I tried recursion, to have a "clean sheet" from where it went wrong for me.
EDIT 1 - added info on object structure
the object passed should be completely generic and allow for objects of any structure to be handed to the component, currently it should then just evaluate what type the properties are and make an input element from that.
one of the current objects I'm passing have the following JSON Schema
{
"id": "XYZ1",
"type": "twilio-api",
"sid": "someSID",
"from": "+phonenumberhere",
"name": "TWILIO SMS",
"credentials": {
"token": "someapitoken"
}
}
above object currently renders like so:
Assuming you have a Input component:
function Input = ({ name, type, value }) => {
// most of your code can fit here
return <input name={name} type={type} value={value} />
}
You can use your version of code, I use a simplified version as above to make our discussion easier. With that we can design a InputList component:
function InputList = ({ object }) => {
console.log('list', object)
return (
<div>
{Object.entries(object).map(([property, value]) => {
if (typeof value === "object") {
return <InputList object={value} />
} else {
return <Input name={property} />
}
})}
</div>
)
}
You can see inside this InputList, there's a call to InputList again, so that is the recursion you are looking for. The recursion stops when you don't have an object inside an object any more.
NOTE: React requires value and onChange to drive any input box. Otherwise they'll just behave like a native input. But this is not part of this question.
If we see the code, a word is always repeated (name, surname, position, nation).
const nameInput = document.getElementById('name-personalized')
let nameInputValue
const surnameInput = document.getElementById('surname-personalized')
let surnameInputValue
const positionInput = document.getElementById('position-personalized')
let positionInputValue
const nationInput = document.getElementById('nation-personalized')
let nationInputValue
nameInput.addEventListener('input', (event) => {
let value = event.target.value
value = value.trim().toUpperCase()
nameInputValue = value
})
surnameInput.addEventListener('input', (event) => {
let value = event.target.value
value = value.trim().toUpperCase()
surnameInputValue = value
})
positionInput.addEventListener('input', (event) => {
let value = event.target.value
value = value.trim().toUpperCase()
positionInputValue = value
})
nationInput.addEventListener('input', (event) => {
let value = event.target.value
value = value.trim()
nationInputValue = value
})
I want to write just that word, example:
avoidRepeat(name)
And let's say using a for cycle but I don't know how to do that.
Here's one way. Just wrap all the repeated code up in a function and pass the specifics as argument functions. I have removed the globals and replaced them with callback functions.
I'd say this solution is not optimal as it decreases code flexibility for the sake of making the code shorter. Passing two functions to the same function isn't ideal for readability.
You may find libraries like jquery, rxjs or even functional libraries like ramda could help reduce repetition and keep flexibility and readability intact.
<html>
<head><title>whatever</title></head>
<body>
<label for="name-personalized">name-personalized: </label><input type="text" id="name-personalized"><br>
<label for="surname-personalized">surname-personalized: </label><input type="text" id="surname-personalized"><br>
<label for="position-personalized">position-personalized: </label><input type="text" id="position-personalized"><br>
<label for="nation-personalized">nation-personalized: </label><input type="text" id="nation-personalized">
</body>
<script>
function avoidRepeat(name, mapFn, cbFn) {
const el = document.getElementById(name)
el.addEventListener('input', (event) => {
const val = mapFn(event.target.value);
cbFn(val, event.target);
})
}
const fnTrim = s => s.trim()
const fnTrimUpper = s => s.trim().toUpperCase()
const fnLog = (val, el) => console.log('for', el.getAttribute('id'), 'the value is ' + val)
avoidRepeat('name-personalized', fnTrimUpper, fnLog)
avoidRepeat('surname-personalized', fnTrimUpper, fnLog)
avoidRepeat('position-personalized', fnTrimUpper, fnLog)
avoidRepeat('nation-personalized', fnTrim, fnLog)
</script>
</html>
You can store the data in an object via the input's ID and just add an event listener to the inputs. Then accessed in bracket notation. I also removed -personalized from the keys.
This code is all you need for all of those and even future inputs
var values = {};
const inputs = document.querySelectorAll("input");
inputs.forEach(function(el) {
el.addEventListener('input', (event) => {
values[event.target.getAttribute("id").replace("-personalized","")] = event.target.value.trim()
})
});
You can access the data like this
console.log(values["nation"]);
I'm a beginner to React; I understand that setState is asynchronous, but I don't understand why in the example pen below the box below the refresh is not done immediately, and only updates after the second character is input.
Codepen: (updated to link to correct pen)
https://codepen.io/anon/pen/odZrjm?editors=0010
Portion:
// Returns only names matching user's input
filterList = (term) => {
let finalList = []
for (let x = 0; x < this.state.names.length; x++) {
if (this.state.names[x].toLowerCase().includes(term)) {
finalList.push(this.state.names[x])
}
}
finalList.sort()
return finalList
}
// Returns the name list as string
listNames = () => {
let filteredNames = [], filter = this.state.filter.toLowerCase(), names = ""
if (filter === "") return "Enter filter chars"
// Generate filtered array with only names matching filter
filteredNames = this.filterList(filter)
// Build and return comma-separated list of filtered names
names = filteredNames.join(', ')
if (names.length === 0) return 'None found'
return names
}
handleChange = (event) => {
let result, alert = 'alert-primary'
result = this.listNames()
this.setState({
filter: event.target.value,
namesList: result
})
}
If I replace the line "result = this.listNames()" in handleChange with just "result = 'test'" then it updates immediately. Can you please explain why it does not when I call the functions?
It occurs because you are calling the listNames method before this.state.filter is set, thus reaching:
if (filter === "") return "Enter filter chars"
Here's a working pen: https://codepen.io/anon/pen/gzmVaR?editors=0010
This is because listNames() is being called before setState
when you call handleChange method, it can't find 'this' scope.
so 'this' is undefined and can't call this.listNames().
solution:
use onChange={this.handleChange.bind(this)}
instead of onChange={this.handleChange}
I have simple react input component that accepts only numbers, each keyboard input i check inputs and return validated entries only, after 3rd character I add - character to the input. everything works fine but when i edit / remove one char from first third character my caret jumps to last position its annoying, how can i keep caret at the same position.
Link to working example
My input component
class App extends Component {
constructor(props){
super(props)
this.state = {
value: ''
}
}
render() {
const {value} = this.state
return (
<div className="App">
<input type="text" value={value} onChange={this._onChange}/>
</div>
)
}
_onChange = (e) => {
this.setState({value: this._validate(e.target.value)})
}
_validate(string){
return new Input(string)
.filterNumbersOnly()
.addSeparator()
.getString()
}
}
export default App;
Here is my string validation class
export default class Input{
constructor(string){
this.string = string
}
filterNumbersOnly(){
const regex = /(\d)/g
,strArray = this.string.match(regex)
this.string = strArray ? strArray.reduce((a,b) => { return `${a}${b}` }, '') : ''
return this
}
addSeparator(){
const charArray = this.string.split('')
this.string = charArray.reduce((a,b,index) => {
if(index > 2 && index < 4) {
return `${a}-${b}`
}
return `${a}${b}`
}, '')
return this
}
getString(){
return this.string
}
}
There is way if you could preserve the selection range and element target and then apply it after the set state i.e on set state callback like this:
_onChange = (e) => {
var r =[e.target.selectionStart,e.target.selectionEnd];
var el = e.target;
this.setState({value: this._validate(e.target.value)},()=>{
//just to make sure you extend cursor in case of - addition
if (r[1] === this.state.value.indexOf("-")+1)
r[1]++;
el.setSelectionRange(r[0], r[1]);
})
}
Sample url https://codesandbox.io/s/628q1rvnl3
It resolve all my problem, init selection in callback setState
let el = ev.target
this.setState({
sum: value
}, () => {
el.setSelectionRange(1, 1)
})