Mobx react form add fields on click depending on condition - javascript

I have a function that generates fields for a form like so:
export const makeFields: Function = (itemData: Object) => {
return [
{
// PROJECT DETAIL SECTION
name: 'chooseAccount',
label: 'Choose Account',
fields: [{
name: 'account',
label: 'Choose Trading Account',
rules: 'required',
...(itemData ? { value: itemData.trading_account ? itemData.trading_account.name : null } : null)
}]
},
{
name: 'projectDetails',
label: 'Project detail',
fields: [
{
name: 'projectCode',
label: 'Project code',
rules: 'required',
...(itemData ? { value: itemData.code } : null)
},
...
and the component that uses this function for the form fields:
...
export default class ProjectForm extends React.Component<*> {
props: TypeProps;
getMode = () => this.props.mode
componentDidMount() {
const projectDetailsStore: Object = this.props.projectDetailsStore;
this.getMode() === 'edit'
?
projectDetailsStore.loadProjectDetails(this.props.projectId)
:
projectDetailsStore.resetStore();
}
#computed get form(): Object {
const itemData: Object = (typeof this.props.itemData === 'undefined') ? {} : this.props.itemData;
const fields: Array<*> = makeFields(this.props.projectDetailsStore.details);
return this.getMode() === 'edit'
? projectEdit(fields, itemData)
: projectCreate(fields);
}
render(): React.Element<*> {
const t: Function = this.props.t;
const TAmodel: AutoCompleteData = new AutoCompleteData(autoCompleteTradingAccounts);
const Pmodel: AutoCompleteData = new AutoCompleteData(autoCompleteProject);
const projectDetailsStore: Object = this.props.projectDetailsStore;
this.form.add(
{
name: 'test'
}
)
console.log(this.form)
return (
<PageWrapper>
{projectDetailsStore.loadingProjectDetails
?
<Loader />
:
<FormWrapper form={this.form}>
<form>
<FormSection form={this.form} section="chooseAccount">
<InputLabel htmlFor="account">
{t('projectForm: Choose trading account')}
</InputLabel>
<ElectroTextField field={this.form.$('chooseAccount.account')} />
{/* <ElectroAutoComplete
field={this.form.$('chooseAccount.account')}
form={this.form}
props={{
model: TAmodel
}}
/> */}
</FormSection>
<FormSection form={this.form} section="projectDetails">
<ElectroTextField field={this.form.$('projectDetails.projectCode')} />
<ElectroTextField field={this.form.$('projectDetails.projectName')} />
...
I would like to add some fields to the form based on a condition. I have tried the following:
this.form.add(
{
name: 'test'
}
)
it doesn't throw an error but nothing happens.
The add method takes an object as per the docs (https://foxhound87.github.io/mobx-react-form/docs/api-reference/fields-methods.html). Ideally I would like a click event to fire the add method and add the newly created field.

this.form is neither a React state nor a MobX observable, so that nothing happens when you change its value.
To get it to work, you should create an observable form field that is initialized by makeFields, and use an action function to change its value, and then use observer to re-render.
If you are not quite familiar with mentioned above, read React & MobX official tutorials first.

Related

Passing value to state using react-select

I'm new to react and trying to learn on my own. I started using react-select to create a dropdown on a form and now I'm trying to pass the value of the option selected. My state looks like this.
this.state = {
part_id: "",
failure: ""
};
Then in my render
const {
part_id,
failure
} = this.state;
My form looks has 2 fields
<FormGroup>
<Label for="failure">Failure</Label>
<Input
type="text"
name="failure"
placeholder="Failure"
value={failure}
onChange={this.changeHandler}
required
/>
</FormGroup>
<FormGroup>
<Label for="part_id">Part</Label>
<Select
name="part_id"
value={part_id}
onChange={this.changeHandler}
options={option}
/>
</FormGroup>
the changeHandler looks like this
changeHandler = e => {
this.setState({ [e.target.name]: e.target.value });
};
The change handler works fine for the input but the Select throws error saying cannot read property name. I went through the API docs and came up with something like this for the Select onChange
onChange={part_id => this.setState({ part_id })}
which sets the part_id as a label, value pair. Is there a way to get just the value? and also how would I implement the same with multiselect?
The return of react-select onChange event and the value props both have the type as below
event / value:
null | {value: string, label: string} | Array<{value: string, label: string}>
So what the error means is that you can't find an attribute of null (not selected), or any attributes naming as name (you need value or label)
For multiple selections, it returns the sub-list of options.
You can find the related info in their document
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' },
{ value: 'vanilla', label: 'Vanilla' },
];
Update
For your situation (single selection)
option having type as above
const option = [
{value: '1', label: 'name1'},
{value: '2', label: 'name2'}
]
state save selected value as id
changeHandler = e => {
this.setState({ part_id: e ? e.value : '' });
};
pick selected option item via saved id
<Select
name="part_id"
value={option.find(item => item.value === part_id)}
onChange={this.changeHandler}
options={option}
/>
For multiple selections
state save as id array
changeHandler = e => {
this.setState({ part_id: e ? e.map(x => x.value) : [] });
};
pick via filter
<Select
isMulti // Add this props with value true
name="part_id"
value={option.filter(item => part_id.includes(item.value))}
onChange={this.changeHandler}
options={option}
/>
onChange function is a bit different in react-select
It passes array of selected values, you may get first one like
onChange={([selected]) => {
// React Select return object instead of value for selection
// return { value: selected };
setValue(selected)
}}
I have tried the above solutions but some of these solutions does update the state but it doesn't gets rendered on the Select value instantly.
Herewith a demo example:
this.state = {
part_id: null,
};
handleUpdate = (part_id) => {
this.setState({ part_id: part_id.value }, () =>
console.log(`Option selected:`, this.state.part_id)
);
};
const priceOptions = [
{ value: '999', label: 'Item One' },
{ value: '32.5', label: 'Item Two' },
{ value: '478', label: 'Item Three' }
]
<Select
onChange={this.handleUpdate}
value={priceOptions.find(item => item.value === part_id)}
options={priceOptions}
placeholder={<div>Select option</div>}
/>

How can I get the current value of an input which is part of an array of objects?

So I am trying to fill an array with data. I am facing one problem.
1 - I am trying to find an index for every proper key in the array of objects. But I get an error once a new input is added. Yes, I am adding inputs dynamically too.
The inputs gets added well.
This is for example, how the data should look before been send to the backend. Like this is how the final object should be shaped:
{
"previous_investments" : [
{"name" : "A Name", "amount" : 10000},
{"name" : "Some Name", "amount" : 35000}
]
}
Seems to be easy but I am having a hard time.
This is how my main component looks:
const PreviousInvestments = ({
startupFourthStepForm,
previousInvestmentsInputs,
startupFourthStepFormActionHandler,
previousInvestmentsInputsActionHandler,
}) => {
const handleMoreInputs = async () => {
await startupFourthStepFormActionHandler(
startupFourthStepForm.previous_investments.push({
name: '',
amount: undefined,
}),
);
await previousInvestmentsInputsActionHandler(
`previous_investments-${previousInvestmentsInputs.length}`,
);
};
return (
<div className="previous-investments-inputs">
<p>Previous Investments</p>
{previousInvestmentsInputs.map((input, index) => (
<div key={input}>
<FormField
controlId={`name-${index}`}
onChange={e => {
startupFourthStepFormActionHandler({
// HERE IS WHERE I THINK I AM PROBABLY FAILING
previous_investments: [{ name: e.currentTarget.value }],
});
}}
value={startupFourthStepForm.previous_investments[index].name}
/>
</div>
))}
<Button onClick={() => handleMoreInputs()}>
+ Add more
</Button>
</div>
);
};
export default compose(
connect(
store => ({
startupFourthStepForm:
store.startupApplicationReducer.startupFourthStepForm,
previousInvestmentsInputs:
store.startupApplicationReducer.previousInvestmentsInputs,
}),
dispatch => ({
previousInvestmentsInputsActionHandler: name => {
dispatch(previousInvestmentsInputsAction(name));
},
startupFourthStepFormActionHandler: value => {
dispatch(startupFourthStepFormAction(value));
},
}),
),
)(PreviousInvestments);
In the code above, this button adds a new input and also it adds a new object to the array using the function handleMoreInputs:
<Button onClick={() => handleMoreInputs()}>
+ Add more
</Button>
This is the reducer:
const initialState = {
startupFourthStepForm: {
previous_investments: [{ name: '', amount: undefined }],
},
previousInvestmentsInputs: ['previous_investments-0'],
}
const handlers = {
[ActionTypes.STARTUP_FOURTH_STEP_FORM](state, action) {
return {
...state,
startupFourthStepForm: {
...state.startupFourthStepForm,
...action.payload.startupFourthStepForm,
},
};
},
[ActionTypes.PREVIOUS_INVESTMENTS_INPUTS](state, action) {
return {
...state,
previousInvestmentsInputs: [
...state.previousInvestmentsInputs,
action.payload.previousInvestmentsInputs,
],
};
},
}
The funny thing is that I am able to type in the first input and everything goes well. But once I add a new input, a second one, I get this error:
TypeError: Cannot read property 'name' of undefined
43 | controlId={startupFourthStepForm.previous_investments[index].name}
Wo what do you think I am missing?
The handler for ActionTypes.STARTUP_FOURTH_STEP_FORM is defined to be invoked with an object for startupFourthStepForm in the payload and effectively replace it.
Where this handler is invoked, you need to ensure that it is called with previous_investments field merged with the new value
startupFourthStepFormActionHandler({
...startupFourthStepForm,
previous_investments: [
...startupFourthStepForm.previous_investments.slice(0, index),
{
...startupFourthStepForm.previous_investments[index],
name: e.currentTarget.value
},
...startupFourthStepForm.previous_investments.slice(index+1,)
],
});
I suggest to refactor away this piece of state update from the handler to the reducer so that updates to the store are reflected in co-located.
This can be done by passing the index of the item in previous_investments as a part of the payload for ActionTypes.STARTUP_FOURTH_STEP_FORM action.

check if the key in an object is string or not when using prop-types

I have a following kind of props need to be checked
const columns = {
name: {
key : 'company',
label : 'Company',
},
employee: {
key : 'employee',
label: 'Employee',
},
}
Using flow i would check this in the below way
[key: string]: { // the key is dynamic
key: string,
label?: string,
}
How can i do such checks when using prop-types? I am very new to this.
Update
<Table
isSearchable
columns={columns}
data={data}
/>
The column props will always be an object with a key value approach where key should always be string and value should be an object of key(name) and label
may be the terms is creating the confusion so i am updating the columns props as
const columns = {
name: { // how do i check name which is a key and is dynamic
uniqueName : 'company',
displayName : 'Company',
},
employee: {
uniqueName : 'employee',
displayName: 'Employee',
},
}
The PropTypes package assumes that props are named and known ahead of time. While it might be technically possible to check run arbitary checks on keys (I'm eyeing up custom validators and this question), I think you're in for a very hard time. I'd like to frame challenge and suggest an alternative.
Instead of passing an object that looks like this:
const columns = {
name: {
uniqueName : 'company',
displayName : 'Company',
},
employee: {
uniqueName : 'employee',
displayName: 'Employee',
},
};
Pass columns as an array of columns. The benefit here is that it's dead simple to check with PropTypes, and I'd imagine much eaiser to reason about in code.
function App() {
const columns = [
{
column: 'name',
uniqueName: 'company',
displayName : 'Company',
},
{
column: 'employee',
uniqueName: 'employee',
displayName : 'Employee',
},
];
return (
<Report
columns={columns}
/>
);
}
import React from 'react';
import PropTypes from 'prop-types';
function Report(props) {
const { columns } = props;
return (
<div>
{columns.map( (column) => {
return (
<p key={column.column}>
{column.column}
</p>
)
})}
</div>
)
}
Report.propTypes = {
columns : PropTypes.arrayOf(
PropTypes.shape({
column: PropTypes.string,
uniqueName: PropTypes.string,
displayName: PropTypes.string
})
)
};
export default Report;
In my own projects, I'd take it a step further and make Column a class of it's own and check it with PropTypes.instanceOf:
const columns = [
Column.init( { ... } ),
Column.init( { ... } ),
]
...
Report.propTypes = {
columns : PropTypes.arrayOf(PropTypes.instanceOf(Column))
};
You can use PropTypes.shape with PropTypes.oneOfType
PropTypes.oneOfType([
PropTypes.string,
PropTypes.shape({
name: React.PropTypes.string.isRequired,
label: React.PropTypes.string,
}),
]);
Check out this example

Filter state in React without removing data

I'm trying to make a react component that can filter a list based on value chosen from a drop-down box. Since the setState removes all data from the array I can only filter once. How can I filter data and still keep the original state? I want to be able to do more then one search.
Array list:
state = {
tree: [
{
id: '1',
fileType: 'Document',
files: [
{
name: 'test1',
size: '64kb'
},
{
name: 'test2',
size: '94kb'
}
]
}, ..... and so on
I have 2 ways that I'm able to filter the component once with:
filterDoc = (selectedType) => {
//way #1
this.setState({ tree: this.state.tree.filter(item => item.fileType === selectedType) })
//way#2
const myItems = this.state.tree;
const newArray = myItems.filter(item => item.fileType === selectedType)
this.setState({
tree: newArray
})
}
Search component:
class SearchBar extends Component {
change = (e) => {
this.props.filterTree(e.target.value);
}
render() {
return (
<div className="col-sm-12" style={style}>
<input
className="col-sm-8"
type="text"
placeholder="Search..."
style={inputs}
/>
<select
className="col-sm-4"
style={inputs}
onChange={this.change}
>
<option value="All">All</option>
{this.props.docTypes.map((type) =>
<option
value={type.fileType}
key={type.fileType}>{type.fileType}
</option>)}
</select>
</div>
)
}
}
And some images just to get a visual on the problem.
Before filter:
After filter, everything that didn't match was removed from the state:
Do not replace original data
Instead, change what filter is used and do the filtering in the render() function.
In the example below, the original data (called data) is never changed. Only the filter used is changed.
const data = [
{
id: 1,
text: 'one',
},
{
id: 2,
text: 'two',
},
{
id: 3,
text: 'three',
},
]
class Example extends React.Component {
constructor() {
super()
this.state = {
filter: null,
}
}
render() {
const filter = this.state.filter
const dataToShow = filter
? data.filter(d => d.id === filter)
: data
return (
<div>
{dataToShow.map(d => <span key={d.id}> {d.text}, </span>)}
<button
onClick={() =>
this.setState({
filter: 2,
})
}
>
{' '}
Filter{' '}
</button>
</div>
)
}
}
ReactDOM.render(<Example />, document.getElementById('root'))
<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>
<body>
<div id='root' />
</body>
Don't mutate local state to reflect the current state of the filter. That state should reflect the complete available list, which should only change when the list of options changes. Use your filtered array strictly for the view. Something like this should be all you need to change what's presented to the user.
change = (e) => {
return this.state.tree.filter(item => item.fileType === e.target.value)
}

Using JsonSchemaForm on change to update field's content

I am trying to use JsonSchema-Form component but i ran into a problem while trying to create a form that, after choosing one of the options in the first dropdown a secondary dropdown should appear and give him the user a different set o options to choose depending on what he chose in the first dropdown trough an API call.
The thing is, after reading the documentation and some examples found here and here respectively i still don't know exactly how reference whatever i chose in the first option to affect the second dropdown. Here is an example of what i have right now:
Jsons information that are supposed to be shown in the first and second dropdowns trough api calls:
Groups: [
{id: 1,
name: Group1}
{id: 2,
name: Group2}
]
User: [User1.1,User1.2,User2.1,User2.2,User3.1,User3.2, ....]
If the user selects group one then i must use the following api call to get the user types, which gets me the the USER json.
Component That calls JSonChemaForm
render(){
return(
<JsonSchemaForm
schema={someSchema(GroupOptions)}
formData={this.state.formData}
onChange={{}}
uiSchema={someUiSchema()}
onError={() => {}}
showErrorList={false}
noHtml5Validate
liveValidate
>
)
}
SchemaFile content:
export const someSchema = GroupOptions => ({
type: 'object',
required: [
'groups', 'users',
],
properties: {
groups: {
title: 'Group',
enum: GroupOptions.map(i=> i.id),
enumNames: GroupOptions.map(n => n.name),
},
users: {
title: 'Type',
enum: [],
enumNames: [],
},
},
});
export const someUISchema = () => ({
groups: {
'ui:autofocus': true,
'ui:options': {
size: {
lg: 15,
},
},
},
types: {
'ui:options': {
size: {
lg: 15,
},
},
},
});
I am not really sure how to proceed with this and hwo to use the Onchange method to do what i want.
I find a solution for your problem.There is a similar demo that can solve it in react-jsonschema-form-layout.
1. define the LayoutField,this is part of the demo in react-jsonschema-form-layout.To make it easier for you,I post the code here.
Create the layoutField.js.:
import React from 'react'
import ObjectField from 'react-jsonschema-form/lib/components/fields/ObjectField'
import { retrieveSchema } from 'react-jsonschema-form/lib/utils'
import { Col } from 'react-bootstrap'
export default class GridField extends ObjectField {
state = { firstName: 'hasldf' }
render() {
const {
uiSchema,
errorSchema,
idSchema,
required,
disabled,
readonly,
onBlur,
formData
} = this.props
const { definitions, fields, formContext } = this.props.registry
const { SchemaField, TitleField, DescriptionField } = fields
const schema = retrieveSchema(this.props.schema, definitions)
const title = (schema.title === undefined) ? '' : schema.title
const layout = uiSchema['ui:layout']
return (
<fieldset>
{title ? <TitleField
id={`${idSchema.$id}__title`}
title={title}
required={required}
formContext={formContext}/> : null}
{schema.description ?
<DescriptionField
id={`${idSchema.$id}__description`}
description={schema.description}
formContext={formContext}/> : null}
{
layout.map((row, index) => {
return (
<div className="row" key={index}>
{
Object.keys(row).map((name, index) => {
const { doShow, ...rowProps } = row[name]
let style = {}
if (doShow && !doShow({ formData })) {
style = { display: 'none' }
}
if (schema.properties[name]) {
return (
<Col {...rowProps} key={index} style={style}>
<SchemaField
name={name}
required={this.isRequired(name)}
schema={schema.properties[name]}
uiSchema={uiSchema[name]}
errorSchema={errorSchema[name]}
idSchema={idSchema[name]}
formData={formData[name]}
onChange={this.onPropertyChange(name)}
onBlur={onBlur}
registry={this.props.registry}
disabled={disabled}
readonly={readonly}/>
</Col>
)
} else {
const { render, ...rowProps } = row[name]
let UIComponent = () => null
if (render) {
UIComponent = render
}
return (
<Col {...rowProps} key={index} style={style}>
<UIComponent
name={name}
formData={formData}
errorSchema={errorSchema}
uiSchema={uiSchema}
schema={schema}
registry={this.props.registry}
/>
</Col>
)
}
})
}
</div>
)
})
}</fieldset>
)
}
}
in the file, you can define doShow property to define whether to show another component.
Next.Define the isFilled function in JsonChemaForm
const isFilled = (fieldName) => ({ formData }) => (formData[fieldName] && formData[fieldName].length) ? true : false
Third,after you choose the first dropdown ,the second dropdown will show up
import LayoutField from './layoutField.js'
const fields={
layout: LayoutField
}
const uiSchema={
"ui:field": 'layout',
'ui:layout': [
{
groups: {
'ui:autofocus': true,
'ui:options': {
size: {
lg: 15,
},
},
}
},
{
users: {
'ui:options': {
size: {
lg: 15,
},
},
doShow: isFilled('groups')
}
}
]
}
...
render() {
return (
<div>
<Form
schema={schema}
uiSchema={uiSchema}
fields={fields}
/>
</div>
)
}

Categories