I am trying to capture all click events outside of my SearchBar component so that I can then tell the dropdown menu to close when one clicks out of it. I looked up examples of how to do this online and I need to use the global variable 'document' in javascript. However, it seems react native does not support this. Does anyone know a work around to use the 'document' variable or a react native equivalent?
class Products extends Component {
constructor(props) {
super(props);
this.setWrapperRef = this.setWrapperRef.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
}
setWrapperRef(node) {
this.wrapperRef = node;
}
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
alert('You clicked outside of me!');
}
}
componentWillMount() {
this.props.dispatch(getProductList());
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
render() {
const {isLoading, products} = this.props.products;
if (isLoading) {
return <Loader isVisible={true}/>;
}
return (
<View ref={this.setWrapperRef} style={styles.wrapper}>
<Header/>
<View style={styles.bodyWrapper}>
<ScrollView style={styles.scrollView}>
<ProductsContainer data={{productsList: { results: products }}}/>
</ScrollView>
<SearchBar style={styles.searchBar}/>
</View>
<Footer/>
</View>
);
}
}
function mapStateToProps(state) {
const {products} = state;
return {
products
};
}
export default connect(mapStateToProps)(Products);
You can't use document, it's an object on the window. The above answer is incorrect and hasn't taken into account this platform is React Native (answer has since been removed).
To handle click events, you you need to wrap everything in a TouchableWithoutFeedback.
<TouchableWithoutFeedback
onPress={this.hideSearchBar}
/>
I would add a zIndex style to the TouchableWithoutFeedback and one in styles.scrollView. Make sure the zIndex inside of styles.scrollView is more than the one you added to the TouchableWithoutFeedback.
Related
I'm building a React-Native app and trying to optimize it, i runned into the case of my Flatlist.
So this Flatlist basically renders few elements and each of these elements are selectable.
The issue i'm facing is that selecting one single item rerenders the whole Flatlist, and thus all items it contains.
I've seen a lot of solutions online already, and tried them without any success.
Here is my code :
Class component containing the Flatlist
const keyExtractor = (item) => item.id
export default class OrderedList extends Component {
state = {
selected: null,
}
onPressSelect = (id) => {
console.log(this.state.selected)
if(this.state.selected === id) {
this.setState({ selected: null})
}
else {
this.setState({ selected: id})
}
}
renderItemOrdered = ({item}) => {
const { group, wording, description, id: uniqueID } = item
const { id, name } = group
return (
<CategoryCard
type="ordered"
// item={item}
uniqueID={uniqueID}
groupName={name}
groupID={id}
description={description}
title={wording}
selected={this.state.selected}
onPressSelect={() => this.onPressSelect(item.id)}
/>
)
}
render() {
return (
<FlatList
initialNumToRender={10}
maxToRenderPerBatch={10}
data={this.props.data}
renderItem={this.renderItemOrdered}
keyExtractor={keyExtractor}
extraData={this.state.selected} ---> Tried with and without it
/>
)
}
}
Class component containing the renderItem method
export default class CategoryCard extends Component {
shouldComponentUpdate = (nextProps, nextState) => {
return nextProps.selected !== this.props.selected &&
nextProps.onPressSelect !== this.props.onPressSelect
}
render(){
if(this.props.type === 'ordered') {
return (
<Pressable style={this.props.selected === this.props.uniqueID ? styles.cardContainerSelected : styles.cardContainer} onPressIn={this.props.onPressSelect}>
<View style={[styles.cardHeader, backgroundTitleColor(this.props.groupID)]}>
<Text style={[styles.cardGroupName, textTitleColor(this.props.groupID)]}>{this.props.groupName}</Text>
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{this.props.wording}</Text>
<Text style={styles.cardDescription} numberOfLines={3} ellipsizeMode="tail">{this.props.description}</Text>
</View>
</Pressable>
)
}
}
}
What i already tried :
At first my components were functional components so i changed them into class components in order to make things works. Before that, i tried to use React.memo, also to manually add a function areEqual to it, to tell it when it should rerender, depending on props.
It didn't give me what i wanted.
I also tried to put all anonymous functions outside return statements, made use of useCallback, played around the ShouldComponentUpdate (like adding and removing all the props, the onPress prop, selected props)... None of that worked.
I must be missing something somewhere.. If you can help me with it, it would be a big help !
I'm trying to cascade a state change from a parent component down to a child.
export default class App extends React.Component {
constructor(props) {
super(props);
this.listUpdater = new_list_updater();
this.state = {
schoollist: this.listUpdater.update_list(),
}
}
listUpdateCallback = () => {
console.log("Callback triggered!");
console.log(this.state.schoollist.slice(1,3));
this.setState({schoollist: this.listUpdater.update_list()});
console.log(this.state.schoollist.slice(1,3));
}
render() {
return (
<SafeAreaView style={styles.container}>
<Header updateCallback={this.listUpdateCallback}/>
<SchoolList school_list={this.state.schoollist}/>
</SafeAreaView>
);
}
}
listUpdater.update_list() is a method in a class that implements getISchools and ShuffleArray and stores the list of schools that are being shuffled. It returns the shuffled list of schools.
import { shuffleArray } from './Shuffle'
import { getISchools } from './iSchoolData'
class ListUpdater{
constructor() {
console.log("Initiating class!");
this.currentSchoolList = [];
}
update_list() {
console.log("Updating list!");
if (this.currentSchoolList.length == 0){
this.currentSchoolList = getISchools();
}
else{
shuffleArray(this.currentSchoolList);
}
return(this.currentSchoolList);
}
}
export function new_list_updater(){
return new ListUpdater();
}
As far as I can tell everything works. When I press a refresh button in the Header component, it triggers the updateCallback, which updates the list stored in the state variable (verified by logging to console and ComponentDidUpdate()
This is the Component not refreshing:
import React from 'react';
import { StyleSheet, Text, View, SafeAreaView, FlatList } from 'react-native';
export default class SchoolList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<SafeAreaView style={styles.listArea}>
<FlatList
data = {this.props.school_list}
renderItem = {({item}) =>
<View style={styles.row}>
<View style={styles.num_area}>
<Text style={styles.num_text}>{item.key}</Text>
</View>
<View style={styles.text_area}>
<Text style={styles.univ_text}>{item.univ}</Text>
<Text style={styles.school_text}>{item.school}</Text>
</View>
</View>
}
/>
</SafeAreaView>
);
}
componentDidUpdate(){
console.log("SchooList Updated!");
}
}
The flow I'm expecting is:
Parent passes updateCallback reference to Header (child)
Refresh button in Header triggers updateCallback in Parent
updateCallback in Parent updates state with setState
Parent and relevant children that use state variable re-render, displaying new list
1-3 appear to be working, 4 is not!
Maybe your componenet is not re-rendering when you use setState for some reason. Try adding a warn in the render method to check this. I also noticed you are mutating the array this.currentSchoolList, winch is passade as reference for your state (all objects are passed as refence). Try replaceing this making a copy of the array beforing calling shuffleArray(this.currentSchoolList).
You can copy the array this way (this is ES6 sintax): newArray = [...oldArrray];
Or using other methods.
According to the docs on react-navigation you can call navigate from the top level component using the following:
import { NavigationActions } from 'react-navigation';
const AppNavigator = StackNavigator(SomeAppRouteConfigs);
class App extends React.Component {
someEvent() {
// call navigate for AppNavigator here:
this.navigator && this.navigator.dispatch(
NavigationActions.navigate({ routeName: someRouteName })
);
}
render() {
return (
<AppNavigator ref={nav => { this.navigator = nav; }} />
);
}
}
However I'm trying to figure out how can this be done if the logic that does the dispatch is in another component that is rendered on the same level as the navigator? In my case I create my navigators (A drawer with a stack navigator and other nested navigators) and then I render them using the <Drawer>. On the same level I'm loading my <PushController> component to handle push notifications. The pushcontroller actually gets the event that I want to dispatch on.
I can't figure out how to pass(?) the ref to the pushcontroller component so I can use it, currently the following isn't working. I get the console log telling me that the fcm.ACTION.OPEN_NOTIFICATION triggered but no dispatch occurs. I suppose it could be because the ref is created during a render and it isn't available to pass yet when the render occurs? But I'm also not sure you would do things this way in order to give another component access to a ref declared at the same level. Thanks for your help in advance!
Drawer + PushController rendering
render(){
return(
<View style={{flex: 1}}>
<Drawer ref={nav => { this.navigator = nav; }}/>
<PushController user={this.props.user} navigator={this.navigator}/>
</View>
)
}
PushController snippet:
import { NavigationActions } from 'react-navigation';
async doFCM() {
FCM.getInitialNotification().then(notif => {
console.log('Initial Notification', notif);
if(notif.fcm.action === "fcm.ACTION.OPEN_NOTIFICATION"){
console.log('fcm.ACTION.OPEN_NOTIFICATION triggered', notif);
this.props.navigator && this.props.navigator.dispatch(NavigationActions.navigate({routename: 'Chat'}))
}
});
}
Answer was to move the navigate call to a function defined at render and pass it to the component.
import { NavigationActions } from 'react-navigation';
...
//class definition omitted for brevity
render(){
const callNavigate = (routeName, params) => {
console.log('Params', params);
this.navigator.dispatch({
type: NavigationActions.NAVIGATE,
routeName: routeName,
params: params
})
}
return(
<View style={{flex: 1}}>
<Drawer ref={nav => this.navigator = nav }/>
<PushController callNavigate={callNavigate}/>
</View>
)
}
The function is called within PushController like this:
this.props.callNavigate('RouteName', params);
I am new to React-native and enzyme, I am trying to create a custom component here.
I will be displaying an Image based on this.props.hasIcon. I set default props value for hasIcon as true. When I check Image exists in enzyme ShallowWrapper. I am getting false.
tlProgress.js
class TLProgress extends Component {
render() {
return (
<View style={styles.container}>
{this.renderImage}
{this.renderProgress}
</View>
);
}
}
TLProgress.defaultProps = {
icon: require("./../../img/logo.png"),
indeterminate: true,
progressColor: Colors.TLColorAccent,
hasIcon: true,
progressType: "bar"
};
and renderImage() has the Image
renderImage() {
if (this.props.hasIcon) {
return <Image style={styles.logoStyle} source={this.props.icon} />;
}
}
Now, If I check Image exists in enzyme am getting false.
tlProgress.test.js
describe("tlProgress rendering ", () => {
let wrapper;
beforeAll(() => {
props = { indeterminate: false };
wrapper = shallow(<TLProgress {...props} />);
});
it("check progress has app icon", () => {
expect(wrapper.find("Image").exists()).toBe(true); // fails here..
});
});
You are not calling the renderImage function in your render() -- you forgot the brackets, thus it is being interpreted as an undefined variable.
It should be: (I am assuming you want to call renderProgress() and not renderProgress as well)
class TLProgress extends Component {
render() {
return (
<View style={styles.container}>
{this.renderImage()}
{this.renderProgress()}
</View>
);
}
You're searching for a tag called 'Image' instead of looking for your component Image
It should be :
import Image from '../Image';
wrapper.find(Image) //do your assert here
I am trying to access this.props in the clicked() method, but they are undefined. How can I access this.props through the methods in the ListItemExample class?
My goal is to maintain the id into the show view that shows the detail view for a clicked ListItemExample.
export default class Example extends Component {
constructor() {
super();
let ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
dataSource: ds,
isLoading: true
};
}
componentDidMount() {
this.fetchData()
}
fetchData() {
Api.getPosts().then((resp) => {
console.log(resp);
this.setState({
dataSource: this.state.dataSource.cloneWithRows(resp.posts),
isLoading: false
})
});
}
render() {
return (
<View style={styles.container}>
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderRow.bind(this)}
style={styles.postList}
/>
</View>
);
}
renderRow(post) {
return (
<PostListItem
id={post.id}
coverImage={post.cover_image}
title={post.title}
lockedStatus={post.status}
time={post.time} />
);
}
}
export default class ListItemExample extends Component {
constructor() {
super();
}
render() {
return (
<TouchableHighlight onPress={this.clicked} >
<View style={styles.postItem}>
</View>
</TouchableHighlight>
);
}
clicked() {
console.log("clicked props");
console.log(this.props);
Actions.openPost();
}
}
You need to do onPress={this.clicked.bind(this)}
With the shift of React from createClass to ES6 classes we need to handle the correct value of this to our methods on our own, as mentioned here: http://www.newmediacampaigns.com/blog/refactoring-react-components-to-es6-classes
Change your code to have the method bounded to correct value of this in constructor using this.clicked = this.clicked.bind(this) in your constructor
The no autobinding was a deliberate step from React guys for ES6 classes. Autobinding to correct context was provided with React.createClass. Details of this can be found here: https://facebook.github.io/react/blog/2015/01/27/react-v0.13.0-beta-1.html#autobinding
So based on this you could also change your clicked method as:
export default class ListItemExample extends Component {
constructor(props) {
super(props);
}
render() {
return (
<TouchableHighlight onPress={this.clicked} >
<View style={styles.postItem}>
</View>
</TouchableHighlight>
);
}
clicked = () => {
console.log("clicked props");
console.log(this.props);
Actions.openPost();
}
}
Add bind(this) to binding your method into current component, like
yourMethod(){
console.log(this.props)
}
<SomeComponent onClick={this.yourMethod.bind(this)} />
I had same issue when using FBSDK with React Native. And #fandro answer almost did explain a lot.
I had this:
render() {
const infoRequest = new GraphRequest(
'/me',
null,
this.responseInfoCallback,
);
return (
<View>
<LoginButton
readPermissions={["email"]}
onLoginFinished={
(error, result) => {
if (error) {
console.log("Login failed with error: " + result.error);
} else if (result.isCancelled) {
console.log("Login was cancelled");
} else {
new GraphRequestManager().addRequest(infoRequest).start(1000);
console.log("Login was successful with permissions: " + result.grantedPermissions)
}
}
}
onLogoutFinished={() => alert("User logged out")}/>
</View>
);
}
responseInfoCallback(error, result) {
if (error) {
alert('Error fetching data: ' + error.toString());
} else {
alert('Success fetching data: ' + result.toString());
this.props.onLoginSuccess();
}
}
And call of this.props.onLoginSuccess() caused issue "undefined is not an object evaluating this.props.onLoginSuccess()". So when I changed the code in const infoRequest = new GraphRequest(...) to include .bind(this):
const infoRequest = new GraphRequest(
'/me',
null,
this.responseInfoCallback.bind(this),
);
It worked. Hope this helps.
You can use arrow functions for your handlers and bind to the lexical scope automagically. ... or go down the traditional .bind(this) at invocation time or in your constructor. But note, as I think one answer uses this approach: you must change your babel settings and ensure you have "optional": ["es7.classProperties"] for arrow functions in classes to work properly.
In then compnent that you're trying enter to another view you must send the object navigation by you use this in the onPress of component imported
example
Implementing component in a view
<CardComponent title="bla bla" navigation={this.props.navigation} />
Component template
`<View>
<Button title={this.props.title} onPress={()=>
this.props.navigation.navigate("anotherAwesomeView")}/>
</View>`
This problem is because the component that you're trying implement is not defined on stackNavigation, and by this the methot navigation is not avalible for you, and passing this object navigator by params you'll can access to it