Wrapping a TextField in my own reusable component - reactjs

So I have a component that adds a * at the beginning of typed in text that looks like this:
export default class MyTextField extends Component {
constructor() {
super();
this.state = {
value: '',
};
}
static propTypes = {
onChange: PropTypes.func,
value: PropTypes.string
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
console.log(`I am the parent wit newValue ${newValue}.`);
this.setState({
value: this.addSymbol(newValue, '*')
});
}
render() {
const {
onChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.state.value}
onChange={this.change.bind(this)}
{...other } >
</TextField >
);
}
}
Now I use MyTextFieldin another component like this:
<MyTextField
style={styleTextFields}
floatingLabelText='Add text'
type='text'
/>
Now when I start typing everything works great.
If I change the code above and add value='test' prop on MyTextField everything breaks. It appears its because I added a prop to TextField which can not be overwritten by MyTextField value={this.state.value}.
So how can I add prop value and still add a * at the beginning of the typed text?
The other part is if the user adds their own onChange, how can I get both MyTextField and the component using MyTextField onChange events to get called?
=======Beginning of Edit 1===============
#Patrick - I have made tweaks based on your comment and the first part is now working. So I can type in <TextField /> and I see the changes.
The changes are below:
export default class MyTextField extends Component {
constructor(props) {
super();
this.state = {
value: props.value ? this.addSymbol(props.value.toString(), '*') : ''
};
}
static propTypes = {
onChange: PropTypes.func,
value: PropTypes.string
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
console.log(`I am the parent wit newValue ${newValue}.`);
this.setState({
value: this.addSymbol(newValue, '*')
});
}
render() {
const {
value, // eslint-disable-line no-unused-vars
onChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.state.value}
onChange={this.change.bind(this)}
{...other } >
</TextField >
);
}
}
I am not sure how to make the onChange tweak because you are not supposed to modify component state?
I also did try this in the render() function which does not work:
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
console.log(`I am the parent wit newValue ${newValue}.`);
this.setState({
value: this.addSymbol(newValue, '*')
});
}
render() {
const {
value, // eslint-disable-line no-unused-vars
onChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.addSymbol(this.state.value, '*')}
{...other } >
</TextField >
);
}
Anyway, thank you for your help.
=======Ending of Edit 1===============
=======Beginning of Edit 2===============
I fixed the onChange event problem by adding my own event called onNewValueChange and calling it in the onChange. Here is the final code:
export default class MyTextField extends Component {
constructor() {
super();
this.state = {
value: props.value ? this.addSymbol(props.value.toString(), '*') : ''
};
}
static propTypes = {
onNewValueChange: PropTypes.func,
value: PropTypes.string
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
change(input, newValue) {
const value = value: props.value ? this.addSymbol(props.value.toString(), '*') : '';
this.setState({
value: value
});
if(this.props.onNewValueChange) {
this.props.onNewValueChange(value);
}
}
render() {
const {
onNewValueChange, // eslint-disable-line no-unused-vars
...other
} = this.props;
return (
<TextField
value={this.state.value}
onChange={this.change.bind(this)}
{...other } >
</TextField >
);
}
}
Now I use MyTextField in another component like this:
<MyTextField
style={styleTextFields}
floatingLabelText='Add text'
type='text'
onNewValueChange={(newValue) => console.log(newValue) }
/>
#Patrick thank you for your help getting me in the right direction.
=======Ending of Edit 2===============

In general, I think this pattern of omitting props before passing everything else inside a component is confusing, at least for me. Being explicit as possible should always be a priority.
If I understood you correctly, the expected behavior is as follows:
You can pass a default text, this text should be prefixed with a * by your component.
You can modify the text, but no matter how you modify it, it should always be prefixed with a * - so an empty string should just be *.
I've created this fiddle which hopefully does what you meant : https://jsfiddle.net/69z2wepo/78041/
The key concept is to separate between props and state, a prop is passed only once, and should really be responsible for the initial state of your
Here's the two classes I created :
class Input extends React.Component{
render(){
return (<input onChange={this.props.onChange} value={this.props.value}/>)
}
}
class WrappedInput extends React.Component{
constructor(props){
super();
let val = props.value || '';
this.state = {
value : this.addSymbol(val,'*')
}
}
handleChange(e){
let newVal = e.target.value;
this.setState({value:this.addSymbol(newVal,'*')});
}
addSymbol(newValue, symbol) {
return newValue.indexOf(symbol) === -1 ? `${symbol}${newValue}` : newValue;
}
render(){
return (<Input onChange={this.handleChange.bind(this)} value={this.state.value}/>)
}
}

Related

How to refactor class component to functional(have to rerender, got undefined )

In IssueEdit component the main state of my app is stored(which I got from an API
call).
I pass that data to a specialized input component where issue.effort have to be
converted to string and stored as string. It works well in case of Class Component. I tried to refactor the class component to functional component and this is when the problem started. The problem is that after first render formattedValue is undefined so nothing is displayed. The input field is empty. How to make functional component behave equally as class component?
export const IssueEdit = () => {
const [issue, setIssue] = useState({effort: 5})
...
return (
...
<td>
<NumInput
name="effort"
value={issue.effort}
/>
</td>
...
*** Class Component ***
function format(num) {
return num != null ? num.toString() : '';
}
export default class NumInput extends React.Component {
constructor(props) {
super(props);
this.state = { value: format(props.value) };
this.onChange = this.onChange.bind(this);
}
onChange(e) {
if (e.target.value.match(/^\d*$/)) {
this.setState({ value: e.target.value });
}
}
render() {
const { value } = this.state;
return (
<input
type="text"
value={value}
onChange={this.onChange}
/>
);
}
}
*** Functional Component ***
export const NumInput = ({ value }) => {
const format = (num) => {
num != null ? num.toString() : ''
}
const [formattedValue, setFormattedValue] = useState(format(value))
const onChange = (e) => {
if (target.value.match(/^\d*$/)) setFormattedValue(target.value)
}
return (
<input
type="text"
value={formattedValue}
onChange={onChange}
/>
)
}
Is this just a typo ? onChange={onChangeValue} should be onChange={onChange}.
You named your function onChange in the functional component but are then invoking it with onChangeValue which doesnt exist.
Another problem as the author commented I'll edit so everyone can see:
const format = (num) => {
num != null ? num.toString() : ''
}
// what u meant
const format = (num) => {
return num != null ? num.toString() : ''
}

React not rerendering the component when prop changes

I have a picklist component that render some children based on the selected option. The problem comes when I render the same component in two options but with different props, because in that case, the component is not rerendered with the new props.
Let me clarify the problem: I have a picklist, I select option "A", then a text component is rendered below the picklist, I type "error" in that text field, then select option "B" in the picklist, then the other text field component disappear and another text field component is rendered just below the picklist. The last component should have been rendered empty, but the problem is that it contains the word "error".
Here's a minimized version of the code reproducing the error:
import React from "react";
class TextField extends React.Component {
constructor(props) {
super(props);
this.state = { value: props.value };
}
_onChange = (event) => {
this.setState({ value: event.currentTarget.value });
};
_handleBlur = (event) => {
this.setState({ value: event.currentTarget.value.trim() });
};
render() {
const { value } = this.state;
return (
<div>
<label>TextField</label>
<input
type="text"
onChange={this._onChange}
onBlur={this._handleBlur}
value={value}
/>
</div>
);
}
}
class Picklist extends React.Component {
constructor(props) {
super(props);
this.state = { selectedOption: "" };
this.options = ["Blank", "A", "B"];
}
// eslint-disable-next-line consistent-return
_handleSelectorCallback = (newOption) => {
this.setState({ selectedOption: newOption.currentTarget.value });
};
_renderChildren(selectedOption) {
if (!selectedOption) {
return null;
}
if (selectedOption === "A") {
return <TextField value="optionA"/>;
}
if (selectedOption === "B") {
return <TextField value="optionB"/>;
}
}
_renderOptions() {
const { selectedOption } = this.state;
return this.options.map((option) => (
<option value={option} selected={selectedOption === option}>
{option}
</option>
));
}
render() {
const { selectedOption } = this.state;
return (
<React.Fragment>
<div>
<label>Demo of the Error!</label>
<select
onChange={this._handleSelectorCallback}
value={selectedOption}
>
{this._renderOptions()}
</select>
</div>
{this._renderChildren(selectedOption)}
</React.Fragment>
);
}
}
export default Picklist;
Ignore how bad the component is written, is just to reproduce the error I'm having. Any idea why the component is not being rerendered with a new value?
I think what's happening is, when you switch from one <TextField> to the other, React is trying to be efficient by just passing different props to the same instance. You can tell React they are different and should be rerendered by adding a key:
_renderChildren(selectedOption) {
if (!selectedOption) {
return null;
}
if (selectedOption === "A") {
return <TextField key="A" value="optionA" />;
}
if (selectedOption === "B") {
return <TextField key="B" value="optionB" />;
}
}
alternatively, you could make your TextField a controlled component, which means it has no internal state, and the value/onChange fn are passed in as props. I edited your codesandbox to follow this pattern: https://codesandbox.io/s/empty-wind-43jq4?file=/src/App.js
I tried your sandbox. Your TextField component does not get unmounted so it's constructor gets called just once. Every change you make that has to do with that component, goes to it's componentDidUpdate hook.
So this is what you are missing:
componentDidUpdate() {
if (this.state.value !== this.props.value) {
this.setState({ value: this.props.value });
}
}

react input component gets store overwritten onChange

Ok, I'm new to react and mobx, and I'm experiencing some issues to manipulate the store.
When I'm typing at the input, the value gets overwritten for each char typed.
The component:
#withStore
#observer
class ConfigModel extends Component {
configModel;
constructor(props) {
super(props);
this.configModel = this.props.store.configModelStore;
}
render() {
const fieldsObj = this.configModel.modelConfig;
const fieldHelpers = this.configModel.helperModelStore.modelConfig;
const callbackOnChange = this.configModel;
const campos = merge(fieldHelpers, fieldsObj); // _.merge()
return (
<Form key={'configModelForm'}>
<>
{Object.entries(campos).map((campo) => {
if (campo[1].advanced) {
return;
}
if (campo[1].type === 'input') {
return (
<InputRender
key={campo[1].id}
field={campo[1]}
onChange={callbackOnChange.valueOnChange}
/>
);
}
})}
</>
</Form>
);
}
}
And my store define some observables (some options were omitted for simplicity, like the type evaluated at the component above):
#observable modelConfig = [{
id: 'postType',
value: '',
disabled: false,
advanced: false,
},
{
id: 'pluralName',
value: '',
disabled: false,
advanced: true,
},
...
]
And also define some actions:
#action valueOnChange = (e, {id, value}) => {
this.modelConfig.filter((config, index) => {
if (config.id === id) {
this.modelConfig[index].value = value;
console.log(this.modelConfig[index].value);
}
});
The console.log() above prints:
I truly believe that I'm forgetting some basic concept there, so can someone spot what am I doing wrong?
*EDIT:
I have another component and another store that is working correctly:
#observable name = '';
#action setName = (e) => {
this.name = e.target.value;
console.log(this.name);
}
So my question is:
Why the action that targets a specific value like this.name works fine and the action that targets a index generated value like this.modelConfig[index].value doesn't works?
The problem was at the <InputRender> component that was also receiving the #observable decorator. Just removed and it worked.
// #observer <---- REMOVED THIS
class InputRender extends Component {
render() {
const item = this.props.field;
return (
<InputField
id={item.id}
label={
<InfoLabel
label={item.label}
action={item.action}
content={item.popupContent}
/>
}
placeholder={item.placeholder}
onChange={this.props.onChange}
value={item.value}
disabled={item.disabled}
error={item.error}
throwError={item.throwError}
/>
);
}
}

Controlled Inputs, displayed values will not update to last digit

i'm using React(Typescript Version) to display some input inside a form.
The problem (as you can see from the image) is that when i update the values, from the setState function, values will not 'scroll' on the right
render() {
return(
<input
name={this.props.input.Name}
type={this.props.input.Type}
defaultValue={this.state.value}
ref={this._input}
key={key()}
)}
The function that updates the Value is a common set Function :
public set Value(data: string) {
this.setState({
internalValue: data,
inputError: !this.validateValue(data)
});
}
Note that the input works as expected if i write from the Keyboard, but if i write the input using a 'simulated' keyboard on screen happens what i just described
Any ideas?
Thank you
Update after simbathesailor support:
render() {
return(
<input
name={this.props.input.Name}
type={this.props.input.Type}
defaultValue={this.state.value}
ref={this._input}
key={key()}
onChange={this.setValue}
/>
)
}
componentDidUpdate(prevProps: InputProps, prevState: InputState) {
if (prevState.value!== this.state.value) {
this._input.current.focus();
}
}
setValue(event: React.ChangeEvent<HTMLInputElement>) {
console.log('change');
this.setState({
value: event.target.value
})
}
shouldComponentUpdate(nextProps: InputProps, nextState: InputState): boolean {
return (this.state.value!= nextState.value);
}
public set Value(data: string) {
this.setState({
value: data,
inputError: !this.validateValue(data)
}, () => {
this._input.current.focus();
});
}
You can use the refs and commit lifecycle method componentDidUpdate method. to achieve this.
In the example mentioned below, it is done for the uncontrolled component. But idea will remain same for controlled component also.
class Test extends React.Component {
constructor(props) {
super(props)
this.InputRef = React.createRef()
this.state = {
value: 0
}
}
setValue = (event) => {
this.setState({
value:event.target.value
})
}
update = () => {
this.setState({
value: (this.state.value || 0) + 1000
})
}
componentDidUpdate(prevProps, prevState) {
if(prevState.value !== this.state.value) {
this.InputRef.current.focus()
}
}
render() {
return (
<div>
<input
value={this.state.value}
onChange={this.setValue}
ref={this.InputRef}
/>
<button onClick={this.update}>update</button>
</div>
)
}
}
ReactDOM.render(<Test />, document.getElementById("root"))
Here is the codepen link to see it working:
Uncontrolled approach(javascript) codepen link
Controlled approach(javascript) codepen link
I have tried typescript for the first time. Thanks for your question :). Typescript is good. And here is your desired solution needed in typescript.
Codesandbox link(Typescript)

React state from HOC and component

I have a form with controlled inputs managed via a Higher Order Component. The structure is like this:
Field Higher Order Component
function BaseField(WrappedComponent) {
class WrappedField extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
active: false,
}
}
setValue = (e) => {
this.setState({ value: e.target.value })
}
...
<WrappedComponent
{...this.props}
value={this.state.value}
set={this.setValue}
active={this.state.active}
/>
....
Field
import React from 'react';
import BaseField from './BaseField';
const TextField = (props) => {
return <input
value={props.value}
onChange={props.set}
name={props.name}
type={props.type}
/>
}
export default BaseField(TextField);
When using TextField this works well - however, I want to use this with more flexibility - for example, I'd like to be able to enhance the onChange functionality in some cases, always having it set state but also have it do other things based on state or functions in the component the TextField is used in.
Am I misunderstanding how HOCs work?
You can use something like createChainedFunction from react-bootstrap:
function createChainedFunction(...funcs) {
return funcs
.filter(f => f != null)
.reduce((acc, f) => {
if (typeof f !== 'function') {
throw new Error('Invalid Argument Type, must only provide functions, undefined, or null.');
}
if (acc === null) {
return f;
}
return function chainedFunction(...args) {
acc.apply(this, args);
f.apply(this, args);
};
}, null);
}
and something from my react utils:
export function copyPropsWithout(props, without) {
const propKeys = Object.keys(props);
const passProps = propKeys.reduce((obj, propKey) => {
if (without.indexOf(propKey) === -1) {
obj[propKey] = props[propKey];
}
return obj;
}, {});
return passProps;
}
I'd add these to your utils and then use them like:
...
<WrappedComponent
{...copyPropsWithout(this.props, ['onChange'])}
value={this.state.value}
set={createChainedFunction(this.setValue, this.props.onChange}}
active={this.state.active}
/>
....

Resources