Open compojoom opened 8 years ago
Hello @compojoom!
I'm not sure I follow completely, but you have the value
prop and the options
prop. The first one is used to set the actual value of the Form
(in your case, it would set the position
value) and the other is used to customize how your form behaves.
Please take a look how I solved to provide some default options depending on the value passed https://github.com/APSL/react-native-floating-label/blob/master/FloatingLabel.js#L18-L21. You could do the same using props.options
instead of props.value
and set some defaults in your factory.
Hey @alvaromb! Thanks for the fast reply. You know that with react you can have this: https://facebook.github.io/react/docs/reusable-components.html You can define what properties your component expect and you can also define defaultPropValues when one of the expected props is not passed to the component. That's very easy to do.
The issue is that if I define what props my component expects -> those props are never recognized by react as you are passing them over to the props.options object and not directly to the props object.
Actually I can define what expect as properties like this:
static propTypes = {
options: PropTypes.shape({
position: PropTypes.shape({
latitude: PropTypes.number.isRequired,
longitude: PropTypes.number.isRequired
}),
region: PropTypes.shape({
latitude: PropTypes.number.isRequired,
longitude: PropTypes.number.isRequired,
latitudeDelta: PropTypes.number.isRequired,
longitudeDelta: PropTypes.number.isRequired
}),
userPosition: PropTypes.bool
})
}
Now if I pass a position.latitude, without longitude react complains. The same goes for region. This helps to define the api. However there is a problem with this code:
static get defaultProps() {
return {
options: {
position: {
latitude: LATITUDE,
longitude: LONGITUDE
},
region: {
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA
},
userPosition:true
}
}
}
If I do that, no default value is set position, region or userPosition, since react does this check in React.CreateElement
// Resolve default props
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
Note if (props[propName] === undefined) {
- when the propname is options, the value here is not undefined. Options at this point already has a factory. And my position, userposition etc are never set.
If I change the syntax of my defaultProps to:
static get defaultProps() {
return {
position: {
latitude: LATITUDE,
longitude: LONGITUDE
},
region: {
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA
},
userPosition:true
}
}
then my options will end up with default values for position, region and userPosition, but I can't enforce to pass the correct proptypes. And if I pass userPosition: false in my form options, then those won't end up in the props object, but in the props.options.userPosition.
Do you understand the issue now? I would love to use the default things react provide for our component. But due to the way we pass props that doesn't seem to be possible.
You are right, it's not possible to leverage defaultProps
. Fortunately handling default options in a custom way is quite easy
@compojoom are you using options
to set default values of your form?
No, to pass default values for the fields I use the value={state.value}
Props are just for layout stuff.
@compojoom thanks for sharing your code, a google map component is a nice example of custom factory
This is the final version of the code that I'm going to live with for now.
import React, {PropTypes} from 'react';
import {
View,
Text,
StyleSheet,
Dimensions
} from 'react-native';
import MapView from 'react-native-maps';
var t = require('tcomb-form-native');
var Component = t.form.Component;
const {width, height} = Dimensions.get('window');
const ASPECT_RATIO = width / height;
const LATITUDE = 51.023033;
const LONGITUDE = 10.3621663;
const LATITUDE_DELTA = 0.0922;
const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO;
class PositionOnMap extends Component {
static propTypes = {
options: PropTypes.shape({
region: PropTypes.shape({
latitude: PropTypes.number.isRequired,
longitude: PropTypes.number.isRequired,
latitudeDelta: PropTypes.number.isRequired,
longitudeDelta: PropTypes.number.isRequired
}),
userPosition: PropTypes.bool
})
}
constructor(props) {
super(props);
this.state.region = {
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA
};
if (props.options.region) {
this.state.region = {
...this.state.region,
...props.options.region
}
}
this.state.marker = null;
if (props.value && props.value.latitude !== null && props.value.longitude !== null) {
this.state.region.latitude = props.value.latitude;
this.state.region.longitude = props.value.longitude;
this.state.marker = {
coordinate: {
latitude: props.value.latitude,
longitude: props.value.longitude
}
}
}
this.onMapPress.bind(this)
}
/**
* Add our marker on the map
*
* @param e - the onClick event from the map
*/
onMapPress(e) {
this.setState({
marker: {
coordinate: e.nativeEvent.coordinate
},
value: e.nativeEvent.coordinate
});
}
/**
* Update our marker and value state when we drag the marker
*
* @param e - the event
*/
onMarkerDragEnd(e) {
this.setState({
marker: {
coordinate: e.nativeEvent.coordinate,
},
value: e.nativeEvent.coordinate
});
}
/**
* Get the user's position and update the region state if necessary
*/
componentDidMount() {
if (this.props.options.userPosition) {
navigator.geolocation.getCurrentPosition(
(position) => {
// If the user has set a marker already, there is no need to change the region position
if (!this.state.marker) {
this.setState(
{
region: {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}
}
);
}
},
(error) => console.log(error),
{enableHighAccuracy: false, timeout: 20000, maximumAge: 1000}
);
}
}
shouldComponentUpdate(nextProps, nextState) {
var should = super.shouldComponentUpdate(nextProps, nextState)
if (should) {
return true;
}
should = (
nextState.region !== this.state.region
);
return should;
}
getTemplate() {
return (locals) => {
var stylesheet = locals.stylesheet;
var controlLabelStyle = stylesheet.controlLabel.normal;
if (locals.hasError) {
controlLabelStyle = stylesheet.controlLabel.error;
}
var label = locals.label ? <Text style={controlLabelStyle}>{locals.label}</Text> : null;
return (
<View style={{marginLeft: -20, marginRight:-20}}>
{label}
<View style={styles.mapContainer}>
<MapView style={styles.map}
region={this.state.region}
onRegionChange={(region) => this.setState({region})}
onPress={(e) => this.onMapPress(e)}
>
{(() => {
if (this.state.marker) {
return <MapView.Marker
key={this.state.marker.key}
onDragEnd={(e) => this.onMarkerDragEnd(e)}
coordinate={this.state.marker.coordinate}
pinColor={this.state.marker.color}
draggable
/>
}
})()}
</MapView>
</View>
</View>
);
}
}
}
var styles = StyleSheet.create({
mapContainer: {
height: 200,
justifyContent: 'flex-end',
alignItems: 'stretch',
},
map: {
...StyleSheet.absoluteFillObject,
backgroundColor: '#000'
},
});
export default PositionOnMap;
I don't like the fact that I have styles in here. Maybe I should move them to the stylesheet, but then the person who does the stylesheet should know about them and I have some hardcoded values in there. But for now I can live with it :)
@alvaromb @gcanti - could you please share some light on something. My Factory renders a map for a position field. The map relies on some additional parameters to properly display - such as region. If I don't have a region I'm setting a default one.
Now I'm trying to pass a region to my factory.
<Form
ref="form"
type={FieldModel.model}
options={FieldModel.options}
value={this.state}
onChange={this.onChange.bind(this)}
/>
this.state has a region value. But this value never arrives in the Factory?
What options am I supposed to pass in order for my Factory to see the region?
this is my form definition:
// Our company model
var model = t.struct({
id: t.maybe(t.String),
name: t.String,
position: t.struct({
latitude: t.Number,
longitude: t.Number
})
});
var options = function () {
return {
stylesheet: stylesheet,
auto: 'none',
fields: {
id: {
hidden: true
},
name: {
placeholder: 'Feldname',
underlineColorAndroid: 'transparent',
},
position: {
label: 'Feld Position',
factory: PositionOnMap,
userPosition: true
},
}
}
};
If I pass a region in the options configuration, then my factory is picking it up, but I don't have the region here. I have it first in my react component when I try to render the form??? How are you dealing with such situations?
but I don't have the region here
Not sure I understand, why not? Aren't options a function of the value / state?
var options = function (value) {
return {
...
position: {
label: 'Feld Position',
factory: PositionOnMap,
userPosition: true,
region: someFunctionOf(value)
},
...
}
}
@gcanti wow! Thank you! I seem to have been on a trip of some kind. Of course you are right.
I have my model and option definitions in a different file. And for some reason I ignored the fact that options is a function and when I render the form I can pass it some parameters...
Thank you! I can't believe how easy this was and how I hard I made it seem :D
Hey there!
thanks for the component. Apart from some initial difficulties and the styling for android - it's awesome :)
Contrary to my initial expectations, creating a factory turned out to be really easy (it's a react.component at the end) and that made abstracting my code very easy.
I've created a factory that renders a google map and a marker. This way the user could move the marker around and update the latitude/longitude for this position.
Here is the code in case someone else needs it. (I could create a repository with it later, once I clean up some things)
My question is - how do you pass props and define default values for them? To use the component I do:
var Position = t.struct({ latitude: t.Number, longitude: t.Number });
// here we are: define your domain model var Whatever = t.struct({ position: Position, });
var options = { fields: { position: { factory: PositionOnMap, userPosition: true } },
};
Now when I access props in my constructor - userPosition is passed to the options object. It's not a standalone value in the props. So using the propTypes and defaultProps to define what the component expects doesn't work?
As you can see in componentDidMount() I'm using this.props.options.userPosition to find the user's position. However I don't want to pass userPosition: true in the field options. I would rather prefer that userPosition is per default set to true and only when we don't need it pass userPosition: false.
Thanks in advance for any guidelines.