When a user clicks a square it becomes currentHtmlObject. I want people to be able to update it's properties in the right sidebar.
I have no idea how to take a single input field and update an object's property that I'm holding in a react-redux state and update the main viewing area DrawingCanvas.
I got kinda close where the info I was entering into the form was activating my reducers and actions. But I couldn't figure out how to distinguish between left and top.
// React
import React from 'react'
export class RightSidebar extends React.Component {
constructor(props) {
super(props)
}
handleChange(evt) {
console.log(evt)
this.props.onUpdateCurrentHtmlObject(evt.target.value)
}
render() {
const { currentHtmlObject } = this.props
return (
<form>
{this.props.currentHtmlObject.id}
<div className="right-sidebar">
<div className="form-group">
<label>Position X</label>
<input
type="number"
name="left"
className="form-control"
value={this.props.currentHtmlObject.styles ? this.props.currentHtmlObject.styles.left : ''}
onChange={this.handleChange.bind(this)} />
</div>
<div className="form-group">
<label>Position Y</label>
<input
type="number"
className="form-control"
value={this.props.currentHtmlObject.styles ? this.props.currentHtmlObject.styles.top : ''}
onChange={this.handleChange.bind(this)} />
</div>
</div>
</form>
)
}
}
RightSidebar.defaultProps = {
currentHtmlObject: {
styles: {
left: null,
top: null
}
}
}
There is no need to distinguish between left and top, let's assume you have an action named update and all it does is to update a selected object's property. Here is what the action method may look like:
updateSelectedObj(id, payload){
return {
type: UPDATE_SELECTED_OBJ,
id: id,
payload: payload
}
}
Here is what your event handler might look like in class RightSidebar:
handleChange(evt) {
// since your top and left input fields have a corresponding name property, evt.target.name will return either `left` or `top`
store.dispatch(updateSelectedObj({styles:{evt.target.name:evt.target.value}})
}
Here is your reducer:
[UPDATE_SELECTED_OBJ]: (state, action) => {
// I assume you have a list of objects in the canvas but only one can
// be selected at a time. and I assume the name of the list is objList
let selectedObj = state.objList.filter(obj => obj.id == action.id)[0]
selectedObj = Object.assign({}, selectedObj, action.payload)
return { objList: state.objList.map(obj => obj.id === action.id? Object.assign({}, obj, selectedObj : obj) }
}
I might suggest simplifying the component itself. Sorry for being brief :). I can update w/ more context when I get some time.
This is a stripped down example, but basically thinking of each "number input" as only needing a value and onChange (emits value, not an event).
You would make use of react-redux's connect so that updateObject is a callback accepting the "patch data" to be merged into the currentObject's state.
/**
* #param currentObject
* #param updateObject An already bound action creator that accepts payload to "update object"
*/
function SideBar({currentObject, updateObject}) {
const {styles} = currentObject;
return (
<div>
<NumberInput
value={styles.left}
onChange={left => updateObject({left})}
/>
<NumberInput
value={styles.top}
onChange={top => updateObject({top})}
/>
</div>
)
}
The connect statement might look something like
const SideBarContainer = connect(
(state, {objectId}) => ({
currentObject: _.find(state.objects, {id}),
}),
(dispatch, {objectId}) => ({
updateObject: data => dispatch(
actions.updateObject(objectId, data)
)
})
)(SideBar);
And the actual usage, maybe something like
<SidebarContainer objectId={currentObjectId} />
Related
Recently I started learning react and I decided to use in my project functional components instead of class-based. I am facing an issue with keeping state on one of my components.
This is generic form component that accepts array of elements in order to draw all of necessary fields in form. On submit it returns "model" with values coming from input fields.
Everything working fine until I added logic for conditionally enabling or disabling "Submit" button when not all required fields are set. This logic is fired either on component mount using useEffect hook or after every input in form input. After re-render of the component (e.g. conditions for enabling button are not met, so button becomes disabled), component function is fired again and my logic for creating new mutable object from passed props started again, so I am finished with empty object.
I did sort of workaround to make a reference of that mutated object outside of scope of component function, but i dont feel comfortable with it. I also dont want to use Redux for that simple sort of state.
Here is the code (I am using Type Script):
//component interfaces:
export enum FieldType {
Normal = "normal",
Password = "password",
Email = "email"
}
export interface FormField {
label: string;
displayLabel: string;
type: FieldType;
required: boolean;
}
export interface FormModel {
model: {
field: FormField;
value: string | null;
}[]
}
export interface IForm {
title: string;
labels: FormField[];
actionTitle: string;
onSubmit: (model: FormModel) => void;
}
let _formState: any = null;
export function Form(props: IForm) {
let mutableFormModel = props.labels.map((field) => { return { field: field, value: null as any } });
//_formState keeps reference outside of react function scope. After coponent redraw state inside this function is lost, but is still maintained outside
if (_formState) {
mutableFormModel = _formState;
} else {
_formState = mutableFormModel;
}
const [formModel, setFormModel] = useState(mutableFormModel);
const [buttonEnabled, setButtonEnabled] = useState(false);
function requiredFieldsCheck(formModel: any): boolean {
let allRequiredSet = true;
formModel.model.forEach((field: { field: { required: any; }; value: string | null; }) => {
if (field.field.required && (field.value === null || field.value === '')) {
allRequiredSet = false;
}
})
return allRequiredSet;
}
function handleChange(field: FormField, value: string) {
let elem = mutableFormModel.find(el => el.field.label === field.label);
if (elem) {
value !== '' ? elem.value = value as any : elem.value = null;
}
let submitEnabled = requiredFieldsCheck({ model: mutableFormModel });
setFormModel(mutableFormModel);
setButtonEnabled(submitEnabled);
}
useEffect(() => {
setButtonEnabled(requiredFieldsCheck({ model: mutableFormModel }));
}, [mutableFormModel]);
function onSubmit(event: { preventDefault: () => void; }) {
event.preventDefault();
props.onSubmit({ model: formModel })
}
return (
<FormStyle>
<div className="form-container">
<h2 className="form-header">{props.title}</h2>
<form className="form-content">
<div className="form-group">
{props.labels.map((field) => {
return (
<div className="form-field" key={field.label}>
<label>{field.displayLabel}</label>
{ field.type === FieldType.Password ?
<input type="password" onChange={(e) => handleChange(field, e.target.value)}></input> :
<input type="text" onChange={(e) => handleChange(field, e.target.value)}></input>
}
</div>
)
})}
</div>
</form>
{buttonEnabled ?
<button className={`form-action btn btn--active`} onClick={onSubmit}> {props.actionTitle} </button> :
<button disabled className={`form-action btn btn--disabled`} onClick={onSubmit}> {props.actionTitle} </button>}
</div>
</FormStyle >
);
}
So there is quite a lot going on with your state here.
Instead of using a state variable to check if your button should be disabled or not, you could just add something render-time, instead of calculating a local state everytime you type something in your form.
So you could try something like:
<button disabled={!requiredFieldsCheck({ model: formModel })}>Click me</button>
or if you want to make it a bit cleaner:
const buttonDisabled = !requiredFieldsCheck({model: formModel});
...
return <button disabled={buttonDisabled}>Click me</button>
If you want some kind of "caching" without bathering with useEffect and state, you can also try useMemo, which will only change your calculated value whenever your listeners (in your case the formModel) have changes.
const buttonDisabled = useMemo(() => {
return !requiredFieldsCheck({model: formModel});
}, [formModel]);
In order to keep value in that particular case, I've just used useRef hook. It can be used for any data, not only DOM related. But thanks for all inputs, I've learned a lot.
I am looking into fixing a bug in the code. There is a form with many form fields. Project Name is one of them. There is a button next to it.So when a user clicks on the button (plus icon), a popup window shows up, user enters Project Name and Description and hits submit button to save the project.
The form has Submit, Reset and Cancel button (not shown in the code for breviety purpose).
The project name field of the form has auto suggest feature. The code snippet below shows the part of the form for Project Name field.So when a user starts typing, it shows the list of projects
and user can select from the list.
<div id="formDiv">
<Growl ref={growl}/>
<Form className="form-column-3">
<div className="form-field project-name-field">
<label className="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated custom-label">Project Name</label>
<AutoProjects
fieldName='projectId'
value={values.projectId}
onChange={setFieldValue}
error={errors.projects}
touched={touched.projects}
/>{touched.projects && errors.v && <Message severity="error" text={errors.projects}/>}
<Button className="add-project-btn" title="Add Project" variant="contained" color="primary"
type="button" onClick={props.addProject}><i className="pi pi-plus" /></Button>
</div>
The problem I am facing is when some one creates a new project. Basically, the autosuggest list is not showing the newly added project immediately after adding/creating a new project. In order to see the newly added project
in the auto suggest list, after creating a new project,user would have to hit cancel button of the form and then open the same form again. In this way, they can see the list when they type ahead to search for the project they recently
created.
How should I make sure that the list gets immediately updated as soon as they have added the project?
Below is how my AutoProjects component looks like that has been used above:
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import axios from "axios";
import { css } from "#emotion/core";
import ClockLoader from 'react-spinners/ClockLoader'
function escapeRegexCharacters(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Use your imagination to render suggestions.
const renderSuggestion = suggestion => (
<div>
{suggestion.name}, {suggestion.firstName}
</div>
);
const override = css`
display: block;
margin: 0 auto;
border-color: red;
`;
export class AutoProjects extends Component {
constructor(props) {
super(props);
this.state = {
value: '',
projects: [],
suggestions: [],
loading: false
}
this.getSuggestionValue = this.getSuggestionValue.bind(this)
this.setAutoSuggestValue = this.setAutoSuggestValue.bind(this)
}
// Teach Autosuggest how to calculate suggestions for any given input value.
getSuggestions = value => {
const escapedValue = escapeRegexCharacters(value.trim());
if (escapedValue === '') {
return [];
}
const regex = new RegExp(escapedValue, 'i');
const projectData = this.state.projects;
if (projectData) {
return projectData.filter(per => regex.test(per.name));
}
else {
return [];
}
};
// When suggestion is clicked, Autosuggest needs to populate the input
// based on the clicked suggestion. Teach Autosuggest how to calculate the
// input value for every given suggestion.
getSuggestionValue = suggestion => {
this.props.onChange(this.props.fieldName, suggestion.id)//Update the parent with the new institutionId
return suggestion.name;
}
fetchRecords() {
const loggedInUser = JSON.parse(sessionStorage.getItem("loggedInUser"));
return axios
.get("api/projects/search/getProjectSetByUserId?value="+loggedInUser.userId)//Get all personnel
.then(response => {
return response.data._embedded.projects
}).catch(err => console.log(err));
}
setAutoSuggestValue(response) {
let projects = response.filter(per => this.props.value === per.id)[0]
let projectName = '';
if (projects) {
projectName = projects.name
}
this.setState({ value: projectName})
}
componentDidMount() {
this.setState({ loading: true}, () => {
this.fetchRecords().then((response) => {
this.setState({ projects: response, loading: false }, () => this.setAutoSuggestValue(response))
}).catch(error => error)
})
}
onChange = (event, { newValue }) => {
this.setState({
value: newValue
});
};
// Autosuggest will call this function every time you need to update suggestions.
// You already implemented this logic above, so just use it.
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
});
};
// Autosuggest will call this function every time you need to clear suggestions.
onSuggestionsClearRequested = () => {
this.setState({
suggestions: []
});
};
render() {
const { value, suggestions } = this.state;
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: value,
value,
onChange: this.onChange
};
// Finally, render it!
return (
<div>
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
/>
<div className="sweet-loading">
<ClockLoader
css={override}
size={50}
color={"#123abc"}
loading={this.state.loading}
/>
</div>
</div>
);
}
}
The problem is you only call the fetchRecord when component AutoProjects did mount. That's why whenever you added a new project, the list didn't update. It's only updated when you close the form and open it again ( AutoProjects component mount again)
For this case I think you should lift the logic of fetchProjects to parent component and past the value to AutoProjects. Whenever you add new project you need to call the api again to get a new list.
I have a react component that looks like this. I call a method cleanUpInvoices
to format a date in my array object(invoices). This works very well without any
problem. I tried to setState to dateCleanUpResult.
All I get is "dateCleanUpResult is not defined". I have tried so many things and nothing works.
I am not able to set state.
What is wrong with this code?
Here is the entire code
class Tester extends PureComponent {
constructor(){
super();
this.state = {
invoices:[],
startDate:'',
endDate:'',
queryResult:[],
dateCleanUpResult:[]
};
this.searchForInvoicesByDates = this.searchForInvoicesByDates.bind(this);
this.handleChange = this.handleChange.bind(this);
this.cleanUpInvoices = this.cleanUpInvoices.bind(this);
}
handleChange({ target }) {
this.setState({
[target.name]: target.value
});
}
componentDidMount() {
const getCustomerId = this.props.customer.customerId;
AXIOS_AUTHED.get(`${API}/customers/${getCustomerId}/invoices?sort=settledDate,desc`)
.then(res => {
const invoices= res.data.content;
this.setState({ invoices });
})
}
cleanUpInvoices(){
const invoice = this.state.invoices;
invoice.forEach(function(invoicer) {
const newDate = invoicer.settledDate.substring(0, invoicer.settledDate.indexOf('T'));
invoicer.settledDate = moment(newDate, 'YYYY-MM-DD').format('MM-DD-YYYY');
});
return this.setState({
dateCleanUpResult: invoice
}, () => this.state.dateCleanUpResult);
}
searchForInvoicesByDates(startDate, endDate){
var myResult = this.cleanUpInvoices();
console.log(myResult);
//now perform your date search based on the result from above
let cleanedStartDate = moment(startDate).format('MM-DD-YYYY');
let cleanedEndDate = moment(endDate).format('MM-DD-YYYY');
let filteredResult = [];
for(let i = 0; i < this.state.dateCleanUpResult.length; i++){
if(this.state.dateCleanUpResult[i].settledDate >= cleanedStartDate && this.state.dateCleanUpResult[i].settledDate <= cleanedEndDate) {
filteredResult.push(this.state.dateCleanUpResult[i]);
}
}
console.log(filteredResult);
const listItems = filteredResult.map((number) =>
<li key={number.orderNumber}>{number.orderNumber} - {moment(number.settledDate).format('MMM-DD-YYYY')} </li>
);
this.setState({queryResult:listItems});
return (
<ul>{listItems}</ul>
);
}
render() {
return (
<PageBase
navigation={['Customer Solution', 'Tester App']}
>
<div className="row">
<div className="col-xs-12 col-sm-12 col-md-12 col-lg-12">
<Paper>
<Typography className="customer-solution-subheader" component="h3" variant="subheading">
Tester App
</Typography>
<form>
<div className="customer-form-details">
<span>DATE RANGE COVERED*</span><br/>
<span className="ctrCalendar">
<label htmlFor="start">Start date:</label>
<input type="date" id="start" name="startDate" value={this.state.startDate} onChange={this.handleChange} required></input>
</span>
<span className="ctrCalendar">
<label htmlFor="start">End date:</label>
<input type="date" id="end" name="endDate" value={this.state.endDate} onChange={this.handleChange} required></input>
</span>
<span>
<Button variant="contained" className="next-button" id="btnSearchDates" onClick={() =>this.searchForInvoicesByDates(this.state.startDate, this.state.endDate)}>Search</Button><br/><br/>
</span>
<p>Search Result (Invoices/Dates)</p>
<div role="content" className="invContentParent">
<div name="teach" id="invContentChild">
</div>
</div>
</div>
</form>
</Paper>
</div>
</div>
</PageBase>
);
}
}
export default Tester;
It's almost right, but what's happining is that when you return the for the array it could be just the empty array you are initializing at the state of the component.
We fix that be passing a callback function to setState method, which is a function that returns the state you want, the updated one right?
That callback function will be invoked after insuring that the new state has been set, and, we also return the setState function, because it's the one that is returning the new state.
return this.setState({
dateCleanUpResult: invoice
}, () => this.state.dateCleanUpResult);
That ARTICLE is a good explanation for that matter.
I created a jsfiddle showing that you can update the state in your case.
http://jsfiddle.net/efp82rjg/3/
I think you are getting an issue because you are assuming cleanUpInvoices() will return the updated value of state. But it won't because setState is asynchronous and even though the value will be updated but it won't show the updated value to you. If you want to access the updated value after setState() then use the call back that is available to you after setState function. Please read the documentation here: https://reactjs.org/docs/react-component.html#setstate
class Hello extends React.Component {
constructor() {
super();
this.state = {
invoices: [
{ settledDate: 'no' },
{ settledDate: 'no' },
],
dateCleanUpResult: [],
};
this.cleanUpInvoices = this.cleanUpInvoices.bind(this);
}
cleanUpInvoices() {
const invoice = this.state.invoices;
invoice.forEach((invoicer) => {
invoicer.settledDate = 'testing';
});
this.setState({
dateCleanUpResult: invoice,
});
return this.state.dateCleanUpResult;
}
render() {
return (
<div>
<button onClick={this.cleanUpInvoices}>test</button>
{this.state.dateCleanUpResult.map(item => (
<div>{item.settledDate}</div>
))}
</div>
);
}
I'm assuming you receive the value of "undefined" where you are logging the returned value cleanUpInvoices in your searchForInvoicesByDateMethod. Although this is the result of the async nature of setState, implementing a setState callback will not remedy this issue (without more, it will merely let you access the updated state in the scope of that handler). If you want to stick to your current implementation, I would return a promise and async/await the returned promise value. This will delay execution of code in your searchForInvoicesByDates method when cleanUpInvoices is called.
cleanUpInvoices(){
// Avoid directly mutating state and generating circular reference by creating new array with map
const invoice = this.state.invoices.map(invoice => Object.assign({}, invoice));
//.....
//.....
return new Promise((res) => {
return this.setState({
dateCleanUpResult: invoice
}, () => res(this.state.dateCleanUpResult)));
}
}
////////
async searchForInvoicesByDates(startDate, endDate){
var myResult = await this.cleanUpInvoices();
//....
}
Trying not to deviate from the question posed, here is another quick thought that might simplify the implementation. Looks like you're initially making an API call on componentDidMount to retrieve the full universe of invoices, then generating a subset by date based on user input. Is it necessary to maintain the generated subset in state? Each time invoices are searched for by date, it seems you want to use the full universe returned from the API as your filter starting point. If that's the case, you can just immediately return the result from cleanUpInvoices -- no need for a state update. This would be different, of course, if you needed the invoice subset for further reference or manipulation.
Lets imagine we want an input for a "product" (stored in redux) price value.
I'm struggle to come up with the best way to handle input constraints. For simplicity, lets just focus on the constraint that product.price cannot be empty.
It seems like the 2 options are:
1: Controlled
Implementation: The input value is bound to product.price. On change dispatches the changePrice() action.
The main issue here is that if we want to prevent an empty price from entering the product store, we essentially block the user from clearing the input field. This isn't ideal as it makes it very hard to change the first digit of the number (you have to select it and replace it)!
2: Using defaultValue
Implementation: We set the price initially using input defaultValue, that allows us to control when we want to actually dispatch changePrice() actions and we can do validation handling in the onChange handler.
This works well, unless the product.price is ever updated from somewhere other than the input change event (for example, an applyDiscount action). Since defaultValue doesn't cause rerenders, the product.price and the input are now out of sync!
So what am I missing?
There must be a simple & elegant solution to this problem but I just can't seem to find it!
What I have done in the past is to use redux-thunk and joi to solve input constraints/validation using controlled inputs.
In general I like to have one update action that will handle all the field updating. So for example if you have two inputs for a form, it would looks something like this:
render() {
const { product, updateProduct } = this.props;
return (
<div>
<input
value={product.name}
onChange={() => updateProduct({...product, name: e.target.value})}
/>
<input
value={product.price}
onChange={() => updateProduct({...product, price: e.target.value})}
/>
</div>
)
}
Having one function/action here simplifies my forms a great deal. The updateProject action would then be a thunk action that handles side effects. Here is our Joi Schema(based off your one requirement) and updateProduct Action mentioned above. As a side note, I also tend to just let the user make the mistake. So if they don't enter anything for price I would just make the submit button inactive or something, but still store away null/empty string in the redux store.
const projectSchema = Joi.object().keys({
name: Joi.number().string(),
price: Joi.integer().required(), // price is a required integer. so null, "", and undefined would throw an error.
});
const updateProduct = (product) => {
return (dispatch, getState) {
Joi.validate(product, productSchema, {}, (err, product) => {
if (err) {
// flip/dispatch some view state related flag and pass error message to view and disable form submission;
}
});
dispatch(update(product)); // go ahead and let the user make the mistake, but disable submission
}
}
I stopped using uncontrolled inputs, simply because I like to capture the entire state of an application. I have very little local component state in my projects. Keep in mind this is sudo code and probably won't work if directly copy pasted. Hope it helps.
So I think I've figure out a decent solution. Basically I needed to:
Create separate component that can control the input with local state.
Pass an onChange handler into the props that I can use to dispatch my changePrice action conditionally
Use componentWillReceiveProps to keep the local value state in sync with the redux store
Code (simplified and in typescript):
interface INumberInputProps {
value: number;
onChange: (val: number) => void;
}
interface INumberInputState {
value: number;
}
export class NumberInput extends React.Component<INumberInputProps, INumberInputState> {
constructor(props) {
super(props);
this.state = {value: props.value};
}
public handleChange = (value: number) => {
this.setState({value});
this.props.onChange(value);
}
//keeps local state in sync with redux store
public componentWillReceiveProps(props: INumberInputProps){
if (props.value !== this.state.value) {
this.setState({value: props.value});
}
}
public render() {
return <input value={this.state.value} onChange={this.handleChange} />
}
}
In my Product Component:
...
//conditionally dispatch action if meets valadations
public handlePriceChange = (price: number) => {
if (price < this.props.product.standardPrice &&
price > this.props.product.preferredPrice &&
!isNaN(price) &&
lineItem.price !== price){
this.props.dispatch(updatePrice(this.props.product, price));
}
}
public render() {
return <NumberInput value={this.props.product.price} onChange={this.handlePriceChange} />
}
...
What i would do in this case is to validate the input onBlur instead of onChange.
For example consider these validations in the flowing snippet:
The input can't be empty.
The input should not contain "foo".
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
myVal: '',
error: ''
}
}
setError = error => {
this.setState({ error });
}
onChange = ({ target: { value } }) => {
this.setState({ myVal: value })
}
validateInput = ({ target: { value } }) => {
let nextError = '';
if (!value.trim() || value.length < 1) {
nextError = ("Input cannot be empty!")
} else if (~value.indexOf("foo")) {
nextError = ('foo is not alowed!');
}
this.setError(nextError);
}
render() {
const { myVal, error } = this.state;
return (
<div>
<input value={myVal} onChange={this.onChange} onBlur={this.validateInput} />
{error && <div>{error}</div>}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit
As a followup to your comments.
To make this solution more generic, i would pass the component a predicate function as a prop, only when the function will return a valid result i would call the onChange that passed from the parent or whatever method you pass that updating the store.
This way you can reuse this pattern in other components and places on your app (or even other projects).
Does redux-form field value can hold object instead of just a string?
Consider following example
class SelectQuestions extends Component{
render(){
const {fields:{question1,question2},handleSubmit}=this.props;
return <div>
<form onSubmit={handleSumbit(this.handleFunction.bind(this))}>
<SelectQuestion {...question1}/>
<SelectQuestion {...question1}/>
<button type="submit" className="btn btn-default">Submit</button>
</form>
</div>
}
}
class SelectQuestion extends Component{
render (){
<select></select>
}
}
'SelectQuestion' takes array of Questions, each Question has question id and question name.
Once the user selects the question I wanted to to return QuesionId and QuesitonName. How it can be achieved using redux-form in react
TL:DR
Yes it can hold an object. Check out this example:
https://codesandbox.io/s/vq6k6195rl
Explanation:
For a select, you can define your selects like this:
const myFancyQuestions = [
{ id: 1, label: 'Why?', category: 'Asking why' },
{ id: 2, label: 'Who?', category: 'Asking who' },
{ id: 3, label: 'When?', category: 'Asking when' }
];
Then wrap your component with Field component.
<Field component={ObjectSelect} name="myFancySelect" options={myFancyQuestions}/>
And then just show it in your render:
class ObjectSelect extends React.Component {
render() {
const { input, multiple, options, value, ...rest } = this.props;
const parse = event => {
// This is what redux-form will hold:
return JSON.parse(event.target.value);
}
return (
<select
onBlur={event => input.onBlur(parse(event))}
onChange={event => input.onChange(parse(event))}
{...rest}>
{options.map(option =>
<option key={option.id}
value={JSON.stringify(option)}
selected={input.value.id == option.id}>
{option.label}
</option>)}
</select>
)
}
}
Yes, the value can be a complex object. There's even an example demonstrating it.
Complex Values Example
I think the better way to achieve that is to create two virtual fields selectedQuestionId and selectedQuestionName. Those fields are not to display. Then, each SelectQuestion can trigger a callback like:
setSelectedQuestion = (event) => {
var questionName = event.target.name; //or something like that
var questionId = event.target.id; //or something like that
this.props.fields.selectedQuestionName.onChange(questionName );
this.props.fields.selectedQuestionId.onChange(questionId );
}
onChange can be called on redux-form fields to programmatically set the value.
This is a bit late, but still..
Yes, it can! You can simply use a FormSection here.
It will split your form to an objects under the hood, and will keep all sections separtely in your single form object.
Just need to pass a name prop to the FormSection, that will be a name of your sub-object.
import { FormSection } from 'redux-form';
render(){
return(
<form onSubmit={...}>
<FormSection name="firstGroup">
<SelectQuestion {...question1}/>
</FormSection>
<FormSection name="secondGroup">
<SelectQuestion {...question1}/>
</FormSection>
<button type="submit" className="btn btn-default">Submit</button>
</form>
)
}
Then check your form structure through the Redux devTools.
Enjoy ;)
It is simple, you can just add ".value" (or anything else..) to name prop and it will create object and update value inside it on changes...
<Field name="companyName.value" placeholder="Name of the Company" component={ReduxFormTextInput} />
And you will get object :
companyName: {
value:"<inputted string>"
}