How to use react-native multiple checkbox and get the selected Id - react-native

i have used react-native-select-multiple. but not working, How do i use multiple checkbox in react native and get the select multiple id and pass to next page..?

this is how to use the library, get selected items, pass the items selected and get the items on the next page.
const fruits = ['Apples', 'Oranges', 'Pears', 'banana']
this.state = {
selectedFruits: [],
}
onSelectionsChange = (selectedFruits) => {
// selectedFruits is array of { label, value }
this.setState({
selectedFruits
})
}
<
SelectMultiple
items = {
fruits
}
rowStyle = {
{
backgroundColor: 'transparent'
}
}
labelStyle = {
{
color: 'white'
}
}
checkboxStyle = {
{
borderColor: 'white',
backgroundColor: 'white'
}
}
style = {
{
borderWidth: 1,
borderColor: 'white',
width: setWidth,
height: setHeight
}
}
selectedItems = {
this.state.selectedFruits
}
onSelectionsChange = {
this.onSelectionsChange
}
/>
navigate('Screen', {
fruits: this.state.selectedFruits
} //using react-navigation. or Actions.Screen({fruits: this.state.selectedFruits})//using react-native-router-flux
this.props.navigation.state.params.fruits //using react-navigation or this.props.fruits//using react-native-router-flux

Related

React Native Table with Row Selection

I am trying to create a React Native screen that allows the user to select which items to send to the server for batch processing.
My thought was to have a table, and allow the user to select the rows they want, then click a button to submit to the server.
I need the state to contain a list of the ids from those rows, so that I can use it to allow the user to send a request with that array of ids.
A mock-up of my code is below, but it doesn't work. When the update of state is in place, I get an error of "selected items is not an object". When it is commented out, of course the state update doesn't work, but it also doesn't set the value of the checkbox from the array if I hard code it in the state initialization (meaning is 70 is in the array, the box is still not checked by default), and it does allow the box to get checked but not unchecked. How do I get it working?
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import CheckBox from '#react-native-community/checkbox';
import { Table, Row, TableWrapper, Cell } from 'react-native-table-component';
import moment from 'moment';
class FruitGrid extends Component {
constructor(props) {
super(props);
}
state = {
selectedItems : [70],
data: []
};
refresh() {
let rows = [
[69,'David','Apples'],
[70,'Teddy','Oranges'],
[73,'John','Pears']
];
this.setState({data: rows});
}
componentDidMount() {
this.refresh();
}
setSelection(id) {
const { selectedItems } = this.state;
if (id in selectedItems)
{
this.setState({selectedItems: selectedItems.filter(i => i != id)});
}
else
{
this.setState({selectedItems : selectedItems.push(id)});
}
}
render() {
const { selectedItems, data } = this.state;
let columns = ['',
'Person',
'Fruit'];
return (
<View style={{ flex: 1 }}>
<Table borderStyle={{borderWidth: 2, borderColor: '#c8e1ff'}}>
<Row data = {columns} />
{
data.map((rowData, index) =>
(
<TableWrapper key={index} style={styles.row}>
<Cell key={0} data = {<CheckBox value={rowData[0] in selectedItems} onValueChange={this.setSelection(rowData[0])} />} />
<Cell key={1} data = {rowData[1]} textStyle={styles.text}/>
<Cell key={2} data = {rowData[2]} textStyle={styles.text}/>
</TableWrapper>
)
)
}
</Table>
</View>
);
}
}
export default FruitGrid;
const styles = StyleSheet.create({
btn: { width: 58, height: 18, backgroundColor: '#8bbaf2', borderRadius: 2 },
btnText: { textAlign: 'center', color: '#000000' },
text: { margin: 6 },
row: { flexDirection: 'row' },
});
I introduced my own module for this feature from which you will be able to select row easily and much more.
You can use this component like this below
import DataTable, {COL_TYPES} from 'react-native-datatable-component';
const SomeCom = () => {
//You can pass COL_TYPES.CHECK_BOX Column's value in true/false, by default it will be false means checkBox will be uncheck!
const data = [
{ menu: 'Chicken Biryani', select: false }, //If user select this row then this whole object will return to you with select true in this case
{ menu: 'Chiken koofta', select: true },
{ menu: 'Chicken sharwma', select: false }
]
const nameOfCols = ['menu', 'select'];
return(
<DataTable
onRowSelect={(row) => {console.log('ROW => ',row)}}
data={data}
colNames={nameOfCols}
colSettings={[{name: 'select', type: COL_TYPES.CHECK_BOX}]}
/>
)
}
export default SomeCom;
React Native DataTable Component
Thanks to a friend, I found 3 issues in the code that were causing the problem:
When checking to see if the array contains the object, I first need to check that the array is an array and contains items. New check (wrapped in a function for reuse):
checkIfChecked(id, selectedItems)
{
return selectedItems?.length && selectedItems.includes(id);
}
The state update was modifying the state without copying. New state update function:
setSelection(id) {
const { selectedItems } = this.state;
if (this.checkIfChecked(id,selectedItems))
{
this.setState({selectedItems: selectedItems.filter(i => i != id)});
}
else
{
let selectedItemsCopy = [...selectedItems]
selectedItemsCopy.push(id)
this.setState({selectedItems : selectedItemsCopy});
}
}
The onValueChange needed ()=> to prevent immediate triggering, which lead to a "Maximum Depth Reached" error. New version
onValueChange={()=>this.setSelection(rowData[0])} />}
The full working code is here:
import React, { Component } from 'react';
import { StyleSheet, View } from 'react-native';
import CheckBox from '#react-native-community/checkbox';
import { Table, Row, TableWrapper, Cell } from 'react-native-table-component';
import moment from 'moment';
class FruitGrid extends Component {
constructor(props) {
super(props);
}
state = {
selectedItems : [],
data: []
};
refresh() {
let rows = [
[69,'David','Apples'],
[70,'Teddy','Oranges'],
[73,'John','Pears']
];
this.setState({data: rows});
}
componentDidMount() {
this.refresh();
}
setSelection(id) {
const { selectedItems } = this.state;
if (this.checkIfChecked(id,selectedItems))
{
this.setState({selectedItems: selectedItems.filter(i => i != id)});
}
else
{
let selectedItemsCopy = [...selectedItems]
selectedItemsCopy.push(id)
this.setState({selectedItems : selectedItemsCopy});
}
}
checkIfChecked(id, selectedItems)
{
return selectedItems?.length && selectedItems.includes(id);
}
render() {
const { selectedItems, data } = this.state;
let columns = ['',
'Person',
'Fruit'];
return (
<View style={{ flex: 1 }}>
<Table borderStyle={{borderWidth: 2, borderColor: '#c8e1ff'}}>
<Row data = {columns} />
{
data.map((rowData, index) =>
(
<TableWrapper key={index} style={styles.row}>
<Cell key={0} data = {<CheckBox value={this.checkIfChecked(rowData[0],selectedItems)} onValueChange={()=>this.setSelection(rowData[0])} />} />
<Cell key={1} data = {rowData[1]} textStyle={styles.text}/>
<Cell key={2} data = {rowData[2]} textStyle={styles.text}/>
</TableWrapper>
)
)
}
</Table>
</View>
);
}
}
export default FruitGrid;
const styles = StyleSheet.create({
btn: { width: 58, height: 18, backgroundColor: '#8bbaf2', borderRadius: 2 },
btnText: { textAlign: 'center', color: '#000000' },
text: { margin: 6 },
row: { flexDirection: 'row' },
});

Detox: detect that element was displayed

We have a toast component in our app that is adding considerable flakiness to our tests. The toast component displays an animated View for 4s and then disappears. In a lot of tests I need to check what the message content is in order to continue with the test.
The toast component is implemented with the following code:
// #flow
import * as React from "react"
import { StyleSheet, View, Animated, Dimensions, Text } from "react-native"
import type {
TextStyle,
ViewStyle,
} from "react-native/Libraries/StyleSheet/StyleSheet"
import type AnimatedValue from "react-native/Libraries/Animated/src/nodes/AnimatedValue"
import type { CompositeAnimation } from "react-native/Libraries/Animated/src/AnimatedImplementation"
import { AnimationConstants } from "constants/animations"
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 0,
right: 0,
elevation: 999,
alignItems: "center",
zIndex: 10000,
},
content: {
backgroundColor: "black",
borderRadius: 5,
padding: 10,
},
text: {
color: "white",
},
})
type Props = {
style: ViewStyle,
position: "top" | "center" | "bottom",
textStyle: TextStyle,
positionValue: number,
fadeInDuration: number,
fadeOutDuration: number,
opacity: number,
}
type State = {
isShown: boolean,
text: string | React.Node,
opacityValue: AnimatedValue,
}
export const DURATION = AnimationConstants.durationShort
const { height } = Dimensions.get("window")
export default class Toast extends React.PureComponent<Props, State> {
static defaultProps = {
position: "bottom",
textStyle: styles.text,
positionValue: 120,
fadeInDuration: AnimationConstants.fadeInDuration,
fadeOutDuration: AnimationConstants.fadeOutDuration,
opacity: 1,
}
isShown: boolean
duration: number
callback: Function
animation: CompositeAnimation
timer: TimeoutID
constructor(props: Props) {
super(props)
this.state = {
isShown: false,
text: "",
opacityValue: new Animated.Value(this.props.opacity),
}
}
show(text: string | React.Node, duration: number, callback: Function) {
this.duration = typeof duration === "number" ? duration : DURATION
this.callback = callback
this.setState({
isShown: true,
text: text,
})
this.animation = Animated.timing(this.state.opacityValue, {
toValue: this.props.opacity,
duration: this.props.fadeInDuration,
useNativeDriver: true,
})
this.animation.start(() => {
this.isShown = true
this.close()
})
}
close(duration?: number) {
const delay = typeof duration === "undefined" ? this.duration : duration
if (!this.isShown && !this.state.isShown) return
this.timer && clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.animation = Animated.timing(this.state.opacityValue, {
toValue: 0.0,
duration: this.props.fadeOutDuration,
useNativeDriver: true,
})
this.animation.start(() => {
this.setState({
isShown: false,
})
this.isShown = false
if (typeof this.callback === "function") {
this.callback()
}
})
}, delay)
}
componentWillUnmount() {
this.animation && this.animation.stop()
this.timer && clearTimeout(this.timer)
}
render() {
const { isShown, text, opacityValue } = this.state
const { position, positionValue } = this.props
const pos = {
top: positionValue,
center: height / 2,
bottom: height - positionValue,
}[position]
if (isShown) {
return (
<View style={[styles.container, { top: pos }]}>
<Animated.View
style={[
styles.content,
{ opacity: opacityValue },
this.props.style,
]}
>
{React.isValidElement(text) ? (
text
) : (
<Text style={this.props.textStyle}>{text}</Text>
)}
</Animated.View>
</View>
)
}
return null
}
}
Normally we display the toast message for 4s, but I decided to display it in e2e tests for 1.5s in order to make some what faster.
I'm testing for the presence of the toast like this:
await expect(element(by.text(text))).toBeVisible()
await waitFor(element(by.text(text))).toBeNotVisible().withTimeout(2000)
However it happens often that detox fails at "toBeVisible". I can see the message on the screen, but for some reason detox is missing it.
What is the minimum time I should keep the message on the screen for detox to detect it?
On .circleCI we record videos of failing tests. When a test fails with "cannot find element" and I watch the video I clearly see the toast appearing on the screen, but detox fails to find it.
I'm still not sure if there is a better way, but I found a way that currently works for us.
Instead of automatically hiding the toast in e2e test, we mock the modal component to display and stay visible until tapped on.
Once detox sees the element we tap on it, close it and continue with our test.
I also had exactly the same problem in my project and the the solution that we found was to disable detox synchronization around the toast.
As an example, this is how the code would look like:
await device.disableSynchronization();
await element(by.id(showToastButtonId)).tap();
await waitFor(element(by.text('Toast Message')))
.toExist()
.withTimeout(TIMEOUT_MS);
await device.enableSynchronization();
Reference: https://github.com/wix/Detox/blob/master/docs/Troubleshooting.Synchronization.md#switching-to-manual-synchronization-as-a-workaround

React native multi-column FlatList insert banner

I'm using multi-column FlatList in my React Native application to display items like below (left image). I'm trying to integrate AdMob banner into the application like many other apps did, and insert the ads banner in the middle of the list, like below (right image).
As far as I can tell, FlatList doesn't support this type of layout out-of-the-box. I'm wondering what would be a good practice to implement this feature and doesn't impact app performance.
(Side note, the list supports pull-to-refresh and infinite loading when approaching end of the list.).
Thank you in advance for any suggestions.
In such a case, I always recommend to drop the numColumns property and replace it by a custom render function, which handles the columns by its own.
Let's say we have the following data structure:
const DATA =
[{ id: 1, title: "Item One"}, { id: 2, title: "Item Two"}, { id: 3, title: "Item Three"},
{ id: 4, title: "Item Four"}, { id: 5, title: "Item Five"}, { id: 6, title: "Item Six"},
{ id: 7, title: "Item Seven"}, { id:8, title: "Item Eight"}, { id: 9, title: "Item Nine"},
{ id: 10, title: "Item Ten"}, { id: 11, title: "Item eleven"},
{ id: 12, title: "Item Twelve"}, { id: 13, title: "Item Thirteen"}];
As I said we don't use the numColumns property instead we are restructuring our data so we can render our list how we want. In this case we want to have 3 columns and after six items we want to show an ad banner.
Data Modification:
modifyData(data) {
const numColumns = 3;
const addBannerAfterIndex = 6;
const arr = [];
var tmp = [];
data.forEach((val, index) => {
if (index % numColumns == 0 && index != 0){
arr.push(tmp);
tmp = [];
}
if (index % addBannerAfterIndex == 0 && index != 0){
arr.push([{type: 'banner'}]);
tmp = [];
}
tmp.push(val);
});
arr.push(tmp);
return arr;
}
Now we can render our transformed data:
Main render function:
render() {
const newData = this.modifyData(DATA); // here we can modify the data, this is probably not the spot where you want to trigger the modification
return (
<View style={styles.container}>
<FlatList
data={newData}
renderItem={({item, index})=> this.renderItem(item, index)}
/>
</View>
);
}
RenderItem Function:
I removed some inline styling to make it more clearer.
renderItem(item, index) {
// if we have a banner item we can render it here
if (item[0].type == "banner"){
return (
<View key={index} style={{width: WIDTH-20, flexDirection: 'row'}}>
<Text style={{textAlign: 'center', color: 'white'}}> YOUR AD BANNER COMPONENT CAN BE PLACED HERE HERE </Text>
</View>
)
}
//otherwise we map over our items and render them side by side
const columns = item.map((val, idx) => {
return (
<View style={{width: WIDTH/3-20, justifyContent: 'center', backgroundColor: 'gray', height: 60, marginLeft: 10, marginRight: 10}} key={idx}>
<Text style={{textAlign: 'center'}}> {val.title} </Text>
</View>
)
});
return (
<View key={index} style={{width: WIDTH, flexDirection: 'row', marginBottom: 10}}>
{columns}
</View>
)
}
Output:
Working Example:
https://snack.expo.io/SkmTqWrJS
I'd recommend this beautiful package
https://github.com/Flipkart/recyclerlistview
Actually, we were handling thousands of data list in our app, flatlist was able to handle it quite good, but still we were looking for a high performance listview component to produce a smooth render and memory efficient as well. We stumbled upon this package. Trust me it's great.
Coming to your question, this package has the feature of rendering multiple views out-of-the-box. It has got a good documentation too.
So basically, the package has three important step to setup the listview.
DataProvider - Constructor function the defines the data for each
element
LayoutProvider - Constructor function that defines the layout (height
/ width) of each element
RowRenderer - Just like the renderItem prop in flatlist.
Basic code looks like this:
import React, { Component } from "react";
import { View, Text, Dimensions } from "react-native";
import { RecyclerListView, DataProvider, LayoutProvider } from "recyclerlistview";
const ViewTypes = {
FULL: 0,
HALF_LEFT: 1,
HALF_RIGHT: 2
};
let containerCount = 0;
class CellContainer extends React.Component {
constructor(args) {
super(args);
this._containerId = containerCount++;
}
render() {
return <View {...this.props}>{this.props.children}<Text>Cell Id: {this._containerId}</Text></View>;
}
}
export default class RecycleTestComponent extends React.Component {
constructor(args) {
super(args);
let { width } = Dimensions.get("window");
//Create the data provider and provide method which takes in two rows of data and return if those two are different or not.
let dataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
});
//Create the layout provider
//First method: Given an index return the type of item e.g ListItemType1, ListItemType2 in case you have variety of items in your list/grid
this._layoutProvider = new LayoutProvider(
index => {
if (index % 3 === 0) {
return ViewTypes.FULL;
} else if (index % 3 === 1) {
return ViewTypes.HALF_LEFT;
} else {
return ViewTypes.HALF_RIGHT;
}
},
(type, dim) => {
switch (type) {
case ViewTypes.HALF_LEFT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.HALF_RIGHT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.FULL:
dim.width = width;
dim.height = 140;
break;
default:
dim.width = 0;
dim.height = 0;
}
}
);
this._rowRenderer = this._rowRenderer.bind(this);
//Since component should always render once data has changed, make data provider part of the state
this.state = {
dataProvider: dataProvider.cloneWithRows(this._generateArray(300))
};
}
_generateArray(n) {
let arr = new Array(n);
for (let i = 0; i < n; i++) {
arr[i] = i;
}
return arr;
}
//Given type and data return the view component
_rowRenderer(type, data) {
//You can return any view here, CellContainer has no special significance
switch (type) {
case ViewTypes.HALF_LEFT:
return (
<CellContainer style={styles.containerGridLeft}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.HALF_RIGHT:
return (
<CellContainer style={styles.containerGridRight}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.FULL:
return (
<CellContainer style={styles.container}>
<Text>Data: {data}</Text>
</CellContainer>
);
default:
return null;
}
}
render() {
return <RecyclerListView layoutProvider={this._layoutProvider} dataProvider={this.state.dataProvider} rowRenderer={this._rowRenderer} />;
}
}
const styles = {
container: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#00a1f1"
},
containerGridLeft: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#ffbb00"
},
containerGridRight: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#7cbb00"
}
};
In the LayoutProvider, you can return multiple type of view based on the index or you can add a viewType object in your data array, render views based on that.
this._layoutProvider = new LayoutProvider(
index => {
if (index % 3 === 0) {
return ViewTypes.FULL;
} else if (index % 3 === 1) {
return ViewTypes.HALF_LEFT;
} else {
return ViewTypes.HALF_RIGHT;
}
},
(type, dim) => {
switch (type) {
case ViewTypes.HALF_LEFT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.HALF_RIGHT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.FULL:
dim.width = width;
dim.height = 140;
break;
default:
dim.width = 0;
dim.height = 0;
}
}
);
tl;dr: Check the https://github.com/Flipkart/recyclerlistview and use the layoutProvider to render different view.
Run the snack: https://snack.expo.io/B1GYad52b

How do I get access to to this.props.navigation if I'm on my RootPage?

I need to pass this.props.navigation to a helper function. However, I'm currently sitting on the root page, which initially creates the stackNavigator. So far I've tried importing Navigator, NavigatorActions and withNavigation from react-navigation and then sending it/them along as a parameter to my helper function, but I keep getting the error message: cannot read property 'state' of undefined.
Here is my RootPage code that is relevant:
/**
* RootPage has the Navigator. So it controls navigation.
*/
import React, { Component } from 'react';
import {
Platform,
BackHandler,
View,
Animated,
StyleSheet,
Image,
TouchableWithoutFeedback,
TouchableOpacity,
Alert
} from 'react-native';
import { connect } from 'react-redux';
import { BoxShadow } from 'react-native-shadow';
import { NavigationActions } from 'react-navigation';
import { vsprintf } from 'sprintf-js';
import RootNavigator from './RootNavigator';
//Test Component
import TestPageFactory from '../TestPage/TestPageFactory';
// Components
import XXBluetoothManager, { XXBluetoothEventEmitter } from '../../nativeComponents/XXBluetooth/XXBluetooth';
import XXText from '../../components/XXText/XXRobotoText';
import XXSyncXXerlay from '../../components/XXSyncXXerlay/XXSyncXXerlay';
// Actions
import { logout } from '../../actions/user';
import { closeSyncModal, toggleRootMenu, openRootMenu, closeRootMenu } from '../../actions/page';
// Utils
import { LAYOUTINFO } from '../../utils/layoutinfo';
import { generateElevationStyle } from '../../utils/util';
import { NavUtils } from '../../utils/navutils';
import { StatusBarManager } from '../../utils/statusbarmanager';
// Config
import { DEVICEINFO } from '../../config';
// Localization
import I18n from '../../localization/i18n';
import { aDeviceHasBeenConnectedFunc } from '../../actions/device';
// Constants
const DEFAULT_PROFILE_IMAGE = require('../../images/profile-image/user.png');
const HEIGHT_RATIO = DEVICEINFO.IS_EXTRA_SMALL_SCREEN ? 0.5 : (DEVICEINFO.SCREEN_HEIGHT >= 667 ? (DEVICEINFO.SCREEN_HEIGHT / 667) : 0.7);
class RootPage extends Component {
static getDerivedStateFromProps(nextProps, prevState) {
let shouldLoadingBeVisible = nextProps.pageStatus && nextProps.pageStatus.isUpdateRequested ? true : false;
if (!nextProps.profile && shouldLoadingBeVisible === prevState.loadingVisible) {
return null;
}
return {
profileImage: nextProps.profile && nextProps.profile.profileImage ?
{ uri: nextProps.profile.profileImage } : DEFAULT_PROFILE_IMAGE,
profileName: nextProps.profile && nextProps.profile.profileName ? nextProps.profile.profileName : '',
loadingVisible: shouldLoadingBeVisible,
};
}
constructor(props) {
super(props);
this.routeStatusBarStyle = 'dark-content';
this.state = {
animatedPosition: new Animated.ValueXY(0, 0),
profileImage: DEFAULT_PROFILE_IMAGE,
profileName: '',
deviceAddress: undefined,
isMenuVisible: false,
loggingOut: false,
device: undefined,
};
}
componentDidMount() {
if (Platform.OS === 'android') {
this.initAndroidHardwareBackEvent();
}
// this.initializeBluetooth();
}
componentWillUnmount() {
this.destroyBluetooth();
}
componentDidUpdate(prevProps, prevState) {
if (this.state.loggingOut) {
if (this.props.signinStatus && !this.props.signinStatus.isLoginSuccess) {
this.setState({ loggingOut: false }, () => {
this.onPressMenu('EntryPage');
});
}
}
if (this.props.pageStatus.isMenuVisible != null && prevState.isMenuVisible !== this.props.pageStatus.isMenuVisible) {
this.setState({ isMenuVisible: this.props.pageStatus.isMenuVisible }, () => {
this.onUpdateMenu(this.props.pageStatus.isMenuVisible);
});
}
// console.log('what are the prev state here? ", ', prevState)
/*
NOTE: We need to know when an item has been connected, if an item has not been connected, we should
make the hearing test available
*/
}
render() {
return (
<View style={styles.rootView}>
{this.renderRootMenu()}
<Animated.View style={[styles.animatedView, { left: this.state.animatedPosition.x, top: this.state.animatedPosition.y }]}>
{this.renderXXerlay()}
{this.renderNavigator()}
</Animated.View>
{this.renderSyncXXerlay()}
</View>
);
}
updateStatusBarStyle(pageId) {
let newStatusBarStyle = 'dark-content';
if (pageId === 'EntryPage') {
newStatusBarStyle = 'light-content';
}
if (newStatusBarStyle !== this.routeStatusBarStyle) {
this.routeStatusBarStyle = newStatusBarStyle;
StatusBarManager.setStyle(newStatusBarStyle, false)
}
}
renderRootMenu() {
return (
<View style={styles.menuView}>
{this.renderMenuTopArea()}
{this.renderMenuBottomArea()}
</View>
)
}
renderMenuTopArea() {
if (DEVICEINFO.IS_ANDROID && (Platform.Version === 19 || Platform.Version === 20)) {
return this.renderMenuTopAreaKitKat();
}
return this.renderMenuTopAreaDefault();
}
renderMenuTopAreaDefault() {
return (
<View style={[styles.menuTopArea, generateElevationStyle(1.5)]}>
{this.renderProfileImage()}
</View>
)
}
renderMenuTopAreaKitKat() {
let shadowOptions = {
width: DEVICEINFO.SCREEN_WIDTH,
height: (DEVICEINFO.SCREEN_HEIGHT * 0.35) + 2,
color: '#000',
border: 2,
opacity: 0.05,
radius: 1,
y: -2.5,
};
return (
<BoxShadow setting={shadowOptions}>
<View style={styles.menuTopArea}>
{this.renderProfileImage()}
</View>
</BoxShadow>
)
}
renderMenuBottomArea() {
return (
<View style={styles.menuBottomArea}>
{this.renderMenuContents()}
</View>
)
}
renderMenuContents() {
const menuData = this.generateMenuData();
let renderOutput = [];
for (let i = 0, len = menuData.length; i < len; i++) {
if (this.state.deviceAddress && menuData[i].onlyNotConnected) {
continue;
}
let extraStyle = {};
if (i === 0) {
let extraStyleMarginTop = DEVICEINFO.IS_EXTRA_SMALL_SCREEN ? 16 : 26;
extraStyle['marginTop'] = extraStyleMarginTop * LAYOUTINFO.DESIGN_HEIGHT_RATIO;
}
renderOutput.push(
<TouchableOpacity
key={menuData[i].text}
activeOpacity={0.8}
underlayColor='transparent'
onPress={() => { menuData[i].onPressMenu() }}
style={[styles.menuButtonTouch, extraStyle]}>
<View style={styles.menuButtonView}>
<Image
source={menuData[i].icon}
style={[styles.menuIcon]} />
<XXText style={[styles.menuText]}>{menuData[i].text}</XXText>
</View>
</TouchableOpacity>
)
}
return renderOutput;
}
generateMenuData() {
return [
this.generateMenuItem(I18n.t('home'), require('../../images/menu-home.png'), 'HomePage'),
this.generateMenuItem(I18n.t('tutorial'), require('../../images/menu-tutorial.png'), 'SelectTutorialPage'),
this.generateMenuItem(I18n.t('item_list'), require('../../images/menu-add-item.png'), 'itemListPage'),
this.generateMenuItem(I18n.t('report'), require('../../images/menu-report.png'), 'ReportDetailPage'),
this.generateTestPageOnly(I18n.t('hearing_test'), require('../../images/menu-demotest.png')),
this.generateMenuItem(I18n.t('equalizer'), require('../../images/menu-sound.png'), 'SoundPage'),
this.generateMenuItem(I18n.t('support'), require('../../images/menu-support.png'), 'SupportPage'),
this.generateMenuItem(I18n.t('account_settings'), require('../../images/menu-settings.png'), 'SettingsPage'),
{
text: I18n.t('logout'),
icon: require('../../images/menu-logout.png'),
onPressMenu: () => {
Alert.alert(
I18n.t('item'),
I18n.t('logout_confirmation'),
[
{ text: I18n.t('cancel'), onPress: () => { } },
{ text: I18n.t('logout'), onPress: () => { this.onPressLogout() } }
]
)
}
},
];
}
generateTestPageOnly(label, icon) {
let deviceAddress;
let versionData;
// console.log('what is the props here: ', this.props.devices)
function loopThruObject(objectOfDevices) {
for (let [key, value] of Object.entries(objectOfDevices)) {
for (let [newKey, newValue] of Object.entries(value)) {
if (newKey === 'macAddress') {
deviceAddress = newValue;
}
if (newKey === 'version')
versionData = newValue
}
}
return;
}
loopThruObject(this.props.devices)
let currentDevice = this.props.devices[deviceAddress];
let newParams = {
navIndex: 1,
device: currentDevice
};
if (this.props.aDeviceHasBeenConnected === true) {
return {
text: label,
icon: icon,
onPressMenu: () => {
let testPageIdentifier = TestPageFactory.getIdentifier({ isDemo: false, versionData: versionData });
console.log('what is the testPage identifier: ', this.props.navigation)
// NavUtils.push(NavigationActions, testPageIdentifier, { device: currentDevice });
NavigationActions.dispatch({ type: 'Navigation/NAVIGATE', routeName: 'FittingTestPage', params: newParams })
}
}
}
if (this.props.aDeviceHasBeenConnected === false) {
return {
text: 'N/A',
icon: icon,
onPressMenu: () => {
}
}
}
}
generateMenuItem(label, icon, onPressId, onlyNotConnected = false) {
return {
text: label,
icon: icon,
onPressMenu: () => {
this.onPressMenu(onPressId);
},
onlyNotConnected
}
}
renderProfileImage() {
imageSource = this.state.profileImage;
let deviceCount = Object.keys(this.props.devices).length;
let deviceConnectionString = this.generateConnectedString(deviceCount);
return (
<View style={styles.profileImageSubContainer}>
<Image
key={this.state.profileImage.uri}
source={imageSource}
style={styles.profileImage} />
{/* <XXText style={[styles.profileText]} fontWeight='Regular'>{this.state.profileName}</XXText> */}
<XXText style={[styles.deviceText]} fontWeight={'Light'}>{deviceConnectionString}</XXText>
</View>
)
}
generateConnectedString(deviceCount) {
let deviceConnectionString;
if (deviceCount === 1) {
deviceConnectionString = vsprintf(I18n.t('one_item_connected_format_str'), deviceCount.toString());
}
else {
deviceConnectionString = vsprintf(I18n.t('items_connected_format_str'), deviceCount.toString());
}
return deviceConnectionString;
}
renderNavigator() {
return (
<RootNavigator
ref={nav => {
this.navigator = nav;
}}
onNavigationStateChange={(prevState, currentState) => {
this.handleNavSceneChange(prevState, currentState);
}} />
)
}
handleNavSceneChange(prevState, currentState) {
const currentScreen = NavUtils.getCurrentRouteNameFromState(currentState);
this.currentPageId = currentScreen;
this.updateStatusBarStyle(currentScreen);
}
renderXXerlay() {
// Workaround for Android position absolute bug
// issue : https://github.com/facebook/react-native/issues/8923
let visible = this.state.isMenuVisible;
return (
<TouchableWithoutFeedback
onPress={() => { this.hideMenu() }}>
<View style={[styles.XXerlayView, { height: visible ? null : 0 }]} />
</TouchableWithoutFeedback>
)
}
renderSyncXXerlay() {
let visible = this.props.pageStatus.syncModal;
if (!visible) {
return null;
}
return (
<XXSyncXXerlay
visible={visible} />
)
}
//
// EVENTS
//
onPressMenu(pageId) {
requestAnimationFrame(() => {
if (this.currentPageId !== pageId) {
if (pageId === 'DemoTestPage') {
NavUtils.push(this.navigator, pageId);
}
else {
NavUtils.resetTo(this.navigator, pageId);
}
this.updateStatusBarStyle(pageId);
}
this.hideMenu();
});
}
onPressLogout() {
this.setState({ loggingOut: true }, () => {
this.props.logout();
});
}
onUpdateMenu(isMenuVisible) {
if (isMenuVisible === undefined || this.previousMenuVisible === isMenuVisible) {
return;
}
this.previousMenuVisible = isMenuVisible;
// When menu needs to be opened
if (isMenuVisible) {
StatusBarManager.setStyle('light-content', isMenuVisible);
Animated.timing(
this.state.animatedPosition,
{
tXXalue: {
// Animate the RootPage to the right.
// x: 317 / 375 * DEVICEINFO.SCREEN_WIDTH,
// y: 126 / 667 * DEVICEINFO.SCREEN_HEIGHT,
x: 317 / 375 * DEVICEINFO.SCREEN_WIDTH,
y: 1 / 1000 * DEVICEINFO.SCREEN_HEIGHT
},
// duration: 300
// debugging mode
duration: 300
}
).start();
return;
}
// When menu needs to be closed
StatusBarManager.setStyle(this.routeStatusBarStyle, isMenuVisible);
Animated.timing(
this.state.animatedPosition,
{
tXXalue: {
x: 0,
y: 0
},
duration: 300
}
).start();
}
//
// Methods
//
initializeBluetooth() {
// Event listeners for XXBluetoothManager
this.itemDeviceStatusChanged = XXBluetoothEventEmitter.addListener('itemDeviceStatusChanged', event => {
// console.log('ROotpage; . initializeBluetooth: ', event)
this.getConnectedDevice();
});
this.getConnectedDevice();
}
destroyBluetooth() {
if (this.itemDeviceStatusChanged) {
this.itemDeviceStatusChanged.remXXe();
}
}
getConnectedDevice() {
console.log('when does getconnectedDevice get fired inside rootpage? :D', this.props)
XXBluetoothManager.getDeviceAddress((error, address) => {
if (error) {
// Not connected
this.setState({ deviceAddress: null });
return;
}
// console.log('when can we read error, address', error, address)
this.setState({ deviceAddress: address });
});
}
initAndroidHardwareBackEvent() {
BackHandler.addEventListener('hardwareBackPress', () => {
if (!this.navigator) {
return false;
}
if (this.navigator.state.index > 0) {
const backAction = NavigationActions.back({
key: null
});
this.navigator.dispatch(backAction);
return true;
}
return false;
});
}
toggleMenu() {
this.props.toggleRootMenu();
}
hideMenu() {
this.props.closeRootMenu();
}
}
const mapStateToProps = (state) => {
return {
signinStatus: state.page.signin,
pageStatus: state.page.root,
profile: state.user.profile,
devices: state.device.devices,
aDeviceHasBeenConnected: state.device.aDeviceHasBeenConnected,
}
}
const mapDispatchToProps = (dispatch) => {
return {
logout: () => {
dispatch(logout());
},
closeSyncModal: () => {
dispatch(closeSyncModal());
},
toggleRootMenu: () => {
dispatch(toggleRootMenu());
},
openRootMenu: () => {
dispatch(openRootMenu());
},
closeRootMenu: () => {
dispatch(closeRootMenu());
},
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RootPage);
const styles = StyleSheet.create({
rootView: {
flex: 1,
flexDirection: 'column',
},
menuView: {
width: DEVICEINFO.SCREEN_WIDTH,
height: '100%',
backgroundColor: '#aab942',
position: 'absolute',
top: 0,
left: 0
},
menuTopArea: {
backgroundColor: '#b7c846',
// height: DEVICEINFO.SCREEN_HEIGHT * 0.35,
height: DEVICEINFO.SCREEN_HEIGHT * 0.28,
width: '100%',
flexDirection: 'column',
},
menuBottomArea: {
// backgroundColor: '#aab942',
width: '100%',
flex: 1,
flexDirection: 'column',
},
animatedView: {
position: 'relative',
flex: 1,
flexDirection: 'column',
backgroundColor: '#fff'
},
profileImageSubContainer: {
height: '100%',
flexDirection: 'column',
justifyContent: 'center',
marginLeft: 40,
marginTop: 10,
},
profileImage: {
width: DEVICEINFO.IS_EXTRA_SMALL_SCREEN ? 70 : 80,
height: DEVICEINFO.IS_EXTRA_SMALL_SCREEN ? 70 : 80,
borderRadius: (DEVICEINFO.IS_EXTRA_SMALL_SCREEN ? 70 : 80) / 2
},
profileText: {
marginTop: DEVICEINFO.IS_EXTRA_SMALL_SCREEN ? 6 : 16,
fontSize: Math.min(LAYOUTINFO.DESIGN_WIDTH_RATIO * 20, 21),
color: '#fff',
backgroundColor: 'transparent'
},
deviceText: {
marginTop: DEVICEINFO.IS_EXTRA_SMALL_SCREEN ? 6 : 16,
fontSize: Math.min(LAYOUTINFO.DESIGN_WIDTH_RATIO * 14, 15),
color: '#fff',
backgroundColor: 'transparent'
},
menuContainer: {
flex: 1,
width: '100%',
height: '100%',
flexDirection: 'column',
alignItems: 'flex-start',
},
// menubutton
menuButtonTouch: {
marginTop: 25 * HEIGHT_RATIO,
marginLeft: 38,
},
menuButtonView: {
flexDirection: 'row',
alignItems: 'center'
},
menuIcon: {
width: 18,
height: 18,
resizeMode: 'contain'
},
// menubuttontext
menuText: {
marginLeft: 17,
fontSize: Math.min(LAYOUTINFO.DESIGN_WIDTH_RATIO * 22, 22),
color: '#fff'
},
XXerlayView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: null,
height: null,
backgroundColor: 'rgba( 255, 255, 255, 0.0001 )',
zIndex: 9999
}
});
Here is the RootNavigator page
import { Platform } from 'react-native';
import { StackNavigator } from 'react-navigation';
// Pages
import EntryPage from '../EntryPage/EntryPage';
import SigninPage from '../SigninPage/SigninPage';
import SignupPage from '../SignupPage/SignupPage';
import ReportSinglePage from '../ReportSinglePage/ReportSinglePage';
import HomePage from '../HomePage/HomePage';
import VolumePage from '../VolumePage/VolumePage';
import ReportPage from '../ReportPage/ReportPage';
import ReportDetailPage from '../ReportDetailPage/ReportDetailPage';
import ItemListPage from '../ItemListPage/ItemListPage';
import AddItemPage from '../AddItemPage/AddItemPage';
import SupportPage from '../SupportPage/SupportPage';
import SettingsPage from '../SettingsPage/SettingsPage';
import SoundPage from '../SoundPage/SoundPage';
import VerticalSoundPage from '../SoundPage/VerticalSoundPage';
import SoundSyncPage from '../SoundSyncPage/SoundSyncPage';
import TestPage from '../TestPage/TestPage';
import iOSTestPage from '../TestPage/iOSTestPage';
import FittingTestPage from '../TestPage/FittingTestPage';
import SendResultSlide from '../SoundSyncPage/SendResultSlide';
import ModePage from '../ModePage/ModePage';
import PolicyPage from '../PolicyPage/PolicyPage';
import PrivacyPage from '../PrivacyPage/PrivacyPage';
import TermsAndConditionsPage from '../TermsAndConditionsPage/TermsAndConditionsPage';
import SelectTutorialPage from '../TutorialPage/SelectTutorialPage';
import PreparingTutorial from '../TutorialPage/PreparingTutorial';
import AddItemTutorial from '../TutorialPage/AddItemTutorial';
import HearingTestTutorial from '../TutorialPage/HearingTestTutorial';
import ReportTutorial from '../TutorialPage/ReportTutorial';
import EqualizerTutorial from '../TutorialPage/EqualizerTutorial';
import AddItemTutorialCompletion from '../TutorialPage/AddItemTutorialCompletion';
import AddItemTutorialFailure from '../TutorialPage/AddItemTutorialFailure';
import HearingTestTutorialCompletion from '../TutorialPage/HearingTestTutorialCompletion';
// import RootPage from '../RootPage/RootPage';
const RootNavigator = StackNavigator(
{
// RootPage: {screen: RootPage},
EntryPage: { screen: EntryPage },
SignupPage: { screen: SignupPage },
HomePage: { screen: HomePage },
TestPage: { screen: TestPage },
iOSTestPage: { screen: iOSTestPage },
FittingTestPage: { screen: FittingTestPage },
ReportPage: { screen: ReportPage },
ReportDetailPage: { screen: ReportDetailPage },
ReportSinglePage: { screen: ReportSinglePage },
ItemListPage: { screen: ItemListPage },
AddItemPage: { screen: AddItemPage },
SoundPage: { screen: VerticalSoundPage },
SoundSyncPage: { screen: SoundSyncPage },
SupportPage: { screen: SupportPage },
SettingsPage: { screen: SettingsPage },
VolumePage: { screen: VolumePage },
SendResultSlide: { screen: SendResultSlide },
PolicyPage: { screen: PolicyPage },
PrivacyPage: { screen: PrivacyPage },
TermsAndConditionsPage: { screen: TermsAndConditionsPage },
SelectTutorialPage: { screen: SelectTutorialPage },
PreparingTutorial: { screen: PreparingTutorial },
AddItemTutorial: { screen: AddItemTutorial },
HearingTestTutorial: { screen: HearingTestTutorial },
ReportTutorial: { screen: ReportTutorial },
EqualizerTutorial: { screen: EqualizerTutorial },
AddItemTutorialCompletion: { screen: AddItemTutorialCompletion },
AddItemTutorialFailure: { screen: AddItemTutorialFailure },
HearingTestTutorialCompletion: { screen: HearingTestTutorialCompletion },
},
{
initialRouteName: 'EntryPage',
navigationOptions: {
header: null,
}
},
{
headerMode: 'none'
}
);
export default RootNavigator;
And just in case, here is the helper function this I'm trying to use:
static push(navigator, pageId, params = {}) {
let newParams = { navIndex: 1 };
if (navigator.state.index) {
newParams.navIndex = navigator.state.index + 1;
}
else if (navigator.state.params && navigator.state.params.navIndex) {
newParams.navIndex = navigator.state.params.navIndex + 1;
}
Object.assign(newParams, params);
navigator.dispatch({ type: 'Navigation/NAVIGATE', routeName: pageId, params: newParams });
}

react native todo list with TextInput

Is it possible to build a todo list with react native that can
add new TexInput with the return key
focus the new TextInput when created
remove TextInputs with the delete key if the TextInput is empty and focus another input
I have a basic list that can add items and focus them but not remove items.
https://snack.expo.io/#morenoh149/todo-list-textinput-spike
import * as React from 'react';
import { TextInput, View } from 'react-native';
export default class App extends React.Component {
currentTextInput = null
state = {
focusedItemId: 0,
items: [
{ id: 0, text: 'the first item' },
{ id: 1, text: 'the second item' },
],
};
addListItem = index => {
let { items } = this.state;
const prefix = items.slice(0, index + 1);
const suffix = items.slice(index + 1, items.length);
const newItem = { id: Date.now(), text: '' };
let result = prefix.concat([newItem]);
result = result.concat(suffix);
this.setState({
focusedItemId: newItem.id,
items: result,
});
};
focusTextInput() {
// focus the current input
this.currentTextInput.focus();
}
componentDidUpdate(_, pState) {
// if focused input id changed and the current text input was set
// call the focus function
if (
pState.focusedItemId !== this.state.focusedItemId
&& this.currentTextInput
) {
this.focusTextInput();
}
}
render() {
const { focusedItemId } = this.state;
return (
<View style={{ flex: 1, justifyContent: 'center' }}>
{this.state.items.map((item, idx) => (
<TextInput
style={{ borderWidth: 1, borderColor: 'black' }}
value={item.text}
ref={item.id === focusedItemId
? c => this.currentTextInput = c
: null}
autoFocus={item.id === focusedItemId}
onChangeText={text => {
const newItems = this.state.items;
newItems[idx].text = text;
this.setState({
items: newItems,
});
}}
onSubmitEditing={event => this.addListItem(idx)}
/>
))}
</View>
);
}
}
To remove items you can add a callback to the onKeyPress and check if it was the Backspace (delete) key and if the text field was empty already. If so, you remove the item from the item list.
onKeyPress={({ nativeEvent: { key: keyValue } }) => {
if(keyValue === 'Backspace' && !items[idx].text) {
this.removeListItem(idx)
}
}}
In the removeListItem function you can remove the item at the index and update the selected id to the id previous in the list to focus this one.
removeListItem = index => {
const { items } = this.state;
const newItems = items.filter(item => item.id !== items[index].id)
this.setState({
focusedItemId: items[index - 1] ? items[index - 1].id : -1,
items: newItems.length ? newItems : [this.createNewListItem()],
});
}
Please find the full working demo here: https://snack.expo.io/#xiel/todo-list-textinput-spike