I have a problem with ListView styling. My ListView has sections. So I need to separate sections by some empty space. I see decision in adding marginBottom style to last element in every section. With css it would be done with :last pseudo-class. Has React Native some alternatives to do this?
Some pseudo code for example:
var dataSource = new React.ListView.DataSource({
rowHasChanged: (a, b) => a !== b,
sectionHeaderHasChanged: (a, b) => a !== b,
getRowData: (dataBlob, sectionId, rowId) => dataBlob[rowId],
getSectionHeaderData: (dataBlob, sectionId, rowId) => dataBlob[sectionId]
});
//test data
var sectionIds = ["clients", "properties"];
var rowIds = [["clients_1", "clients_2"], ["properties_1"]];
var dataBlob = {
clients_1: {
type: "client",
title: "Andrew Chinn",
addInfo: "xxx xxx xxx",
image: ""
},
clients_2: {
type: "client",
title: "Karl Chinn",
addInfo: "xxx xxx xxx",
image: ""
},
properties_1: {
type: "property",
title: "Karl Chinn",
addInfo: "xxx xxx xxx",
image: ""
}
};
const renderSectionHeader = (data) => {
return (
<SomeTestHeader {...data}/>
);
};
const renderRow = (data) => {
return (
<SomeTestComponent {...data}/>
);
};
var Test extends React.Component {
render() {
<View>
<ListView
dataSource={dataSource.cloneWitRowsAndSections(dataBlob, sectionIds, rowIds)}
renderRow={renderRow}
renderSectionHeader={renderSectionHeader}
/>
</View>
}
}
You can do this in your renderRow method :
renderRow = (rowData, sectionId, rowId) => {
var indexSection = _.findIndex(this.state.sectionIds, function(o) {return o === sectionId});
if (rowId == _.last(this.state.rowIds[indexSection])) {
return (
<View style={{backgroundColor: 'red'}}>
<Text>{rowId}</Text>
</View>
);
} else {
return (
<View style={{backgroundColor: 'blue'}}>
<Text>{rowId}</Text>
</View>
);
}
};
You can do the same for your sections with sectionId. I've set up an example here.
ListView exposes a function called renderSeparator which renders a view/component between sections, use it to render the separator you want.
Your other option is just to give marginTop value to the view you're rendering in the renderSectionHeader
Related
When migrating from ListView to Flatlist I am getting values in an object I need to get as a value in array.
I have modified the react-native-modal-filter-picker to a custom component which needs to work for me. Here onSelect shows the results in object.
renderOptionList = () => {
const {
noResultsText,
listViewProps,
keyboardShouldPersistTaps,
keyExtractor,
} = this.props
const { ds } = this.state;
if (!ds.length) {
return (
<FlatList
data={ds}
keyExtractor={keyExtractor||this.keyExtractor}
{...listViewProps}
renderItem={() => (
<View style={styles.noResults}>
<Text style={styles.noResultsText}>{noResultsText}</Text>
</View>
)}
/>
)
} else {
return (
<FlatList
keyExtractor={keyExtractor||this.keyExtractor}
{...listViewProps}
data={ds}
renderItem={this.renderOption}
/>
)
}
};
renderOption = ({item}) => {
const {
selectedOption,
renderOption,
optionTextStyle,
selectedOptionTextStyle
} = this.props;
const { key, label } = item
let style = styles.optionStyle;
let textStyle = optionTextStyle||styles.optionTextStyle;
if (key === selectedOption) {
style = styles.selectedOptionStyle;
textStyle = selectedOptionTextStyle ||styles.selectedOptionTextStyle
}
if (renderOption) {
return renderOption(item, key === selectedOption)
} else {
return (
<TouchableOpacity activeOpacity={0.7}
style={style}
onPress={() => this.props.onSelect(item)}
>
<Text style={textStyle}>{label}</Text>
</TouchableOpacity>
)
}
};
const object1 = {
a: 'somestring',
b: 42,
c: false
};
console.log(Object.values(object1));
Output will be
["somestring", 42, false]
Try this...
JSON.stringfy(object)
A common use of JSON is to exchange data to/from a web server.
JSON.stringfy convert a JavaScript object into a string.
I want to select only one checkbox, not multiple.
If i select two checkboxes one by one the previously selected checkbox should be unselected.
In my below code i can select multiple checkboxes.
import React ,{Component} from "react";
import CircleCheckBox, {LABEL_POSITION} from "react-native-circle-checkbox";
class Select_Delivery_Option extends React.Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({
rowHasChanged: (row1, row2) => row1 !== row2
});
this.state = {
check_data:[],
dataSource: ds.cloneWithRows([]),
checked:false,
isLoading:false,
};
}
//I had call The componentDidMount for json Data here and bind it in Data source;
render() {
return ();
}
_renderRow(rowData: string, sectionID: number, rowID: number) {
return (
<View style={{ flex:1,flexDirection:'column',backgroundColor:'#FFF'}}>
<View style={{ flex:1,flexDirection:'row',backgroundColor:'#FFF'}}>
<View style={{flexDirection:'column',margin:10}}>
{rowData.adbHomeAddress}
<CircleCheckBox
checked={rowData.checked}
onToggle={()=>this._onPressRow(rowID, rowData,rowData.checked)}
labelPosition={LABEL_POSITION.LEFT}
label={rowData.Address1 +" ,\n "+ rowData.Address2 +",\n"+rowData.ctiName+", "+rowData.staName+", "+rowData.ctrName+","+rowData.adbZip+"."}
innerColor="#C72128"
outerColor="#C72128"
styleLabel={{color:'#000',marginLeft:10}}
/>
</View>
</View>
</View>
);
}
_onPressRow = (rowID,rowData,checked) => {
const {check_data,filter} = this.state;
console.log('rowdata',rowData);
console.log('rowid',rowID);
console.log('checked',checked);
rowData.checked = !rowData.checked;
var dataClone = this.state.check_data;
dataClone[rowID] = rowData;
this.setState({check_data: dataClone });
}
}
Link to the CircleCheckBox component used: https://github.com/paramoshkinandrew/ReactNativeCircleCheckbox
I had the same requirement and wasted hours looking for solution. Eventually, I was able to resolve the problem on my own.
Posting my answer below, l have used hooks in the example, let me know if someone wants a class-based solution.
const checkboxComponent = () => {
const [checkboxValue, setCheckboxValue] = React.useState([
{ label: 'Customer', value: 'customer', checked: false },
{ label: 'Merchant', value: 'merchant', checked: false },
{ label: 'None', value: 'none', checked: false },
])
const checkboxHandler = (value, index) => {
const newValue = checkboxValue.map((checkbox, i) => {
if (i !== index)
return {
...checkbox,
checked: false,
}
if (i === index) {
const item = {
...checkbox,
checked: !checkbox.checked,
}
return item
}
return checkbox
})
setCheckboxValue(newValue)
}
return (
<View>
{checkboxValue.map((checkbox, i) => (
<View style={styles.checkboxContainer} key={i}>
<CheckBox
value={checkbox.checked}
onValueChange={(value) => checkboxHandler(value, i)}
/>
<Text style={styles.label}>{checkbox.label}</Text>
</View>
))}
</View>
)
}
export default checkboxComponent
I suggest you to use FlatList instead of ListView it's more advance and easy to use component.
For your issue please create a state checkedItem: -1 and directly assign id of your item you check last then just add a check to your CircleCheckBox item. something like below code.
<CircleCheckBox
checked={rowData.id === this.state.checkedItem}
onToggle={(rowID)=> this.setState({ checkedItem: rowID})}
labelPosition={LABEL_POSITION.LEFT}
label={rowData.Address1 +" ,\n "+ rowData.Address2 +",\n"+rowData.ctiName+", "+rowData.staName+", "+rowData.ctrName+","+rowData.adbZip+"."}
innerColor="#C72128"
outerColor="#C72128"
styleLabel={{color:'#000',marginLeft:10}}
/>
Let me know if any query.
I currently developing a react native app ( version 0.55.2) and mapbox/react-native (version 6.1.2-beta2)
I have a situation where some annotations are shown initially on map render, then further annotations are loaded when the user's zooms.
The first annotations are displayed at the right place.
However, when new annotations are added, there are all stuck at the top left corner.
Following their documentation, https://github.com/mapbox/react-native-mapbox-gl/blob/master/docs/MapView.md, I tried to call the function when the map is loaded or rendered. I even tried a setTimeout. The annotations always appears at the topleft map.
Any ideas how should I approach this?
THanks!
class map extends React.Component {
constructor(props) {
super(props);
this.getMapVisibleBounds = getMapVisibleBounds.bind(this);
this.state = {
...INIT_MAP_STATE
};
}
//compo lifecyle
componentDidUpdate(prevProps, prevState) {
if (this.state.userPosition.longitude !== prevState.userPosition.longitude) {
this.setBounds();//first annotations. works fine
}
if (this.state.zoomLevel !== prevState.zoomLevel) {
this.setBounds(); //update annotations. doesn't work
}
}
render()=>{
const { quest, checkpoint } = this.props;
const { selectedIndex } = this.state;
return (
<View style={styles.container}>
<Mapbox.MapView
styleURL={MAP_STYLE}
zoomLevel={this.state.zoomLevel}
centerCoordinate={[this.state.userPosition.longitude,
this.state.userPosition.latitude]}
style={styles.mapWrap}
>
{this.renderMap(checkpoint, "checkpoint")}
</Mapbox.MapView>
</View>
);
}
setBounds = () => {
this.getMapVisibleBounds(this.map)
.catch(err => {
console.error(err);
})
.then(bounds => {
this._setMapBounds(bounds);// set state bounds
return this.props.onLoadQuest(bounds); //api call
});
}
}
// annotations rendering
class checkPoint extends Component {
constructor(props) {
super(props);
}
renderAnnotations = (data, id) => {
const uniqKey = `checkpoint_${id}`;
return (
<Mapbox.PointAnnotation key={uniqKey} id={uniqKey} coordinate={[data[0], data[1]]}>
<TouchableWithoutFeedback onPress={idx => this.onSelect(id)}>
<Image source={checkPointImg} style={styles.selfAvatar} resizeMode="contain" />
</TouchableWithoutFeedback>
</Mapbox.PointAnnotation>
);
};
render() {
if (!this.props.checkpoint || isEmpty(this.props.checkpoint)) {
return null;
}
const { hits } = this.props.checkpoint;
if (!Array.isArray(hits)) {
return [];
}
return hits.map((c, idx) =>
this.renderAnnotations(c._source.location.coordinates, c._source.id)
);
}
}
"PointAnnotation" is legacy, try passing your points to as an object. You're map render will be so much faster once you make the swap. Something like this.
<MapboxGL.MapView
centerCoordinate={[ userLocation.longitude, userLocation.latitude ]}
pitchEnabled={false}
rotateEnabled={false}
style={{ flex: 1 }}
showUserLocation={true}
styleURL={'your_style_url'}
userTrackingMode={MapboxGL.UserTrackingModes.MGLUserTrackingModeFollow}
zoomLevel={10}
>
<MapboxGL.ShapeSource
key='icon'
id='icon'
onPress={this._onMarkerPress}
shape={{type: "FeatureCollection", features: featuresObject }}
type='geojson'
images={images}
>
<MapboxGL.SymbolLayer
id='icon'
style={layerStyles.icon}
/>
</MapboxGL.ShapeSource>
</MapboxGL.MapView>
Where "featuresObject" looks something like this...
let featuresObject = []
annotation.forEach((annot, index) => {
let lat = annot.latitude
let lng = annot.longitude
featuresObject[index] = {
type: "Feature",
geometry: {
type: "Point",
coordinates: [lng, lat]
},
properties: {
exampleProperty: propertyValue,
}
}
})
Example for polygon layer
Example with custom icon
You can add markers dynamically by using this code:
Create marker component:
const Marker = ({ coordinate, image, id }) => {
return (
<MapboxGL.MarkerView coordinate={coordinate} id={id}>
// Add any image or icon or view for marker
<Image
source={{ uri: image }}
style={{width: '100%', height: '100%'}}
resizeMode="contain"
/>
</MapboxGL.MarkerView>
);
};
Consume it inside MapBoxGL:
<MapboxGL.MapView
style={{
// it will help you keep markers inside mapview
overflow: 'hidden'
}}>
{markers &&
markers?.length > 0 &&
markers.map((marker, index) => (
<Marker
coordinate={[marker.longitude, marker.latitude]}
// id must be a string
id={`index + 1`}
image={getIconUrl(index)}
/>
))
}
</MapboxGL.MapView>
const layerStyles = Mapbox.StyleSheet.create({
icon: {
iconImage: "{icon}",
iconAllowOverlap: true,
iconSize: 0.5,
iconIgnorePlacement: true
}
});
const mapboxIcon = props => {
return (
<Mapbox.ShapeSource
shape={makeMapBoxGeoJson(props.datum, props.mapKey, props.name)}
key={`${props.name}_key_${props.mapKey}`}
id={`${props.name}_${props.mapKey}`}
images={getIcon(props.name)}
onPress={idx => (props.isActive ? props.onSelectId(props.mapKey) : null)}
>
<Mapbox.SymbolLayer
id={`${props.mapKey}_pointlayer`}
style={[layerStyles.icon, { iconSize: props.iconSize ? props.iconSize : 0.5 }]}
/>
</Mapbox.ShapeSource>
);
};
I'm using react native with realm db. The realm schema is as follows:
static schema = {
name: 'TodoItem',
primaryKey: 'id',
properties: {
id: {type: 'string'},
value: {type: 'string'},
Category: {type: 'string'},
completed: {type: 'bool', default: false},
createdTimestamp: {type: 'date'}
}
}
export const todoItemDS = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2, sectionHeaderHasChanged: (s1, s2) => s1 !== s2})
const mapStateToProps = (state, props) => ({
dataSource: todoItemDS.cloneWithRowsAndSections(todoItemsResults),
}
The ListView tag is as follows:
<ListView
dataSource={dataSource}
renderRow={this.renderRow.bind(this)}
renderSectionHeader={this.renderSectionHeader.bind(this)}
/>
and renderSectionHeader:
renderSectionHeader(sectionData, category) {
return (
<Text>{category}</Text>
)
}
renderRow(item){
const {dataSource, deleteTodoItem} = this.props
return (
<View style={{ justifyContent: 'space-between',flexDirection: 'row'}}>
<CheckBox onPress={(e) => this.completed(item.id,item.value,e.target.checked)} style={{marginTop: 15 }}checked={item.completed} />
<Text onPress={(e) => this.goToPageTwo(item.id)} style={{ alignSelf: 'center',flex:10}} >{item.value}
</Text>
<Button iconLeft large transparent primary style={{ height: 30 , flex:1 }} onPress={() => deleteTodoItem(item)}>
<Icon name="trash-o" style={{ color: 'red' }} />
</Button>
</View>)
}
I fill todoItems datasource from this function:
export const getTodoItems = () => {
const todoItems = TodoItem.get().sorted('createdTimestamp', true);
return todoItems
}
However, the rows and sections are rendered with empty sections text and empty rows text as shown in the image.
What is missing in this code and how can I render sections and rows correctly?
I added a listener to realm code that fills the data source as follows:
export const getTodoItems = () => {
console.log('create db:', Realm.path)
const itemData = {}
const todoItems = TodoItem.get().sorted('createdTimestamp', true).filtered('completed=false');
todoItems.addListener((items, changes) => {
// Update UI in response to inserted objects
changes.insertions.forEach((index) => {
if(itemData[items[index].Category]) {
itemData[items[index].Category].push(items[index])
} else
itemData[items[index].Category] = []//;
});
// Update UI in response to modified objects
changes.modifications.forEach((index) => {
});
// Update UI in response to deleted objects
changes.deletions.forEach((index) => {
// Deleted objects cannot be accessed directly
// Support for accessing deleted objects coming soon...
});
});;
todoItems.forEach((item) => {
if(itemData[item.Category]) {
itemData[item.Category].push(item)
} else
itemData[item.Category] = []//;
})
return itemData //todoItems
}
However, I can't see added items. The added item only shows up after adding another item. Any ideas?
The rendered SectionHeader is showing integer is because you didn't construct the dataSource in the right format, see the documentation here: link.
You need to construct something like:
const todoItemsData = {
categoryOne: [itemOne, itemTwo],
categoryTwo: [itemThree, itemFour]
}
But right now what you have is just an array of objects [realmItemOne, realmItemTwo], and if you just pass in an array of objects to construct the dataSource that gonna consumed by the cloneWithRowsAndSections, the category param in renderSectionHeader will becomes integer index accroding to here, Object.keys(arrayOfObjects) will return [0, 1, 2, ...]
So you want to map your todoItemsResults and construct the something like this
const itemData = {}
todoItemsResults.forEach((item) => {
if(itemData[item.Category] {
itemData[item.Category].push(item)
} else
itemData[item.Category] = [];
}
});
const mapStateToProps = (state, props) => ({
dataSource: todoItemDS.cloneWithRowsAndSections(itemData),
}
For the rendered row not showing any data issue, I think is due to the same problem, the item param you are calling within renderRow should be a data attribute for one of you todoItem object. You can try replace {item.value} within {item} to see what item is actually is in your setting. I believe this will be solved if you can construct the right data to feed your dataSource.
For your follow up comments regarding listerning to Realm update:
You can do something like this
class DummyPage extends Component {
constructor(props) {
super(props)
this.realmUpdated = this.realmUpdated.bind(this);
realm.objects('todoItem').addListener('change', this.realmUpdated);
}
componentWillUnmount() {
realm.objects('todoItem').removeListener('change', this.realmUpdated);
}
realmUpdated() {
this.forceUpdate();
}
render() {
//build your data source in here and render the sectionList
}
}
When swiping the row in the SwipeableListView I want to delete the rowitem and re-render the list.
What is now happening is that always the last item in the list is removed, not the item that is swiped.
Any ideas what is wrong?
export default class SwipeList extends Component {
constructor(props) {
super(props);
let ds = SwipeableListView.getNewDataSource();
this.favourites = []
this.state = {
ds:[],
dataSource:ds,
isLoading:true,
closeRow:false,
};
}
componentWillMount () {
store.get('KEY_FAV').then(value => {
typeof(value) === 'object'
? this.favourites = Object.keys(mockdata.favourite)
: this.favourites = JSON.parse(value)
this.setState({
dataSource: this.state.dataSource.cloneWithRowsAndSections(this.genData(this.favourites
)),
isLoading:false
})
})
}
genData = (list) => {
let dataBlob = []
for(let i = 0; i <list.length; i++) {
dataBlob.push({id:list[i], name:list[list[i]]})
}
return [dataBlob, []]
}
Till here it is okay, the SwipeableList is loaded with all RowItems.
But in the below handleSwipeAction() while setting new state for dataSource, the list will only delete the last item, not the selected.
handleSwipeAction = (rowData, SectionID, rowID) => {
AlertIOS.alert('Remove ' + rowData.name + ' \nfrom Favourites?', null,
[
{text:'Cancel', onPress: () => {this.setState({closeRow:true})}, style:'cancel'},
{text:'OK', onPress: () => {
this.favourites.slice()
this.favourites.splice(rowID, 1)
this.setState({
closeRow:true,
})
this.setState({//I THINK HERE IS THE PROBLEM
dataSource:this.state.dataSource.cloneWithRowsAndSections(this.genData(this.favourites))
})
store.set('KEY_FAV', this.favourites)
}}
])
}
onSwipe = (rowData, SectionID, rowID) => {
return (
<View style={styles.actionsContainer}>
<TouchableHighlight
onPress={() => this.handleSwipeAction(rowData, SectionID, rowID)}>
<Text style={styles.actionsItem}>Remove</Text>
</TouchableHighlight>
</View>
);
};
and the render function
render() {
if(this.state.isLoading) return null
return (
<View style={styles.container}>
<SwipeableListView
bounceFirstRowOnMount
enableEmptySections={true}
dataSource={this.state.dataSource}
maxSwipeDistance={this.props.swipeDistance}
renderRow={(item) => this.renderItem(item)}
renderQuickActions={this.onSwipe}
renderSeparator={this.renderSeperator}
doCloseRow={this.state.closeRow}
/>
</View>
);
}
when you are done slicing, I believe if you do:
let ds = SwipeableListView.getNewDataSource(); all over again, and then
this.setState({ dataSource: ds.cloneWithRowsAndSections(this.genData(this.favourites)) })
It should work. For a reason that I still don't get. Also I don't know why you do two setState() in your function. One is enough no?
So this should work:
handleSwipeAction = (rowData, SectionID, rowID) => {
AlertIOS.alert('Remove ' + rowData.name + ' \nfrom Favourites?', null,
[
{text:'Cancel', onPress: () => {this.setState({closeRow:true})}, style:'cancel'},
{text:'OK', onPress: () => {
this.favourites.slice()
this.favourites.splice(rowID, 1)
let ds = SwipeableListView.getNewDataSource(); // add this
this.setState({ dataSource: ds.cloneWithRowsAndSections(this.genData(this.favourites)), closeRow:true })
store.set('KEY_FAV', this.favourites) }} ])
}