My task is to do create a form that will get some fields from STRAPI api.
A form will be component, that will be passed to different pages.
So in each page there will be same form but with different fields based on Strapi.
Form structure will look like this:
const apiCall = {
dropdown: [
// there can be 0 objects or infinite objects, based on strapi api call
{
question: "how are you?",
answers: [
{name: "Good"},
{name: "Bad"}
// infinite number of answers
]
},
{
question: "how are you?",
answers: [
{name: "Good"},
{name: "Bad"}
// infinite number of answers
]
}
]
}
Then in Formik i need to do something like this:
// DROPDOWNS
{apiCall.dropdown?.map(({ answers, question, id}) => (
<div key={id} className="mb-5">
<h2 className="h3 mb-2">
{question}
</h2>
<FormSelect
key={id}
name={} //??? IDK
label={t('validation.oneOfOptions')}
options={
answers?.map(a => (
{value: a.name, label: a.name}
))
}
className={formFieldClasses}
/>
</div>
How should i initialize formik values and validate data? I am using Formik component.
Then i need to send data to another api call where we are going to send emails with answers. But IDK how can i send dynamic data when i dont know data that will come from apiCall?
Normally i will do something like this:
const initialValues: LostFormValues = {
category: CategoryValues.WALLET,
secCategory: SecCategoryValues.TRAIN,
priority: PriorityValues.HIGH,
cityFrom: null,
cityTo: null,
date: today,
ticketNumber: user?.user?.accountCode || '',
firstName: user?.user?.firstName || '',
surname: user?.user?.surname || '',
email: user?.user?.email || '',
phoneNumber: user?.user?.phoneNumber || '',
description: '',
files: [],
agreeWithTerms: false,
};
and then:
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={yup.object().shape({
category: yupFieldEnum(CategoryValues),
secCategory: yupFieldEnum(SecCategoryValues),
date: yupField.date,
ticketNumber: yup.string().nullable(), // not required
priority: yupField.string,
cityFrom: yupField.autoComplete,
cityTo: yupField.autoComplete,
firstName: yupField.string,
surname: yupField.string,
email: yupField.email,
description: yupField.string,
agreeWithTerms: yupField.checkbox,
})}
onSubmit={async (values) => {
const isSent = await sendToExternalService(values);
}}
>
But now i cant just write variables and then send them.. i need to write them dynamicaly
so this quite a big question, and a lot of unnecessary info, not sure I'm getting correct all the points however based on api you want to validate answers so if you have field answers you should have field answer, basically what i would do:
const apiCall = [{..},{..}]
const ParentComponent = () => {
const [answers, setAnswers] = useState({})
return ( <>
{apiCall.dropdown.map(item => (
<SelectComponent item={item} setAnswers={setAnswers} key={..}/>
)}
}
const SelectComponent = ({item}) => (
const formik = useFormik({
initialValues: {
answer: null,
}
validationSchema: Yup.object().shape({
answer: Yup.string().oneOf(item.answers.map(({name})=> name).nullable()}),
onSubmit: (values) => setAnswers(answers => (
{...answers, [item.question]:values.answer})
);
})
return (
<>...
<FormSelect name='answer'
value={formik.values.answer} options={
item.answers.map(a => (
{value: a.name, label: a.name}
))
}
/>
<Button onClick={() => formik.onSubmit()}>Ok</Button>
</>)
)
So now you keep your answers as key value pairs in parent component and you validate exact question, based on what in the api. Note there is many ways to go from here depending on how you would pass data to api...
Not sure if it would work out of the box if you dynamically change apiCall on fly but this should be also possible to handle
Related
I want to allow the user to choose a teacher's name from a drop down and insert that teacher's "teacher_name" and "teacher_id" as 2 separate fields in Firestore database.
I have the following input field which creates a drop down by "teacher_name". Now I can either pass "teacher_name" OR "teacher_id" under optionValue to insert that field. Is there a way to insert both "teacher_name" AND "teacher_id" as 2 separate fields?
<ReferenceInput
label="Teacher"
source="teacher_name"
reference="teachers"
>
<AutocompleteInput
optionText="teacher_name"
optionValue="teacher_name"
defaultValue={null}
/>
</ReferenceInput>
My Firestore looks like this:
Collection name : teachers
Document Structure :
{
teacher_id : "XXX",
teacher_name : "XXX",
other_fields : "XXX",
}
As mentioned in the Answer which explains How to have the whole object act as optionValue in a SelctInput in react-admin as :
This is possible using parse and format. Format makes sure the
form options in the html are just the name strings. parse translates
the selected option into the format you db needs.
Example :
const choices = [
{ id: '1', name: 'Programming' },
{ id: '2', name: 'Lifestyle' },
{ id: '3', name: 'Photography' },
];
<SelectInput
source="category"
choices={choices}
format={(c) => c.name}
parse={(name) => choices.find((c) => c.name === name)}
/>
There is another answer which explains how to input and create two (or more than two) fields using React-Admin as:
allows users to select an existing record related
to the current one (e.g. choosing the author for a post). if that you
want to create a new record instead. You can do so via the onCreate
prop, as explained in the doc:
import { AutocompleteInput, Create, SimpleForm, TextInput } from 'react-admin';
const PostCreate = () => {
const categories = [
{ name: 'Tech', id: 'tech' },
{ name: 'Lifestyle', id: 'lifestyle' },
]; return (
<Create>
<SimpleForm>
<TextInput source="title" />
<AutocompleteInput
onCreate={(filter) => {
const newCategoryName = window.prompt('Enter a new category', filter);
const newCategory = { id: categories.length + 1, name: newCategoryName };
categories.push(newCategory);
return newCategory;
}}
source="category"
choices={categories}
/>
</SimpleForm>
</Create>
);
}
For more information, you can refer to the Official documentation which explains about the input components and common input props.
I've tried even manually setting the default values like in the documentation but no dice. I'm not sure if it's a styling issue or what. So below I posted what I have along with a screenshot.
<Select
components={animatedComponents}
getOptionLabel={convertToLabel}
getOptionValue={option => option.resource_name}
isMulti
onChange={changeEvent}
options={users}
theme={theme => ({
...theme,
borderRadius: 0
})}
defaultValue={(props.value || []).map(convertToValue)}
value={(props.value || []).map(convertToValue)}
/>
convertToValue function
const convertToValue = props => {
return {
label: `${props.name} ${props.family_name}`,
value: props.resource_name
};
};
convertToLabel function
const convertToLabel = props => {
return `${props.name} ${props.family_name}`;
};
changeEvent function
const changeEvent = (selectedOption, i) => {
let option = {
name: "reviewers",
value: selectedOption
};
update({ target: option });
};
users & props objects
users:
[
{
resource_name: "facebook_user1",
name: "Joe",
family_name: "Dirt"
},
{
resource_name: "facebook_user2",
name: "Trident",
family_name: "White"
}
]
props:
{
field: "placeholder",
fieldType: "placeholderType"
value:[
{
resource_name: "facebook_user1",
name: "Joe",
family_name: "Dirt"
},
{
resource_name: "facebook_user2",
name: "Trident",
family_name: "White"
}
]
}
What I see on my screen.
It is extremely difficult to tell exactly what your issue is, without seeing the actual JSX of your select render. Here are a few issues I see, looking at your question, and some hard guesses at what might be happening.
You should show us the full JSX render of your Select implementation
You never show us what your defaultValue prop looks like, but
remember that value is expected to be equal to one of your
options, not just an option 'value'
Your label and option getter
methods signature should be getOptionLabel = (option) => string and
getOptionValue = (option) => string. You've used props, which
might conflict with parent scope, in your instance.
You probably want
your convertToValue method signature to line up with those as well.
Your onChange event method signature doesn't line up with
React-Select, and may be causing you pain. See my answer to this
recent question for help on this.
Was wondering if Formik has a native solution for identifying the addition and deletion (and update) of FieldArray in the form ?
I have the code on sandbox here https://codesandbox.io/s/jn7x2m75o9 ( based on the original Formik Array example # https://github.com/jaredpalmer/formik/blob/master/examples/Arrays.js )
but also the relevant part here :
With an Initial state of 3 friend defined, how can I know in my onSubmithandler which one were modified,deleted,updated.
import React from "react";
import { Formik, Field, Form, ErrorMessage, FieldArray } from "formik";
const initialValues = {
friends: [
{
name: "Friend_A",
email: "email_A#somewhere.com"
},
{
name: "Friend_B",
email: "email_B#somewhere.com"
},
{
name: "Friend_C",
email: "email_C#somewhere.com"
}
]
};
const mySubmit = values => console.log();
const SignIn = () => (
<div>
<h1>Invite friends</h1>
<Formik
initialValues={initialValues}
onSubmit={values => {
var itemRemoved = values.GetItemRemoveFromArray; // This is what I'm looking for
console.log(itemRemoved);
// Would print Friend_A
var itemAdded = values.GetItemAddedFromArray; // This is what I'm looking for
console.log(itemAdded);
// Would print New_Friend
var itemUpdated = values.GetItemUpdatedInArray; // This is what I'm looking for
console.log(itemUpdated);
// Would print Friend_C
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
}, 500);
}}
render={({ values }) => (
<Form>
<FieldArray
name="friends"
render={({ insert, remove, push }) => (
<div>
{values.friends.length > 0 &&
values.friends.map((friend, index) => (
<div className="row" key={index}>
<div className="col">
<label htmlFor={`friends.${index}.name`}>Name</label>
<Field
name={`friends.${index}.name`}
placeholder="Jane Doe"
type="text"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<label htmlFor={`friends.${index}.email`}>Email</label>
<Field
name={`friends.${index}.email`}
placeholder="jane#acme.com"
type="email"
/>
<ErrorMessage
name={`friends.${index}.name`}
component="div"
className="field-error"
/>
</div>
<div className="col">
<button
type="button"
className="secondary"
onClick={() => remove(index)}
>
X
</button>
</div>
</div>
))}
<button
type="button"
className="secondary"
onClick={() => push({ name: "", email: "" })}
>
Add Friend
</button>
</div>
)}
/>
<button type="submit">Invite</button>
</Form>
)}
/>
</div>
);
export default SignIn;
So if with the above a user where to :
Click on the X below Friend_A
Modify Friend_C email to email_C#nothere.com
Click "Add Friend"
Enter value Name: New_Friend_X and email: XX#YY.com
Click "Add Friend"
Enter value Name: New_Friend_Z and email: Friend_Z#coolplace.com
Click "X" button below newly entered "New_Friend_X"
Click "Invite"
in my mySubmit I'm looking for a way to easily get :
Friend_A was Removed
Friend_C was Modified
New_Friend_Z was added (was not in the original initialValues to formik)
(I Don't care about New_Friend_X. No need to know it was added/removed )
Point of this is to minimize rest call to the back end to create/update entity/link and also I really dont want to write my own "secondary state" in the onClick handler of the remove button before calling the remove(index) handler provided by Formik to track what need to be deleted from the DB.
Its not built into Formik, but it is not hard to do in javascript.
First, understand that Formik clones the object you give to initialValues. So in onSubmit, you will compare the final value to your original object.
The incoming data:
const initialFriends = [
{
name: "Friend_A",
email: "email_A#somewhere.com"
},
{
name: "Friend_B",
email: "email_B#somewhere.com"
},
{
name: "Friend_C",
email: "email_C#somewhere.com"
}
];
const initialValues = { friends: initialFriends };
Modified Formik declaration:
<Formik initialValues={initialValues}
...
onSubmit={values => {
const { added, deleted, changed } = addDeleteChange(
initialFriends,
values.friends
);
setTimeout(() => {
alert(
"Added: " + JSON.stringify(Object.fromEntries(added.entries()))
);
alert(
"Deleted: " + JSON.stringify(Object.fromEntries(deleted.entries()))
);
alert(
"Changed:" + JSON.stringify(Object.fromEntries(changed.entries()))
);
alert(JSON.stringify(values, null, 2));
}, 500);
}}
...
Helper functions:
function partition(array, filter) {
let pass = [],
fail = [];
array.forEach(e => (filter(e) ? pass : fail).push(e));
return [pass, fail];
}
const addDeleteChange = (in1, out1) => {
let inMap = new Map(in1.map(f => [f.name, f]));
let outMap = new Map(out1.map(f => [f.name, f]));
let inNames = new Set(inMap.keys());
let outNames = new Set(outMap.keys());
let [kept, added] = partition(out1, f => inNames.has(f.name));
let deleted = in1.filter(f => !outNames.has(f.name));
//alert(JSON.stringify(Object.fromEntries(deleted.entries())));
let changed = kept.filter(f => f.email !== inMap.get(f.name).email);
//alert(JSON.stringify(Object.fromEntries(changed.entries())));
return { added: added, deleted: deleted, changed: changed };
};
Code in codesandbox
NOTE: If you change the name of a friend, that will appear as a delete of original friend and an add of a new friend.
A more robust solution would be to add a (hidden) "id" field to each friend. Then instead of comparing name, would compare id.
That requires generating a new id as add each friend.
Im developing a Soccer Betting system. I want to make a form dynamically and keep the objects in the state with redux-forms.
My basic entity is a Match: Who has a home_team, a away_team, and inputs with a home_result and a away_result.
matches = [
{
name: 1,
type: "group",
home_team: 1,
away_team: 2,
home_result: null,
away_result: null,
date: "2018-06-14T18:00:00+03:00",
stadium: 1,
channels: [],
finished: false
},
{
name: 2,
type: "group",
home_team: 3,
away_team: 4,
home_result: null,
away_result: null,
date: "2018-06-15T17:00:00+05:00",
stadium: 12,
channels: [],
finished: false
},
I want to fill it in a Redux-Form, but get stuck in the best way to make it. I want too that, every time a user changes the values on the input, this is reflected in the State-Json.
To dynamically create a form, you will need to build your data differently.
You need fields object that will look like this(with the matches id):
const fieldsObject = ['match1', 'match2', 'match3']
And a initialValues object that will look like this:
const resultsObject = {
match1_home: 1,
match1_away: 3
}
And so on. Then, the Form will initial its fields based on the initialValues and the names of the fields. The code:
const MyForm = (props) => {
const { handleSubmit, fields } = props;
return (
<form onSubmit={handleSubmit}>
{fields.map(match => (
<div key={match}>
<Field
name={`${match}_home`}
component="input"
/>
<Field
name={`${match}_away`}
component="input"
/>
</div>
))}
<button type="submit">Submit</button>
</form>
)
}
And the usage will look like this:
<MyForm initialValues={resultsObject} fields={fieldsObject} onSubmit={this.submitForm}/>
i am planning to build a generic filter like Gbif Have.
My question is how to approach this problem.
I like to use ReactJs for this project.
What other technology i need to look into along with React and redux in order to design such a generic filter.
I try to design this filter using React and redux only.
In my approach, i try to maintain the query parameter inside the state variable of the get_data method, in which i am fetching the data from the server. As somebody click on any filter button, then i pass custom event from that filter component along with query parameter and handle this event in get_data method. In get_data method again i am saving this value in get_data state parameter and again getting the new filtered data.
Now the Problem with above approach is that as the number of parameter increases it become very difficult to maintain.
my get_data constructor look like this.
constructor(props){
super(props);
this.state={
params:{
max:10,
offset:0,
taxon:[],
sGroup:[],
classification:undefined,
userGroupList:[],
isFlagged:undefined,
speciesName:undefined,
isMediaFilter:undefined,
sort:"lastRevised",
webaddress:""
},
title:[],
groupName:[],
userGroupName:[],
view:1
}
this.props.fetchObservations(this.state.params)
this.loadMore=this.loadMore.bind(this);
};
The way i am getting data from filter component is something like this.
this is my handleInput method which fire onSelect method from one of the filter.
handleInput(value,groupName){
this.setState({
active:true
})
this.props.ClearObservationPage();
var event = new CustomEvent("sGroup-filter",{ "detail":{
sGroup:value,
groupName:groupName
}
});
document.dispatchEvent(event);
};
the way i am handling this event in my get_data component is look something like this.
sGroupFilterEventListner(e){
const params=this.state.params;
if(!params.sGroup){
params.sGroup=[];
}
console.log("params.sGroup",params.taxon)
params.sGroup.push(e.detail.sGroup)
params.sGroup=_.uniqBy(params.sGroup)
const groupName=this.state.groupName;
var titleobject={};
titleobject.sGroup=e.detail.sGroup;
titleobject.groupName=e.detail.groupName;
groupName.push(titleobject);
let newgroupname=_.uniqBy(groupName,"sGroup")
params.classification=params.classification;
let isFlagged=params.isFlagged;
let speciesName=params.speciesName;
let MediaFilter=params.isMediaFilter;
let taxonparams=params.taxon;
taxonparams= taxonparams.join(",");
let sGroupParams=params.sGroup;
sGroupParams=sGroupParams.join(",");
let userGroupParams=params.userGroupList;
userGroupParams=userGroupParams.join(",");
let newparams={
max:10,
sGroup:sGroupParams,
classification:params.classification,
offset:0,
taxon:taxonparams,
userGroupList:userGroupParams,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
}
this.props.fetchObservations(newparams);
this.setState({
params:{
max:10,
sGroup:params.sGroup,
classification:params.classification,
offset:0,
taxon:params.taxon,
userGroupList:params.userGroupList,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
},
groupName:newgroupname
})
}
I registered and unRegistered the sGroupFilterEventListner in my componentDidMount and componentunmount method.
Presently i am also not considering the case where if somebody type in url bar, the filter panel change automatically.
Please consider all the above scenario and suggest me a generic way to do the same. thanks.
My Current Filter Panle look like this
Here's a quick example (React only, no Redux) I whipped up with a dynamic number of filters (defined in the filters array, but naturally you can acquire that from wherever).
const filters = [
{ id: "name", title: "Name", type: "string" },
{
id: "color",
title: "Color",
type: "choice",
choices: ["blue", "orange"],
},
{
id: "height",
title: "Height",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
{
id: "width",
title: "Width",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
];
const filterComponents = {
string: ({ filter, onChange, value }) => (
<input
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
/>
),
choice: ({ filter, onChange, value }) => (
<select
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
size={1 + filter.choices.length}
>
<option value="">(none)</option>
{filter.choices.map(c => (
<option value={c} key={c}>
{c}
</option>
))}
</select>
),
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = { filters: {} };
this.onChangeFilter = this.onChangeFilter.bind(this);
}
onChangeFilter(filterId, value) {
const newFilterState = Object.assign({}, this.state.filters, {
[filterId]: value || undefined,
});
this.setState({ filters: newFilterState });
}
renderFilter(f) {
const Component = filterComponents[f.type];
return (
<div key={f.id}>
<b>{f.title}</b>
<Component
filter={f}
value={this.state.filters[f.id]}
onChange={this.onChangeFilter}
/>
</div>
);
}
render() {
return (
<table>
<tbody>
<tr>
<td>{filters.map(f => this.renderFilter(f))}</td>
<td>Filters: {JSON.stringify(this.state.filters)}</td>
</tr>
</tbody>
</table>
);
}
}
ReactDOM.render(<App />, document.querySelector("main"));
body {
font: 12pt sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main/>
(originally on https://codepen.io/akx/pen/JyemQQ?editors=0010)
Hope this helps you along.