I have created a form which consists of two drop down list fields (amongst other form elements), using React Native Modal Dropdown plugin. The two dropdowns are:
Country List
States list (filtered by the what country that was selected above)
I am currently having a problem with the States Dropdown, which recieves a countryId value from state that is set when a item is selected from the the Country Drop Down. This is then used to pass to my State Drop Down.
It's worth noting that the two dropdown's I have listed above have been seperated into components for reusability.
Code for Form:
static propTypes = {
navigation: PropTypes.object
};
state = {
countryId: 0,
stateId: 0
}
componentWillMount() {
}
onCountryDropDownSelected = (val) => {
this.setState({ countryId: val });
}
onStateDropDownSelected = (val) => {
this.setState({ stateId: val });
}
render() {
return (
<Container>
<StatusBar barStyle="default" />
<CountryDropDownList onSelect={this.onCountryDropDownSelected} />
<Text>Country: {this.state.countryId}</Text>
<StateDropDownList onSelect={this.onStateDropDownSelected} countryId={this.state.countryId} />
</Container>
);
}
Code for StateDropDownList component:
class StateDropDownList extends Component {
static propTypes = {
countryId: PropTypes.number,
onSelect: PropTypes.func
};
state = {
data: [],
errorMessage: '',
isLoading: false
}
componentWillReceiveProps(nextProps) {
if (nextProps.countryId != undefined) {
return this.populatePicker(nextProps.countryId);
}
}
populatePicker(countryId) {
// This method fetches data from the API to store in the 'data' state object.
}
dropDownRenderRow(rowData, rowID, highlighted) {
let evenRow = rowID % 2;
return (
<TouchableHighlight underlayColor='cornflowerblue'>
<View style={{backgroundColor: evenRow ? 'lemonchiffon' : 'white'}}>
<Text style={{color: 'black'}}>
{rowData.name}
</Text>
</View>
</TouchableHighlight>
);
}
dropDownRenderButtonText(rowData) {
console.log('dropDownRenderButtonText', rowData);
return rowData.name;
}
dropDownRenderSeparator(sectionID, rowID, adjacentRowHighlighted) {
return (<View key={rowID} />);
}
dropDownOnSelect(rowID, rowData) {
// Get the selected value and pass to parent component through onSelect() event in parent.
this.props.onSelect(rowData.id);
}
render() {
return (
<View>
<Text>Home State</Text>
{this.state.data && this.state.data.length > 0 ?
<ModalDropdown defaultValue='Select one'
style={{flex:1}, {marginTop: 10}}
options={this.state.data}
onSelect={(rowID, rowData) => this.dropDownOnSelect(rowID, rowData)}
renderButtonText={(rowData) => this.dropDownRenderButtonText(rowData)}
renderRow={this.dropDownRenderRow.bind(this)}
renderSeparator={(sectionID, rowID, adjacentRowHighlighted) => this.dropDownRenderSeparator(sectionID, rowID, adjacentRowHighlighted)} />
:
<Text>No regions for country.</Text>
}
</View>
);
}
}
What I am noticing is that the 'componentWillReceiveProps' function is stopping my dropdown from having a selected value. But unfortunately, I need this function in order for the props to update when passing in the countryId value from the parent.
When I remove the this.props.onSelect(rowData.id); line in this dropDownOnSelect() function, the dropdown value gets set correctly. I guess this is the case, since I am not setting an prop value.
I can see what the issue is, but not a way to get around it.
Any help is appreciated!
I resolved the problem. Made a simple error to check if the nextProps.countryId is not the same as props.countryId, which now makes sense:
componentWillReceiveProps(nextProps) {
if (nextProps.countryId && nextProps.countryId != this.props.countryId) {
return this.populatePicker(nextProps.countryId);
}
}
Related
What I'm trying to achieve:
I have many forms and I want to keep form validation logic in my HOC so that I don't have to repeat validation logic as most forms would have same fields and some extra or less fields.
How I have Implemented:
Learning to create HOC, followed this example HOC example and tried to create an HOC like below.
import React from 'react';
import {
spaceCheck,
specialCharacterCheck,
numberValidator
} from '../../utility/validators';
const fieldHOC = (WrappedComponent) => {
class HOC extends React.Component {
state = {
error: {
name_first: {
fieldType: 'name_first',
errorType: 0
},
name_last: {
fieldType: 'name_last',
errorType: 0
},
email: {
fieldType: 'email',
errorType: 0
}
}
};
getErrorMessage = (fieldType, errorType) => {
this.setState({
error: {
...this.state.error,
[fieldType]: {
...this.state.error[fieldType],
errorType
}
}
});
};
checkFieldsError = (currentFocus, nextFocus) => {
//Not able to get props passed by below component in class functions
console.log('MY PROPS', this.props);
const field = this.props[currentFocus];
if (field === '' || spaceCheck(field)) {
this.getErrorMessage(currentFocus, 1);
} else if (specialCharacterCheck(field)) {
this.getErrorMessage(currentFocus, 2);
} else if (numberValidator(field) || numberValidator(field)) {
this.getErrorMessage(currentFocus, 3);
} else {
this.setState({
error: {
...this.state.error,
[currentFocus]: {
...this.state.error[currentFocus],
errorType: 0
}
}
});
}
this[nextFocus].focus();
}
render() {
const { children } = this.props;
// Here able to access props(name_first, name_last and email) passed from below component
// console.log('PROPS', this.props);
return (
<WrappedComponent
{...this.props}
error={this.state.error}
checkFieldsError={this.checkFieldsError}
>
{children}
</WrappedComponent>
);
}
}
return HOC;
};
export default fieldHOC;
Component in which I'm using this HOC is
const FieldValidation = fieldHOC(View);
class Account extends Component {
//Some class functions
render() {
const { spaceBottom, error } = this.state;
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
keyboardShouldPersistTaps="handled"
behavior={Platform.OS === 'ios' ? 'padding' : null}
>
<KeyboardAwareScrollView
keyboardShouldPersistTaps="handled"
alwaysBounceVertical={false}
contentInset={{ bottom: 0 }}
>
<FieldValidation
name_first={this.state.name_first}
name_last={this.state.name_last}
email={this.state.email}
{...this.props}
>
<View
style={[
styles.UserContainer,
CommonStyle.shadowStyle
]}
>
<Text style={styles.headingTextStyle}>
Account Details
</Text>
<FormTextInputComponent
{...testID('first_name')}
errorType={this.props.error.name_first.errorType}
onChangeText={this.handleTextChange('name_first')}
textInputRef={ref => {this.name_first = ref;}}
autoCapitalize="none"
spellCheck={false}
autoCorrect={false}
blurOnSubmit={false}
onSubmitEditing={() => {
this.props.checkFieldsError('name_first', 'name_last');
}}
/>
{this.props.error.name_first.errorType ?
(
<ErrorMessage textColor="#EA2027" error={error.name_first} />
)
: null}
//Last part
export default fieldHOC(connect(mapStateToProps)(Account));
In the above component, I'm trying to call the validation function written in HOC which is checkFieldsError.
The problem which I'm facing is that the props passed in <FieldValidation like name_first are accessible in HOC's render function but same props are not accessible in the class functions of HOC.
Most probably what I tried to do is an antipattern in React(my guess). Can someone can please help me in figuring out the problem and the proper way to do it?
Edit: Sample implemented in codesandbox Example
Here is a codeSandBox example I have created with what you are trying to achieve, you will notice that inside the HOC I try to access the props in its functions, also check the console to see the console logs, please check the code and follow the same example. You will achieve the same results.
I have a <Select> component from react-select renders a couple options to a dropdown, these options are fetched from an api call, mapped over, and the names are displayed. When I select an option from the dropdown the selected name does not appear in the box. It seems that my handleChange method is not firing and this is where I update the value of the schema name:
handleChange = value => {
// this is going to call setFieldValue and manually update values.dataSchemas
this.props.onChange("schemas", value);
This is not updating the value seen in the dropdown after something is selected.
I'm not sure if I'm passing the right thing to the value prop inside the component itself
class MySelect extends React.Component {
constructor(props) {
super(props);
this.state = {
schemas: [],
fields: [],
selectorField: ""
};
this.handleChange = this.handleChange.bind(this);
}
componentDidMount() {
axios.get("/dataschemas").then(response => {
this.setState({
schemas: response.data.data
});
console.log(this.state.schemas);
});
}
handleChange = value => {
// this is going to call setFieldValue and manually update values.dataSchemas
this.props.onChange("schemas", value);
const schema = this.state.schemas.find(
schema => schema.name === value.target.value
);
if (schema) {
axios.get("/dataschemas/2147483602").then(response => {
this.setState({
fields: response.data.fields
});
console.log(this.state.fields);
});
}
};
updateSelectorField = e => {
this.setState({ selectorField: e.target.value });
};
handleBlur = () => {
// this is going to call setFieldTouched and manually update touched.dataSchemas
this.props.onBlur("schemas", true);
};
render() {
return (
<div style={{ margin: "1rem 0" }}>
<label htmlFor="color">
DataSchemas -- triggers the handle change api call - (select 1){" "}
</label>
<Select
id="color"
options={this.state.schemas}
isMulti={false}
value={this.state.schemas.find(
({ name }) => name === this.state.name
)}
getOptionLabel={({ name }) => name}
onChange={this.handleChange}
onBlur={this.handleBlur}
/>
{!!this.props.error && this.props.touched && (
<div style={{ color: "red", marginTop: ".5rem" }}>
{this.props.error}
</div>
)}
</div>
);
}
}
I have linked an example showing this issue.
In your handleChange function you are trying to access value.target.value. If you console.log(value) at the top of the function, you will get:
{
id: "2147483603"
selfUri: "/dataschemas/2147483603"
name: "Book Data"
}
This is the value that handChange is invoked with. Use value.name instead of value.target.value.
I am trying to set an Int value from the state inside a <TextInput> by first turning it into string (<TextInput> can only receive a string) and then I want to be able to change the value and update the state value with the new <TextInput/> value. When I am trying to change the value inside the <TextInput/>
I get error:
undefined is not an object (evaluating 'this.state.keys.toString')
UPDATE:
I removed the this from this.keyInt and now I receive NaN on input update , the error is gone though
React-Native code:
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
keys: 0,
id: this.props.id
};
}
updateKeysWithInputHandler = (e) => {
keyInt = parseInt(e.target.value);
console.log(`keyInt is: ${keyInt}`);
this.setState({
keys: keyInt
})
}
render() {
return (
<View style={styles.container}>
<TextInput
id={this.props.id}
style={styles.title}
keyboardType='numeric'
maxLength={2}
value={this.state.keys.toString()}
onChange={this.updateKeysWithInputHandler}
/>
</View>
);
}
Ok I fixed my problem thanks to this post and the help from Adam Azad which point me to my problem.
apparently react-native <TextInput/> dont use target and instead its use nativeEvent.text so I changed my code and it worked now.
Working code:
updateKeysWithInputHandler = (val) => {
keyInt = parseInt(val);
console.log(val);
this.setState({
keys: keyInt
})
}
render() {
return (
<View style={styles.container}>
<TextInput
id={this.props.id}
style={styles.title}
keyboardType='numeric'
maxLength={2}
value={this.state.keys.toString()}
onChange={(event) => this.updateKeysWithInputHandler(event.nativeEvent.text)}
/>
</View>
);
}
}
Why keys: this.keyInt?
Shouldn't it be keys: keyInt?
this.setState({
keys: this.keyInt
// ___^^^^^
})
Remove the this keyword because it changes the context from the current scope to Counter scope, where keyInt variable is not defined.
updateKeysWithInputHandler = (event) => {
if (isNaN(event.target.value) || event.target.value.toString().length===
0) {
event.target.value = 0;
}
this.setState({
keys: parseInt(event.target.value)
})
}
I'm displaying an array of items (Circle components) in my view,
but I'm not sure how to hide all the others when I click one (onPress is set up to zoom in and give more info cards about that one Circle I click).
Here is my RN code .. any ideas?
class Circle extends Component {
constructor(props) {
super(props);
this.state = { opened: false};
this.onPress = this.onPress.bind(this);
}
onPress() {
this.props.onPress();
if (this.props.noAnimation) {
return;
}
if (this.state.opened) {
this.refs.nextEpisode.fadeOutLeft();
this.refs.description.fadeOutDownBig();
return this.setState({opened: false, windowPos: null});
}
this.refs.container.measureInWindow((x, y) => {
this.refs.nextEpisode.slideInLeft();
setTimeout(() => this.refs.description.fadeInUpBig(), 400);
this.setState({
opened: true,
windowPos: { x, y },
});
});
}
render() {
const data = this.props.data;
const { opened } = this.state;
const styles2 = getStyles(opened, (this.props.index % 2 === 0));
const containerPos = this.state.windowPos ? {
top: - this.state.windowPos.y + 64
} : {};
return (
<TouchableWithoutFeedback onPress={this.onPress}>
<View style={[styles2.container, containerPos]} ref="container" >
<TvShowImage tvShow={data} style={styles2.image} noShadow={opened} openStatus={opened}/>
<View style={styles2.title}>
<Title
tvShow={data}
onPress={this.onPress}
noShadow={opened}
/>
<Animatable.View
style={styles2.animatableNextEpisode}
duration={800}
ref="nextEpisode"
>
<NextEpisode tvShow={data}/>
</Animatable.View>
</View>
<Animatable.View
style={styles2.description}
duration={800}
ref="description"
delay={400}
>
<Text style={styles2.descriptionText}>{data.item_description}</Text>
</Animatable.View>
</View>
</TouchableWithoutFeedback>
);
}
}
Circle.defaultProps = {
index: 0,
onPress: () => {}
};
(Please ignore that some of variable names are tv-show related when the photos are food-related, haven't had time to fix yet).
FYI the parent component that maps the array of Circle components looks like this:
class Favorites extends React.Component {
constructor(props) {
super(props);
this.state = {
circleCount: 0
};
this.addCircle = this.addCircle.bind(this);
}
componentDidMount() {
for(let i = 0; i < this.props.screenProps.appstate.length; i++) {
setTimeout(() => {
this.addCircle();
}, (i*100));
}
}
addCircle = () => {
this.setState((prevState) => ({circleCount: prevState.circleCount + 1}));
}
render() {
var favoritesList = this.props.screenProps.appstate;
var circles = [];
for (let i = 0; i < this.state.circleCount; i++) {
circles.push(
<Circle key={favoritesList[i].url} style={styles.testcontainer} data={favoritesList[i]}>
</Circle>
);
}
return (
<ScrollView style={{backgroundColor: '#fff9f9'}}>
<View style={styles.favoritesMainView}>
<View style={styles.circleContainer}>
{circles}
</View>
</View>
</ScrollView>
);
}
}
Off the top of my head (if I'm understanding what you want) there could be few solutions.
You may need to track the "selected" circle and style the components based on that. For instance you could style the ones not selected by using {height: 0} or {opacity: 0} if you still need the height of the elements.
To track I would try the following:
In Favorites state:
this.state = {
circleCount: 0,
selected: -1
};
And pass 3 new values to circle, the third is function to change the state of "selected":
<Circle
key={favoritesList[i].url}
style={styles.testcontainer}
data={favoritesList[i]}
index={i}
selected={this.state.selected}
onClick={(index) => {
this.setState(prevState => {
return { ...prevState, selected: index }
});
}}
/>
In Circle we use pass the index that was pressed back up to Favorites using the method we passed down:
onPress() {
this.props.onClick(this.props.index);
...
In Circle's render method we create an opacity style to hide any elements not currently selected (but only if there is one selected - meaning that if it is -1 then none are selected and no opacity should be applied to any Circle:
render() {
const { selected, index } = this.props;
let opacityStyle = {};
if(selected !== -1) {
if(selected !== index) {
opacityStyle = { opacity: 0 }
}
}
Last step is to apply the style (either an empty object, or opacity: 0):
<TouchableWithoutFeedback onPress={this.onPress}>
<View style={[styles2.container, containerPos, opacityStyle]} ref="container" >
I am not sure where you are closing or zooming out. Just when you close or zoom out of a circle you just need to call this.props.onClick(-1) to effectively deselect the circle. This will mean no other circles will have the opacity applied.
One thing you may need to do to ensure "selected" is not removed, is change your setState() method in Favorites:
addCircle = () => {
this.setState(prevState => {
return { ...prevState, circleCount: prevState.circleCount + 1 };
}
)}
In this version we are only changing circleCount and leaving any other properties that prevState has - in this case "selected" remains unchanged.
I have a button on the homescreen which toggles the text in the AlertBar.
So when I press a Button, the text in AlertBar should change according to the state isParked. Currently when I press the button, nothing happens... and I'm unsure why.
Here's my homescreen:
class Home extends Component {
constructor(props) {
super(props);
this.state = {
isParked: false
};
}
pressPark = () => this.setState({isParked:true})
render() {
console.ignoredYellowBox = ['Remote debugger'];
return (
<View>
<View>
<AlertBar isParked={this.state.isParked}/>
</View>
<View style={styles.parkButton}>
<Button title='PARK' onPress={this.pressPark} color='green'/>
</View>
</View>
);
}
}
Here's my AlertBar.js:
class AlertBar extends Component {
state = {
region: 'Singapore',
isParked: this.props.isParked,
alertText: null
}
... some unrelated code ...
componentDidMount() {
if (this.state.isParked === false) {
this.setState({alertText: "You're parking at"})} else if (this.state.isParked === true) {
this.setState({alertText: "You're parked at"})}
alert(this.state.alertText)
}
componentWillUnmount() {
// some unrelated code
}
render() {
... some unrelated code...
return(
<View style={styles.container}>
<Text style={styles.welcomeText}>
{this.state.alertText}
</Text>
<Text style={styles.locationText}>
{this.state.region}
</Text>
</View>
)
}
}
Am I doing this wrong? I can't tell what's wrong.... Please help! Thanks!
Use
if (this.props.isParked === false)
Instead of
if (this.state.isParked === false)
(and dont transfer props to state directly, this make no sense anyway :))
At this point, your AlertBar component is not handling any prop changes.
What you'll need to do, is map your props to the state whenever an update is received.
Add this line of code in your AlertBar.js and it will map isParked to state whenever it receives an update.
componentWillReceiveProps(props) {
this.setState({ isParked: props.isParked });
}