I have started to work on a form project using React final form, but I am struggling to do a two condition question.
Would anyone knows how to do a double conditional logic on React final form. From this example, the single conditional logic code is given :
const Condition = ({ when, is, children }) => ( <Field name={when} subscription={{ value: true }}>
{({ input: { value } }) => (value === is ? children : null)} </Field>)
However, I don't know how to do a double conditional one that would require 2 different answers from two different questions in the form to be checked before displaying the conditional question.
Thank you :)
You could use useFormState from react-final-form to obtain current form state, so MultiCondition component could look like this:
const MultiCondition = ({ condition, children }) => {
const { values = {} } = useFormState({ subscription: { values: true } });
return condition(values) ? children : null;
};
than somewhere inside the form:
<MultiCondition condition={({ gift, firstName }) => gift && firstName === "Joe"}>
<span>gets rendered when condition is satisfied</span>
</MultiCondition>
Related
I am using ant design on my application. There is form.
useEffect(() => {
...props.client
let client = {
employeeId:props.client?.employee.id
};
form.setFieldsValue(client);
}, [props.client])
On update form 'employeeId' field is not sending to backend, but i want to set its value to null. Does someone now best way of that?
<Form.Item name="employeeId">
<Select allowClear>
{props.employees?.map((employee) => (
<Option key={employee.id} value={employee.id}>{employee.name}</Option>
))}
</Select>
</Form.Item>
I think your useEffect maybe a little buggy. I don't know what the ...props.client is doing there.
It might help to destructure the client prop as it is used so many times. Putting a snippet below to see if it works for you.
const Component = (props) => {
const { client, ...otherProps } = props;
useEffect(() => {
// if id is present, use it, else set null.
// Notice the use of ?? instead of || here.
// That is so that id with value 0 is not ignored as it is falsy in nature
form.setFieldsValue({ employeeId: client?.employee?.id ?? null });
}, [client])
}
So I have a fragment factory being passed into a Display component. The fragments have input elements. Inside Display I have an onChange handler that takes the value of the inputs and stores it in contentData[e.target.id]. This works, but switching which fragment is displayed erases their values and I'd rather it didn't. So I'm trying to set their value by passing in the state object to the factory. I'm doing it in this convoluted way to accomodate my testing framework. I need the fragments to be defined outside of any component and passed in to Display as props, and I need them all to share a state object.
My problem is setting the value. I can pass in the state object (contentData), but to make sure the value goes to the right key in the contentData data object I'm trying to hardcode it with the input's id. Except contentData doesn't exist where the fragments are defined, so I get an error about not being able to reference a particular key on an undefined dataObj.
I need to find a way to set the input values to contentData[e.target.id]. Thanks.
File where fragments are defined. Sadly not a component.
const fragments = (onChangeHandler, dataObj) => [
<Fragment key="1">
<input
type="text"
id="screen1_input1"
onChange={onChangeHandler}
value={dataObj['screen1_input1']} // this doesn't work
/>
one
</Fragment>,
<Fragment key="2">
<input
type="text"
id="screen2_input1"
onChange={onChangeHandler}
value={dataObj['screen2_input1']}
/>
two
</Fragment>
]
Display.js
const Display = ({ index, fragments }) => {
const [contentData, setContentData] = useState({})
const onChange = e => {
// set data
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>{fragments(onChange, contentData)[index]}</Fragment>
);
};
After conversing with you I decided to rework my response. The problem is mostly around the implementation others might provide in these arbitrary fragments.
You've said that you can define what props are passed in without restriction, that helps, what we need to do is take in these nodes that they pass in, and overwrite their onChange with ours, along with the value:
const RecursiveWrapper = props => {
const wrappedChildren = React.Children.map(
props.children,
child => {
if (child.props) {
return React.cloneElement(
child,
{
...child.props,
onChange: props.ids.includes(child.props.id) ? child.props.onChange ? (e) => {
child.props.onChange(e);
props.onChange(e);
} : props.onChange : child.props.onChange,
value: props.contentData[child.props.id] !== undefined ? props.contentData[child.props.id] : child.props.value,
},
child.props.children
? (
<RecursiveWrapper
ids={props.ids}
onChange={props.onChange}
contentData={props.contentData}
>
{child.props.children}
</RecursiveWrapper>
)
: undefined
)
}
return child
}
)
return (
<React.Fragment>
{wrappedChildren}
</React.Fragment>
)
}
const Display = ({ index, fragments, fragmentIDs }) => {
const [contentData, setContentData] = useState(fragmentIDs.reduce((acc, id) => ({
...acc, [id]: '' }), {}));
const onChange = e => {
setContentData({
...contentData,
[e.target.id]: e.target.value
})
};
const newChildren = fragments.map(fragment => <RecursiveWrapper onChange={onChange} ids={fragmentIDs} contentData={contentData}>{fragment}</RecursiveWrapper>);
return newChildren[index];
};
This code outlines the general idea. Here we are treating fragments like it is an array of nodes, not a function that produces them. Then we are taking fragments and mapping over it, and replacing the old nodes with nodes containing our desired props. Then we render them as planned.
So quick and short of the problem. I'm building a step wizard tool for users to create custom templates. There are multiple steps that take text input from the user, my application knows which step the user is entering data on and assigns a stepid to that piece of text.
The problem I'm having is putting that data in state so that I can post it to an API based on a specific format. Here is an example of what I want that data to look like:
state: {
steps : [
{templateStepId : 'this will be the number' , data: 'this will be
the message'},
{templateStepId : 'this will be the number' , data: 'this will be
the message'} //there could be multiple text inputs each
corresponding with a different stepId
]
Here is an example of my code:
addMsg = (msg , stepId) =>{
let message = this.state.message
message[stepId] = msg
this.setState({message})
this.setState( prevState => ({
steps: [...prevState.steps, {templateStepId: stepId, data : msg}]
}));
}`
Ignore the first setState, thats for something else.
But here is where my text is coming from in another component:
handleChange = (text, type) =>{
this.props.addMsg(text, type)
}
render() {
return (
<MuiThemeProvider>
<React.Fragment>
<AppBar title = {this.props.text}/>
<TextField
placeholder = {this.props.text}
onChange = {(e) => {this.handleChange(e.target.value , e.target.name)}}
name= {this.props.templateStepId}
id = 'msg'
/>
<Navigation {...this.props} ></Navigation>
</React.Fragment>
</MuiThemeProvider>
)}
I've also tried this as a solution for my add message component:
let newStep = {templateStepId : stepId , data: msg}
this.setState({
steps : [newStep]
})
The previous code here works well and gives me the state object I want, but as soon as the user moves to another text input component it gets completely overwritten.
Edit per request, here is where the component is being called within the step wizard. As you can see there is just the one text input component called message. This component could be called multiple times depending on the stepId coming from the API which is why the stepId changes for each input.
<MuiThemeProvider>
<React.Fragment>
<StepWizard >
{
this.props.templateSteps.map((data, idx) => {
if (data.stepTypeId === "dc448967-7fad-42cf-8706-bbe1d124ceac") {
return <Message
addMsg = {this.props.addMsg}
text = {data.name}
templateStepId = {data.templateStepId}
message = {this.props.message}
startOver = {() => {this.props.startOver()}}
key={idx}
/>
Here is my state from the console in devtools :
So as you can see, it's creating a new object for each letter input. I would appreciate any help at this point as I'm beating my head against the wall trying to figure this out!
handleChange is getting called every time you make a change to the input. This is adding one more step to the state. You could check if the stepId is existing in the state and add data to that stepId only.If the stepId is not present you can add a record.
function getStepIdIndex(arr, id) {
for (let i = 0; i < arr.length; i++) {
if (arr[i].templateStepId === id) {
return i;
}
}
return -1;
}
let addMsg = (msg, stepId) => {
let stepIdIndex = getStepIdIndex(this.state.steps, stepId);
if (stepIdIndex === -1) {
this.setState(prevState => ({
steps: [...prevState.steps, {
templateStepId: stepId,
data: msg
}]
}));
} else {
this.state.steps[stepIdIndex].data=msg;
this.setState({steps:[...this.state.steps]});
}
}
So I have been working hard for days and searching on how to do this. I have a Material UI table in my React App. I want to load a table where if my user has entries in the selected array it will prerender the checks in the DOM. The selected array is populated with the entries I want but my table which uses a onClick I think needs an event to trigger the DOM to render the check. This is relevant part of my table body.
<TableBody>
{this.props.competitorData.map(competitor => {
const isSelected = this.props.isSelected(competitor.key);
return (
<TableRow
hover
onClick={() => this.props.handleClick(competitor)}
role="checkbox"
aria-checked={isSelected}
tabIndex={-1}
key={competitor.key}
selected={isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={isSelected} />
</TableCell>
I have a toggle that loads my table. It fills the selected the array with the subset of data I want trigger in componentWillMount. (it's 2 tables, tier1 and tier 2).
componentWillMount() {
this.renderChecks(this.props.team)
}
renderChecks(team) {
const { selected1 } = this.state;
const { selected2 } = this.state;
let newSelected1 = [];
let newSelected2 = [];
team.map(teammate => {
if (teammate.tier === "1") {
newSelected1 = newSelected1.concat(selected1, teammate.key)
} else if (teammate.tier === "2") {
newSelected2 = newSelected2.concat(selected2, teammate.key)
}
this.setState({ selected1: newSelected1 });
this.setState({ selected2: newSelected2 });
})
}
Essentially I need a way to render isSelected based of another list that is the smaller list (team is a subset of competitorData) that has the same keys. Ive tried so many things it's to many to list here. Im looking for help on what to do to make this work because nothing has worked and Im not sure what direction I should be going on in at this point. I've tried a lot of things that seem to cause instability in the render. Essentially I've tried to make the isSelected more state based but setting and resetting that state with inline functions like
{() => this.myFunctionThatUpdatesIsSelectedState(Key)}
These blow up in render. Sometimes cause an ugly infinite loop.
Update
Based on #Eld0w post below this does render my subset of checks.
checkKeys(val) {
return this.props.team.some(teammate => {
return val.key === teammate.competitorKey;
});
}
getCompetitors = () => {
const { competitorData, team } = this.props;
return competitorData.map(
value => ({
value,
isSelected: this.checkKeys(value)
})
)
}
Tables looks like this now.
<TableBody>
{this.getCompetitors().map(competitor => {
console.log('MYCOMPETITOR2::', competitor);
return (
<TableRow
hover
onClick={event => this.props.handleClick(event, competitor.value)}
role="checkbox"
aria-checked={competitor.isSelected}
tabIndex={-1}
key={competitor.value.key}
selected={competitor.isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={competitor.isSelected} />
</TableCell>
There is small issues I didn't see coming. Now my table renders only the preselected checks since im not using my previous isSelected function which was:
isSelected1 = key => this.state.selected1.indexOf(key) !== -1;
Basically i need to render the existing checks but maintain the standard isSelected function somewhere in the process as well. If I think of something or post anything about it I'll update here as well. Further input is obviously welcome.
I think i need to load my team into my selected array then run my standard isSelected function. But this is where I seem to run into trouble since that is state based. Render goes crazy on me.
Final Update
So it was late last night. I just needed to change the criterion to make this whole thing work. I load my team array in the local state selected array. Then performed isSelected property check on my competitor. Now it loads my preselected and the user can then edit selects in the table from that point.
Final Solution
Load the preselect team into the local selected state array.
componentWillMount() {
this.renderChecks(this.props.team);
}
I have tiered tables. That is just some business logic (not important here). teammate.competitorKey is the key I store that is same key as the larger table, which is competitorData. I need that to get the compares to work.
renderChecks(team) {
const { selected } = this.state;
let newSelected = [];
team.map(teammate => {
if (teammate.tier === '1') {
newSelected = newSelected.concat(selected, teammate.competitorKey)
this.setState({ selected: newSelected });
}
})
}
getCompetitor can now just verify the value key exist in the array using includes
getCompetitors = () => {
const { competitorData, team } = this.props;
console.log('THISSTATESELECTED:::', this.state.selected)
return competitorData.map(
value => ({
value,
isSelected: this.state.selected.includes(value.key)
})
)
}
And Final Table looks like
<TableBody>
{this.getCompetitors().map(competitor => {
return (
<TableRow
hover
onClick={event => this.handleClick(event, competitor.value)}
role="checkbox"
aria-checked={competitor.isSelected}
tabIndex={-1}
key={competitor.value.key}
selected={competitor.isSelected}
>
<TableCell padding="checkbox">
<Checkbox checked={competitor.isSelected} />
</TableCell>
I know this is a lot of writing but is spent a lot of time trying to get all this working. I hope it helps someone looking to do. I will look into making this more redux worth and possibly going the reselect route to optimize but for now im going to enjoy a working table for a day. Thank you again #Eld0w !!
So basically, you want to add an isSelected props to your competitors array depending on another array's values. Avoid using state, it's only props combinations.
The straightforward solution
Instead of mapping directly your competitor's array, map on a function returning an array.
getCompetitors = () => {
const { competitors, team } = this.props;
return competitors.map(
competitor => ({
...competitor,
isSelected: // criterion
})
)
}
Basically, what this does is destructuring the object and adding a new property isSelected thanks to the spread operator ... (ES6)
In your render then call this.getCompetitors().map(competitor => ...) instead of this.props.competitors.
Optimize this solution
You will want to use a plugin such as reselect to avoid any useless render operation.
const competitorSelector = createSelector(
props => props.competitors,
props => props.team,
(competitors, team) => competitors.map(
competitor => ({
...competitor,
isSelected: // criterion
})
)
)
and then use it like this in your render :
this.competitorSelector(this.props)
You will also need to use competitor.isSelected instead of isSelected since the property is now part of your competitor's properties.
First of all this is a simplified example: Codepen Project
I am building an edit form in react, which checks if there are any changes.
You can only save the form if there are any changes and any changes you made will be shown by changing the style (border-left) on the matching input element. It looks like this
To do that I am saving the original data/ state in the component state in the componentDidMount method and compare that to the state of the different inputs.
componentDidMount() {
// if the project is accessed from home and is not a new project, project data will be passed along
if (this.props.project) {
this.setState({
name: this.props.project.name,
tags: this.props.project.tags
}, this.setInitialState)
} else if (this.props.edit && this.props.match.params.id) {
// instead of an api call to get project data, if the project is accessed directly by url
const project = projects.find((project) => project.name === this.props.match.params.id)
this.setState({
name: project.name,
tags: project.tags
}, this.setInitialState)
}
// if there are no project data or an edit prop, it's a new project and the initialState remains empty
}
On each Input Change the input Value is compared to the initialState:
compareInputData() {
const formFields = {
name: {
ref : this.name,
changed : false
},
tags: {
ref : this.tagList,
changed : false
}
}
const state = this.state
const first = this.state.initialState
const nameHasChanged = state.name !== first.name
const tagsHaveChanged = state.tags.length !== first.tags.length
nameHasChanged
? ( formFields.name.changed = true )
: ( formFields.name.changed = false )
tagsHaveChanged
? ( formFields.tags.changed = true )
: ( formFields.tags.changed = false )
nameHasChanged || tagsHaveChanged
? (this.setState({
isChanged: true
}))
: (this.setState({
isChanged: false
}))
this.handleChangedInputStyles(formFields)
}
If there are changes the styling of the matching element is changed:
handleChangedInputStyles(formFields) {
const formFieldKeys = Object.keys(formFields)
formFieldKeys.map(key => {
formFields[key].changed
? formFields[key].ref.style.borderLeft = `2px solid orange`
: formFields[key].ref.style.borderLeft = '1px solid black'
})
}
That is working the way I want it to on normal input fields, but I am also saving related tags as an array, which are displayed as a list. Whenever I update that list (this.state.tags) my original state for the tags is being updated as well (this.state.initialState.tags), which means that I cannot pick up changes in my tag List.
However it does work if I am creating adding a tag to a new project instead of editing an existing one...
I have no idea how to fix that issue, since I don't really know what's causing it and I would love some help.
Thank you for reading through this post :)
Do not store this.state.initialState in the state. Store it in a member instead. For instance:
constructor(props) {
this.initialState = Object.assign({}, whatever...);
this.initialState.tags = [].concat(this.initialState.tags); // Keep a shallow copy of this array.
}
Note: Internally, React may modify the tags array. If you keep a copy, that copy will not be modified.