I am new in react native, and I have question about text input and state.
Let say I make an app to buy nature materials, for instance, sands. I have a buy page and a trade component. buy page has a button for buy and go to the next step. And the trade component inside the buy page has text input to input how much users want to buy. The state is come from the parent (buy page) and I send it via props to it's child (trade component)
I have a text input to bid a price, let's called in inputContainer. And beside it, there's a read-only text-input of the sand's mass, let's called it outputContainer. For example, let say 1 kg of sands have a price of $10. So, when I type 100 (dollars) in inputContainer, then the onchangetext works for outputContainer and returns 10 kg.
The problem is when I delete the value inputContainer until nothing is left, the outputContainer returns NaN. What I want is after deleting all input, the inputContainer automatically returns 0 so the outputContainer also returns 0
buy page.js
state = {
inputTextValue: '',
outputTextValue: ''
}
onChangeValue = inputValue => {
const qty = ParseInt(inputValue) / 10
if (inputValue === '') {
this.setState({
inputTextValue: '0'
});
} else {
this.setState({
inputTextValue: inputValue.replace(/\B(?=(\d{3})+(?!\d))/g, '.')
outputTextValue: qty.toString()
});
}
}
render(){
return (
<View>
<Trade
onChange={this.onChangeValue}
inputText={this.state.inputTextValue}
outputText={this.state.outputTextValue}
/>
</View>
)
}
in trade.js
type Props = {
inputText?: String,
outputText?: String
}
class Trade extends PureComponent<Props> {
static defaultProps = {
inputText: '',
outputText: '',
onChange: () => {}
}
render(){
const { inputText, outputText, onChange } = this.props;
return(){
<View>
<TextInput
onChangeText={onChange}
value={inputText}
/>
<TextInput
onChangeText={onChange}
value={outputText}
editable={false}
/>
</View>
}
}
}
From code above what I got is I do return 0, but when I want to type a new input, the 0 is still in front of the text input.
Well, let me show you the flow when I use that code:
Type the input
input: 100 output: 10
Deleting input
input: 0 output: 0
Type again
input: 0100 output: (ignore this)
What I want in number 3 is => input: 100 when I type 100
How I can achieve that? Thanks
You can try this
this.setState({
inputTextValue: '0',
outputTextValue: '0'
});
You must have to put the values of Input and Output into state and initially set their values to '0'
Related
Please could someone help answer this:
I have 2 NumberInput controls, one input and the other is disabled. I need to input number in the first input field, the disabled field to show this number/100. The two NumberInput will have source fields that will save to the current record in the simpleform.
How do I do this in react-admin
Thanks
Easiest way is to use the method described in the docs under section Linking two inputs
In essence: You can create your own input component where you can access the form values via the hook useFormState. Then just assign the desired value transformed the way you want e.g. divided by 100.
Edit
Found one more even cleaner way - using the final-form-calculate to create a decorator and pass it to the <FormWithRedirect /> component like so:
import createDecorator from 'final-form-calculate'
const calculator = createDecorator(
// Calculations:
{
field: 'number1', // when the value of foo changes...
updates: {
number2: (fooValue, allValues) => allValues["number1"] * 2
}
})
...
<FormWithRedirect
...
decorators={[calculator]}
/>
Check out this code sandbox
Using FormDataConsumer
<FormDataConsumer>
{({ formData }) => (
<NumberInput defaultValue={formData.my_first_input / 100} source="second_input"/>
)}
</FormDataConsumer>
Using the useFormState hook
import { useFormState } from 'react-final-form';
...
const { values: { my_first_input }} = useFormState({ subscription: { values: true } });
...
<NumberInput defaultValue={my_first_input / 100} source="second_input"/>
Source: https://marmelab.com/react-admin/Inputs.html#linking-two-inputs
Dynamic
You need to use the useForm hook of react-final-form to make your input dynamic:
import { useForm, useFormState } from 'react-final-form';
...
const {change} = useForm();
const { values: { my_first_input }} = useFormState({ subscription: { values: true } });
useEffect(() => {
change('my_second_input', my_first_input / 100);
}, [change, my_first_input]);
...
<NumberInput defaultValue={my_first_input / 100} source="second_input"/>
I got a shorter solution to this question:
All I did was to do the calculation within FormDataConsumer. Now, I am able to get the calculated value and it updates the correct record in the array.
Thanks
<FormDataConsumer>
{({
formData, // The whole form data
scopedFormData, // The data for this item of the ArrayInput
getSource, // A function to get the valid source inside an ArrayInput
...rest
}) => {
if (typeof scopedFormData !== 'undefined') {
scopedFormData.total = scopedFormData.quantity * scopedFormData.unitprice;
return (
<NumberInput disabled defaultValue={scopedFormData.total} label="Total" source={getSource('total')} />
)
} else {
return(
<NumberInput disabled label="Total" source={getSource('total')} />
)
}
}}
I am using React Native. What I need to do is from a text that I extract from the DB to apply a format (font color, link) to make #mentions, when searching if in the text finds 1 single match makes replacement all good, but if there are several #mentions in the text, it throws me an error.
Text example:
hey what happened #-id:1- everything ok ?? and you #-id:2- #-id:3- #-id:4-
//// listusers its array, example: [idusuario: "1", usuario: "#luigbren", format: "#-id:1-".....]
const PatternToComponent = (text, usuarios,) => {
let mentionsRegex = new RegExp(/#-(id:[0-9]+)-/, 'gim');
let matches = text.match(mentionsRegex);
if (matches && matches.length) {
matches = matches.map(function (match, idx) {
let usrTofind = matches[idx]
//////////////////////////////////////////////////////////////////
const mentFormat = listusers.filter(function (item) {
const itemData = item.format;
return itemData.indexOf(usrTofind) > -1;
});
if (mentFormat.length > 0) {
let idusuario = mentFormat[0].idusuario
let replc = mentFormat[0].usuario
console.log(usrTofind) //// here find #-id:1-
console.log(replc) //// here is #luigbren for replace
////////// now here replace part of the string, #-id:1- with a <Text> component
///////// with #luigbren and the link, this is repeated for every #mention found
parts = text.split(usrTofind);
for (var i = 1; i < parts.length; i += 2) {
parts[i] = <Text key={i} style={{ color: '#00F' }} onPress={() => { alert('but this is'); }}>{replc}</Text>;
}
return text = parts;
/////////////////////////////////////////////////////////////////////
} else {
return text
}
});
} else {
return text
}
return text
};
in this way the code works well for me only when in the text there is only one mention, example 'hey what happened #-id:1- everything ok ??' , but when placing more than one mention it gives me an error, example: 'hey what happened #-id:1- everything ok ?? #-id:2- #-id:3-' ...
Error:
TypeError: text.split is not a function
And if instead of placing parts = text.split(usrTofind); I place parts = text.toString().split(usrTofind); it gives me this error:
[Object Object]
Update
I found the solution. I found another simple way to replace part of the text with the <Text> component, using this method:
funcMentToLinks = (text, idusuario) => {
const regex = /\#[id:([0-9]+]/g;
const splitText = text.split(regex)
if (splitText.length <= 1) {
return text;
}
const matches = text.match(regex);
return splitText.reduce(function (arr, element, index) {
if (matches[index]) {
return [arr, element, <Text key={index} style={{ backgroundColor: '#ceedf1', fontWeight: 'bold', color: '#4f73c4' }} onPress={() => this.callmyopc(idusuario)}>{replc}</Text>,]
} else {
return [arr, element]
}
}, []);
}
but now I have a problem, I can't find a way to call a function to navigate to another section or invoke direct the this.props.navigation.navigate('opc'), if i put my function .. example: onPress={() => this.callmyopc(idusuario)} gives me an error
Error:
_this2.callmyopc is not a function
if i put it this way onPress={() => {this.props.navigation.navigate('opc', { idperf: idusuario })}} gives me an error..
Error:
Cannot read property 'navigation' of undefined
Note: the functions this.props.navigation.navigate if they work for me in this same file, I use it in other functions, but in this function I cannot invoke it.
I am new to React Native, and currently have two text input boxes and I would like it that when I change one the value shown in the other also changes. But then you will be allowed to edit the second text input and this will in tune change the first one. And so on...
I have tried setting the placeholder as the value, then that will change as the first text input changes, but it only works until you edit the text box.
Essentially I cannot figure out a way to have a text input and output on top of each other.
state = { valueOne: '', valueTwo: '' }
changeValueOne = (valueOne) => {
this.setState({
valueOne,
valueTwo: `${valueOne}-foo`
})
}
changeValueTwo = (valueTwo) => {
this.setState({
valueOne: `${valueTwo}-bar`,
valueTwo
})
}
render() {
const { valueOne, valueTwo } = this.state
return (
<View>
<Input onChangeText={this.changeValueOne} value={valueOne} />
<Input onChangeText={this.changeValueTwo} value={valueTwo} />
</View>
)
}
When I hit the end of the list, fetchmore and updateQuery run the query with the new offset and fetch the new items. But the list itself re-renders with the old data and offset.
I've added the code here for review. Summarizing it, there's a SectionList of recent transactions, which is basically a regular FlatList with items grouped by date here. To get the infinite scroll to work, I followed the docs here for pagination using offset. For some reason, the offset is not increasing to match the size of the list each time I try to fetch more - instead, it updates every other time. How can this be? Console.log statements seem to show the list re-rendering BEFORE the new data is returned.
Log from the first time I hit the end of my list:
EndReached 1: offset is: 10
Top of query function
Top of query function
Query got transactions, returning txnList. txns.length: 10
TransactionList Render: transactions.length: 10
EndReached - UpdateQuery fn... offset: 10
EndReached - UpdateQuery fn... Prev length: 10
EndReached - UpdateQuery fn... Fetchmore length: 10
EndReached - UpdateQuery fn... New data length: 20 . <---this gets returned and should cause a re-render but it doesn't
Log when I scroll to the end of the list a second time:
EndReached: offset is: 10 <-- should be 20
Top of query function
Top of query function
Query got transactions, returning txnList. txns.length: 20 <--seems to be the transactions from the first updateQuery, this should be 10 new ones
TransactionList Render: transactions.length: 20
EndReached - UpdateQuery fn... offset: 10
EndReached - UpdateQuery fn... Prev length: 20
EndReached - UpdateQuery fn... Fetchmore length: 10
EndReached - UpdateQuery fn... New data length: 20 <--merged, skipping the duplicates 11-20 because we already had them from before
code
export class TransactionQuery extends React.Component {
constructor(props) {
super(props);
}
renderEndReached = (fetchMore, offset) => {
fetchMore({
variables: {
limit: this.props.limit,
offset: offset,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prev;
}
copy = prev;
copy.user.transactions = unionBy(
prev.user.transactions,
fetchMoreResult.user.transactions,
"id"
); // this merges old transactions with new, removing any duplicates by matching transaction ID
return copy;
}
});
}
};
render() {
return (
<Query
query={TXN_DATA}
fetchPolicy="cache-and-network"
notifyOnNetworkStatusChange
variables={{ limit: this.props.limit, offset: 0 }}
>
{({ data, error, loading, fetchMore }) => {
if (error) return <Text>ERROR! O NO! {error}</Text>;
if (loading) {
return <Text> LOADING </Text>;
} // this is annoying and causes the list to snap back to the top each time we fetch more items.
if (data && data.user) {
const transactions = data.user.transactions;
if (transactions) {
return (
<TransactionList
transactions={transactions}
renderEndReached={() => this.renderEndReached(fetchMore=fetchMore, offset=transactions.length)}
/>
);
} else {
return <Text>No transactions found</Text>;
}
} else {
return <Text>Transactions query returned no data/user</Text>;
}
}}
</Query>
);
}
}
const Transaction = ({ name, date, amount, logoUrl }) => (
<ListItem icon noBorder>
<Left>
<Thumbnail small source={{ uri: logoUrl }} />
</Left>
<Body>
<Text>{name}</Text>
</Body>
<Right>
<USD value={amount} />
</Right>
</ListItem>
);
class TransactionList extends React.Component {
constructor(props) {
super(props);
}
renderItem = ({ item, index, section }) => {
return <Transaction {...item} />;
};
renderHeader = ({ section }) => {
return (
<ListItem itemDivider style={{ backgroundColor: "white" }}>
<Moment element={Text} format="dddd, MMMM D">
{section.title}
</Moment>
</ListItem>
);
};
render() {
const byDate = groupBy(this.props.transactions, "date");
const dates = Object.keys(byDate)
.sort()
.reverse();
return (
<SectionList
sections={dates.map(date => ({ title: date, data: byDate[date] }))}
renderItem={this.renderItem}
renderSectionHeader={this.renderHeader}
keyExtractor={item => item.id}
onEndReached={this.props.renderEndReached}
/>
);
}
}
Fix:
It seems Query won't re-render if it doesn't think the merged object (copy) is different enough from the previous query result - maybe it's a shallow compare? I discovered this fix by adding a test message to the object before returning it.
copy = prev;
copy.user.transactions = [...prev.user.transactions, ...fetchMoreResult.user.transactions]; // this change isn't detected. Shallow compare?
copy.message = "Hi there"; // this fixes the issue but is ugly
return copy;
A neater fix is to use lodash cloneDeep
copy = cloneDeep(prev);
copy.user.transactions = [...prev.user.transactions, ...fetchMoreResult.user.transactions];
return copy;
Attention: I have posted an answer down there, personally I think it's the best solution so far. Even though it's not the highest rated answer, but based on the result I'm getting, it is very efficient.
---------------------------------------------Original Question-------------------------------------------------------
Suppose I am writing a Twitter clone, but much simpler. I put each item in FlatList and render them.
To "like" a post, I press the "like" button on the post and the "like" button turns red, I press it again, it turns gray.
This is what I have so far: I store all the loaded posts in this.state, each post has a property called "liked", which is boolean, indicating whether this user has liked this post or not, when user presses "like", I go to state.posts and update the liked property of that post, and then use this.setState to update posts like so:
// 1. FlatList
<FlatList
...
data={this.state.posts}
renderItem={this.renderPost}
...
/>
// 2. renderPost
renderPost({ item, index }) {
return (
<View style={someStyle}>
... // display other properties of the post
// Then display the "like" button
<Icon
name='favorite'
size={25}
color={item.liked ? 'red' : 'gray'}
containerStyle={someStyle}
iconStyle={someStyle}
onPress={() => this.onLikePost({ item, index })}
/>
...
</View>
);
}
// 3. onLikePost
likePost({ item, index }) {
let { posts } = this.state;
let targetPost = posts[index];
// Flip the 'liked' property of the targetPost
targetPost.liked = !targetPost.liked;
// Then update targetPost in 'posts'
posts[index] = targetPost;
// Then reset the 'state.posts' property
this.setState({ posts });
}
This approach works, however, it is too slow. The color of the "like" button flips as I press it, but it usually takes about 1 second before the color changes. What I want is that the color would flip almost at the same time when I press it.
I do know why this would happen, I should probably not use this.setState, because when I do that, the posts state changed, and all posts get re-rendered, but what other approach can I try?
You can set extraData in FlatList:
<FlatList
...
extraData={this.state}
data={this.state.posts}
renderItem={this.renderPost}
...
/>
When state.posts or state.posts's item change, FlatList will re-render.
From FlatList#extradata:
A marker property for telling the list to re-render (since it implements PureComponent). If any of your renderItem, Header, Footer, etc. functions depend on anything outside of the data prop, stick it here and treat it immutably.
Update:
Functional component implementation:
export default function() {
// list of your data
const [list, setList] = React.useState([])
const [extraData, setExtraData] = React.useState(new Date())
// some update on the item of list[idx]
const someAction = (idx)=>{
list[idx].show = 1
setList(list)
setExtraData(new Date())
}
return (
<FlatList
// ...
data={list}
extraData={extraData}
/>
)
}
After updating list, I use setExtraData(new Date()) to tell the FlatList to re-render. Because the new time is different from the previous.
Don't get me wrong, #ShubhnikSingh's answer did help, but I retracted it because I found a better solution to this question, long time ago, and finally I remembered to post it here.
Suppose my post item contains these properties:
{
postId: "-L84e-aHwBedm1FHhcqv",
date: 1525566855,
message: "My Post",
uid: "52YgRFw4jWhYL5ulK11slBv7e583",
liked: false,
likeCount: 0,
commentCount: 0
}
Where liked represents whether the user viewing this post has liked this post, which will determine the color of the "like" button (by default, it's gray, but red if liked == true)
Here are the steps to recreate my solution: make "Post" a Component and render it in a FlatList. You can use React's PureComponent if you don't have any props that you pass to your Post such as an array or object that can be deceptively not shallow equal. If you don't know what that means, just use a regular Component and override shouldComponentUpdate as we do below.
class Post extends Component {
// This determines whether a rendered post should get updated
// Look at the states here, what could be changing as time goes by?
// Only 2 properties: "liked" and "likeCount", if the person seeing
// this post ever presses the "like" button
// This assumes that, unlike Twitter, updates do not come from other
// instances of the application in real time.
shouldComponentUpdate(nextProps, nextState) {
const { liked, likeCount } = nextProps
const { liked: oldLiked, likeCount: oldLikeCount } = this.props
// If "liked" or "likeCount" is different, then update
return liked !== oldLiked || likeCount !== oldLikeCount
}
render() {
return (
<View>
{/* ...render other properties */}
<TouchableOpacity
onPress={() => this.props.onPressLike(this.props.postId)}
>
<Icon name="heart" color={this.props.liked ? 'gray' : 'red'} />
</TouchableOpacity>
</View>
)
}
}
Then, create a PostList component that will be in charge of handling the logic for loading posts and handling like interactions:
class PostList extends Component {
/**
* As you can see, we are not storing "posts" as an array. Instead,
* we make it a JSON object. This allows us to access a post more concisely
* than if we stores posts as an array. For example:
*
* this.state.posts as an array
* findPost(postId) {
* return this.state.posts.find(post => post.id === postId)
* }
* findPost(postId) {
* return this.state.posts[postId]
* }
* a specific post by its "postId", you won't have to iterate
* through the whole array, you can just call "posts[postId]"
* to access it immediately:
* "posts": {
* "<post_id_1>": { "message": "", "uid": "", ... },
* "<post_id_2>": { "message": "", "uid": "", ... },
* "<post_id_3>": { "message": "", "uid": "", ... }
* }
* FlatList wants an array for its data property rather than an object,
* so we need to pass data={Object.values(this.state.posts)} rather than
* just data={this.state.posts} as one might expect.
*/
state = {
posts: {}
// Other states
}
renderItem = ({ item }) => {
const { date, message, uid, postId, other, props, here } = item
return (
<Post
date={date}
message={message}
uid={uid}
onPressLike={this.handleLikePost}
/>
)
}
handleLikePost = postId => {
let post = this.state.posts[postId]
const { liked, likeCount } = post
const newPost = {
...post,
liked: !liked,
likeCount: liked ? likeCount - 1 : likeCount + 1
}
this.setState({
posts: {
...this.state.posts,
[postId]: newPost
}
})
}
render() {
return (
<View style={{ flex: 1 }}>
<FlatList
data={Object.values(this.state.posts)}
renderItem={this.renderItem}
keyExtractor={({ item }) => item.postId}
/>
</View>
)
}
}
In summary:
1) Write a custom component (Post) for rendering each item in "FlatList"
2) Override the "shouldComponentUpdate" of the custom component (Post) function to tell the component when to update
Handle the "state of likes" in a parent component (PostList) and pass data down to each child
If you are testing on android than try turning off the developer mode. Or are you hitting some API and updating the post on the server and updating the like button in UI corresponding to the server response? If that is the case do tell me, I too have encountered this and I solved it. Also I have commented the second last line in your code which isn't needed.
// 1. FlatList
<FlatList
...
data={this.state.posts}
renderItem={this.renderPost}
...
/>
// 2. renderPost
renderPost({ item, index }) {
return (
<View style={someStyle}>
... // display other properties of the post
// Then display the "like" button
<Icon
name='favorite'
size={25}
color={item.liked ? 'red' : 'gray'}
containerStyle={someStyle}
iconStyle={someStyle}
onPress={() => this.onLikePost({ item, index })}
/>
...
</View>
);
}
// 3. onLikePost
likePost({ item, index }) {
let { posts } = this.state;
let targetPost = posts[index];
// Flip the 'liked' property of the targetPost
targetPost.liked = !targetPost.liked;
// Then update targetPost in 'posts'
// You probably don't need the following line.
// posts[index] = targetPost;
// Then reset the 'state.posts' property
this.setState({ posts });
}