how to create re-usable field component in redux-form? - reactjs

I have 2-3 fields that is reused is other forms of my application. So i wanted to create those fields as component so that I can resuse is my other forms. but redux-form is complaining that
Error: Field must be inside a component decorated with reduxForm()
Any ideas how can I achieve it? BTW, I am using material-ui
EDIT: providing a better e.g. consider the material-ui toolbar
http://www.material-ui.com/#/components/toolbar
My toolbar consists of a selectField, textField, Toggle button, which may couple of forms. In my app, I want to keep this toolbar in all the forms where I create objects in my application, so I want to include this toolbar in all the forms. After the below answer I tried something dirty like below.
class BaseBar extends React.Component { // eslint-disable-line react/prefer-stateless-function
constructor(props) {
super(props);
this.state = {
value: 3,
isGlueOpen: false,
};
}
handleChange = (event, index, value) => {
this.setState({value: value});
}
render() {
return (
<div>
<Toolbar>
<ToolbarGroup firstChild={true}>
<DropDownMenu value={this.state.value} onChange={this.handleChange}>
<MenuItem value={1} primaryText="All Broadcasts" />
<MenuItem value={2} primaryText="All Voice" />
<MenuItem value={3} primaryText="All Text" />
<MenuItem value={4} primaryText="Complete Voice" />
<MenuItem value={5} primaryText="Complete Text" />
<MenuItem value={6} primaryText="Active Voice" />
<MenuItem value={7} primaryText="Active Text" />
</DropDownMenu>
</ToolbarGroup>
<ToolbarSeparator />
<ToolbarGroup>
<ToggleButton onChange={this.props.glueToggle}/>
</ToolbarGroup>
</Toolbar>
</div>
);
}
}
export default BaseBar;
and including the form like below
<form onSubmit={handleSubmit}>
<div>
<Field
name="basebar"
component={BaseBar}
label="project"
/>
</div>
<div>
<Field
name="subject"
component={renderTextField}
label="subject"
/>
</div>
</form>
But on submit, I am getting the values for subject field but not the basebar values, any suggestions or approach is greatly appreciated.

The error is that you're trying to use the Field outside the context of a connected Redux Form.
Perhaps you could create your shareable component without the <Field> component itself, and then use that component in as many forms as you need, wrapping it with Field. See example usage in the documentation:
http://redux-form.com/6.8.0/docs/api/Field.md/#usage
import MyCustomInput from './MyCustomInput'
...
<Field name="myField" component={MyCustomInput}/>
Where MyCustomInput is your custom, common component.
Or if you have custom logic to map Field props to your components, then use a function which you can then export and reuse in as many forms as you need.
// this is your custom component now
export const renderMyCustomField = (field) => (
<div className="input-row">
<input {...field.input} type="text"/>
{field.meta.touched && field.meta.error &&
<span className="error">{field.meta.error}</span>}
</div>
)
...
//which you can then import in your forms
import { renderMyCustomField } from '...'
// inside your render() method
<Field name="myField" component={renderMyCustomField}/>

Welcome to reusable redux form field component :)
Here my elegant soloution:
import React from 'react';
import { string, object } from 'prop-types';
import { Field } from 'redux-form/immutable';
const Input = ({
input,
meta: { touched, error },
...rest
}) => (
<div>
<input
{...input}
{...rest}
/>
{touched && error && <span>{error}</span>}
</div>
);
Input.propTypes = {
input: object.isRequired,
meta: object.isRequired,
type: string.isRequired
};
Input.defaultProps = {
input: null,
meta: null,
type: 'text'
};
export default props => <Field {...props} component={Input} />;
How to use?
import Input from './Input';
<form>
...
<Input
autoComplete="off"
name="email"
placeholder="Email"
type="email"
/>
...
</form>

Related

#mui/material <Checkbox> with Formik

I wish to build a React (multi step) form using Formik and Mui. I cannot understand how to bind/control the mui checkbox element
import { Button, Checkbox, Typography } from "#mui/material";
import { Field, Form, Formik } from "formik";
import "./styles.css";
export default function App() {
var fruitValues = ["apple", "banana"];
function handleSubmit(values, actions) {
fruitValues = values.fruit;
console.debug(values.fruit);
}
return (
<div className="App">
<Formik initialValues={{ fruit: ["apple"] }} onSubmit={handleSubmit}>
<Form id="test2">
<Checkbox name="fruit" value="apple" label="Apple" />
<Checkbox name="fruit" value="banana" label="Banana" />
<Checkbox name="fruit" value="orange" label="Orange" />
<Button type="submit">Submit</Button>
</Form>
</Formik>
</div>
);
}
See https://codesandbox.io/s/thirsty-wing-91glso?file=/src/App.js:0-1380
I am working on this as well. Seems like something that should be covered in the docs but just isn't.
There is this example. It covers doing something like this ...
<Form>
<Field
type="email"
name="email"
component={TextField}
color={"error"}
/>
But there isn't any explanation of where this comes from or what is happening here.
My reaction is a little late but I faced the same issue and made it work as follow.
import React from 'react'
import { Field, Form, Formik } from 'formik'
import Checkbox from '#mui/material/Checkbox'
import FormControlLabel from '#mui/material/FormControlLabel'
export default function Example() {
return (
<Formik>
<Form>
<Field name="terms_of_condition">
{({ field }) => (
<FormControlLabel
onChange={field.onChange}
control={<Checkbox name="terms_of_condition" />}
label="Terms of conditions"
sx={sx}
value="on"
/>
)}
</Field>
</Form>
</Formik>
)
}

Unable to retrieve the input field of material UI using refs in react js

I am developing a Web application using React JS + Material UI core. Now, I am building a form with the material ui control. Now, I am trying to retrieve the input field value (TextField) using refs of React. It is always saying undefined.
This is my component
class CreateEventComponent extends React.Component{
constructor(props)
{
super(props)
}
submitCreateEventForm(e)
{
e.preventDefault();
alert(this.refs.name.input.value)
}
render()
{
return (
<MuiThemeProvider>
<div className={scss['page-container']}>
<Grid
spacing={16}
container>
<Grid item md={12}>
<Card>
<CardContent>
<form onSubmit={this.submitCreateEventForm.bind(this)}>
<div>
<TextField
ref="name"
className={scss['form-control']}
name="name"
label="Name" />
</div>
<div>
<Grid>
<Button type="submit" color="primary" variant="raised">Save</Button>
</Grid>
</div>
</form>
</CardContent>
</Card>
</Grid>
</Grid>
</div>
</MuiThemeProvider>
)
}
}
function mapStateToProps(state)
{
return {
};
}
function matchDispatchToProps(dispatch)
{
return bindActionCreators({
}, dispatch);
}
const enhance = compose(withWidth(), withStyles(themeStyles, { withTheme: true }), connect(mapStateToProps, matchDispatchToProps))
export default enhance(CreateEventComponent);
As you can see, when form submits, I am trying to alert the name input field using refs. But it is always showing "undefined". I tried using this to fetch the value of TextField.
alert(this.refs.name.value)
It throws error saying name is undefined. So, how can I fetch the value of TextField using Ref?
I used this way as well.
I create ref in the constructor
constructor(props)
{
super(props)
this.nameRef = React.createRef();
}
Then set the ref for the TextField
<TextField
ref={this.nameRef}
className={scss['form-control']}
name="name"
label="Name" />
Then retrieve the values in this ways.
this.nameRef.value
this.nameRef.input.value
It is giving me the same error as well.
Original Answer
You need to create a ref in your constructor.
From the docs:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef(); // create a ref
}
render() {
return <div ref={this.myRef} />;
}
}
Updated Answer
According to Material UI's documentation, you need to pass in a callback to the inputRef prop on your <TextField />.
So, in addition to the original answer, try this as well:
<TextField
inputRef={e => this.nameRef = e}
className={scss['form-control']}
name="name"
label="Name" />
if you are using a stateless functional component with material ui then you can use react hooks.
import React, { useState, useRef } from "react";
let MyComponent = (props) => {
let textInput = useRef(null);
return (
<div>
<Button
onClick={() => {
setTimeout(() => {
textInput.current.focus();
textInput.current.click();
textInput.current.value="myname";
console.log(textInput.current.value);
}, 100);
}}
>
Focus TextField
</Button>
<TextField
fullWidth
required
inputRef={textInput}
name="firstName"
type="text"
placeholder="Enter Your First Name"
label="First Name"
/>
</div>
);
};
For me, this solves the problem:
<TextField
ref={this.nameRef}
onChange={e => {
this.nameRef.current.value = e.target.value;
}}
className={scss['form-control']}
name="name"
label="Name" />

Submit Redux Form from the Parent Component - Remote Submit

Probably I've checked all the docs and questions out there about this problem but I still couldn't fix it since long time. So, I decided to ask here.
As the title says I am trying to submit a redux form from its parent component. I tried adding hidden submit input, into the form and that works although this isn't the redux form way. What should I do?
Thanks for your help.
WorkExperienceForm.js
import React, { Component } from 'react';
import { Field, reduxForm } from 'redux-form';
import { renderHorizontalTextField } from '../Fields/TextFields';
import { renderTextAreaFieldWithLabelAndPopover } from '../Fields/TextAreaFields';
const WorkExperienceForm = ({ handleSubmit, onSubmit, shouldHide, form }) => {
if(shouldHide) return false;
return(
<form onSubmit={ handleSubmit(onSubmit) } className="form-horizontal">
<Field name="companyName"
type="text"
label="Company Name"
placeholder="Apple inc."
id="input-company-name"
component={renderHorizontalTextField} />
<Field name="title"
type="text"
label="Title"
placeholder="Marketing Specialist"
id="input-title"
component={renderHorizontalTextField} />
<Field name="startYear"
type="text"
label="Start Year"
placeholder=""
id="input-start-year"
component={renderHorizontalTextField} />
<Field name="endYear"
type="text"
label="End Year"
placeholder="Blank if current"
id="input-end-year"
component={renderHorizontalTextField} />
<Field name="summary"
rows="4"
label="Summary"
placeholder="Summary..."
id="input-summary"
component={renderTextAreaFieldWithLabelAndPopover} />
</form>
)
}
export default reduxForm({
enableReinitialize: true
})(WorkExperienceForm);
This is the parent component
onSubmit(values){
this.props.createWorkExperience(values, () => {
this.props.notify();
})
}
render(){
if(!this.props.candidate || !this.props.candidate['workExperience'].length > 0) return this.renderEmptyState();
const activeClass = this.state.displayForm ? 'btn btn-success btn-block mt8' : 'btn btn-primary btn-block mt8'
return(
<div>
{ this.renderWorkExperience() }
<WorkExperienceForm {...this.props}
form='postWorkExperienceForm'
onSubmit={this.onSubmit}
shouldHide={!this.state.displayForm} />
<button type={ this.state.displayForm ? 'submit' : 'button' }
htmlFor='postWorkExperienceForm'
className={activeClass} >{ this.state.displayForm ? 'Save' : 'Add Work Experience' }
</button>
</div>
)
}
Yeah, the documentation is sparse regarding this matter.
I think you may have Context issues. I think you should move the button to the Form Component and pass the mySubmit (renamed it from onSubmit for clarity) function to the Form Component with an arrow function via onSubmit property.
mySubmit(formValues){
this.props.createWorkExperience(formValues, () => {
this.props.notify();
})
}
<WorkExperienceForm {...this.props}
form='postWorkExperienceForm'
onSubmit={(formValues) => this.mySubmit(formValues)}
shouldHide={!this.state.displayForm} />
Then in the Form Component wrap the onSubmit in the handleSubmit function.
<form onSubmit={handleSubmit(onSubmit)}>
Here is a example where we recently had to do this with Redux-Form.
Maybe, it will help you some.
Note, we are using FLOW.
Parent Component: beer.add.component.js
Form Component: beer.add.form.component.js
Live production example, so you know that it works.
Note, The SignInForm uses the same pattern.

ReduxForm: using formValueSelector within a FieldArray for conditional fields doesn't render immediately

This sort of works, except additional block doesn't show up when I make a service_type radio selection, it only pops up/re-renders if I complete an additional action, like adding or removing a service block or changing another field.
import './Register.scss';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { Field, reduxForm, FieldArray, formValueSelector } from 'redux-form';
import MenuItem from 'material-ui/MenuItem';
import { RadioButton } from 'material-ui/RadioButton'
import {
TextField,
SelectField,
TimePicker,
RadioButtonGroup
} from 'redux-form-material-ui';
import validate from './validate';
class OnboardPageThree extends Component {
static propTypes = {
handleSubmit: PropTypes.func.isRequired,
previousPage: PropTypes.func.isRequired
}
constructor(props, context) {
super(props, context);
}
renderGroups = ({ fields, meta: { touched, error } }) => {
return (
<div>
{fields.map((service, index) =>
<div className="service-type-group" key={index}>
<h4>{index} Service</h4>
<button
type="button"
className="remove-button"
onClick={() => fields.remove(index)}>Remove
</button>
<div className="field-group half">
<Field
name={`${service}.service_type`}
component={RadioButtonGroup}
floatingLabelText="Service type"
className="textfield">
<RadioButton value="first_come_first_serve" label="First-come first-served"/>
<RadioButton value="appointments" label="Appointment"/>
</Field>
</div>
{/* Sort of works except doesn't populate until forced re-render by adding another to array or updating a previous field */}
{typeof this.props.services[index].service_type != undefined && this.props.services[index].service_type == "first_come_first_serve" ? <div className="fieldset">
<p className="label">Timeslot capacity and length when creating a first-come-first-served (line-based) service blocks</p>
<div className="field-group sentence-inline">
<Field
name={`${service}.max_people`}
type="number"
component={TextField}
label="Number of people per timeslot"
className="textfield"
/>
<p>people are allowed every</p>
<Field
name={`${service}.time_interval`}
component={SelectField}
className="textfield">
<MenuItem value="5" primaryText="5 minutes" />
<MenuItem value="10" primaryText="10 minutes" />
<MenuItem value="15" primaryText="15 minutes" />
<MenuItem value="30" primaryText="30 minutes" />
<MenuItem value="60" primaryText="60 minutes" />
<MenuItem value="all_day" primaryText="All day" />
</Field>
<p>.</p>
</div>
</div> : null}
</div>
)}
<button
type="button"
className="action-button"
onClick={() => fields.push({})}>Add Service
</button>
{touched && error && <span className="error-message">{error}</span>}
</div>
);
}
render() {
const { handleSubmit, previousPage } = this.props;
return (
<form onSubmit={handleSubmit}>
<h2>When do you provide service?</h2>
<FieldArray name="service_options" component={this.renderGroups} />
<div className="justify-flex-wrapper service-actions">
<button type="button" className="back-button" onClick={previousPage}>Back</button>
<button type="submit" className="action-button">Next</button>
</div>
</form>
);
}
}
OnboardPageThree = reduxForm({
form: 'onboarding',
destroyOnUnmount: false,
validate
})(OnboardPageThree)
const selector = formValueSelector('onboarding');
OnboardPageThree = connect(
(state) => {
const services = selector(state, 'service_options');
return {
services
};
}
)(OnboardPageThree);
export default OnboardPageThree;
Yes, that is true. The FieldArray does not re-render on the change of any of its items (that would be really bad for large arrays), only on size changes.
You will have to create a separate connected component for your array items. I have worked up a working demo here.

Setting a defaultValue for TimePicker using redux-form and materialUI?

I can't make any version of setting a defaultValue (or defaultTime or even just value) work for initializing a TimePicker in redux-form from state. All other fields populate correctly, but date/time pickers don't respond to anything. Help is muuuuch appreciated!
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import moment from 'moment';
import { Field, reduxForm, FieldArray, formValueSelector } from 'redux-form';
import MenuItem from 'material-ui/MenuItem';
import {
SelectField,
TextField,
DatePicker,
TimePicker
} from 'redux-form-material-ui';
const selector = formValueSelector('myPantry');
let ServiceGroup = ({ service, index, fields, isAppointment, serviceDate }) =>
<div className="service-type-group" key={index}>
<h4>Service</h4>
<button
type="button"
className="remove-button"
onClick={() => fields.remove(index)}>Remove
</button>
<div className="field-group third">
<Field
name={`${service}.day`}
component={SelectField}
floatingLabelText="Day of week"
className="textfield">
<MenuItem value={1} primaryText="Monday" />
<MenuItem value={2} primaryText="Tuesday" />
</Field>
<Field
name={`${service}.from_time`}
component={TimePicker}
value={null}
floatingLabelText="From"
className="textfield"
/>
<Field
name={`${service}.until_time`}
component={TimePicker}
value={null}
floatingLabelText="To"
className="textfield"
/>
</div>
<div className="field-group half">
<Field
name={`${service}.service_type`}
component={SelectField}
floatingLabelText="Service type"
className="textfield">
<MenuItem value={1} primaryText="First-come first-served" />
<MenuItem value={2} primaryText="Appointment" />
</Field>
</div>
{isAppointment &&
<div className="field-group sentence-inline">
<Field
name={`${service}.max_people`}
type="number"
component={TextField}
label="Number of people per timeslot"
className="textfield"
/>
</div>
}
</div>;
ServiceGroup = connect(
(state, props) => ({
isAppointment: selector(state, `${props.service}.service_type`) == 2,
serviceDate: selector(state, `${props.service}.from_time`)
})
)(ServiceGroup);
class MyPantry extends Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired
}
constructor(props, context) {
super(props, context);
}
renderGroups = ({ fields, meta: { touched, error } }) => {
return (
<div>
{fields.map((service, index) =>
<ServiceGroup service={service} fields={fields} index={index} key={index} />
)}
<button
type="button"
className="action-button"
onClick={() => fields.push({})}>Add Service
</button>
{touched && error && <span>{error}</span>}
</div>
);
}
render() {
const { handleSubmit } = this.props;
return (
<form onSubmit={handleSubmit}>
<div className="section general">
<Field
name="name"
type="text"
component={TextField}
label="Pantry name"
floatingLabelText="Pantry name"
className="textfield full"
/>
<div className="field-group half">
<Field
name="address_street_1"
type="text"
component={TextField}
label="Address"
floatingLabelText="Address"
className="textfield"
/>
<Field
name="address_street_2"
type="text"
component={TextField}
label="Apartment, suite, etc."
floatingLabelText="Apartment, suite, etc."
className="textfield"
/>
</div>
</div>
<h3 className="section-title">Service Setup</h3>
<div className="section service">
<FieldArray name="service_options" component={this.renderGroups} />
</div>
<div>
<button type="submit" className="action-button">Save Pantry Details</button>
</div>
</form>
);
}
}
MyPantry = reduxForm({
form: 'myPantry'
})(MyPantry);
MyPantry = connect(
state => ({
initialValues: state.pantry.data
})
)(MyPantry);
export default MyPantry;
To set default time of TimePicker:
<TimePicker hintText="Pick your time" defaultTime={new Date(2007, 11, 5, 8, 23, 17)} />
here is a working JSFiddle: https://jsfiddle.net/mu5r94m6/2/
You're using this.props.pantry... inside your renderGroups().
You need to bind that function to be able to use this
add a constructor to the class and use bind on that function
constructor(props) {
super(props);
this.renderGroups = this.renderGroups.bind(this);
}
Can now pass format={null} to Time and Date pickers for this. See thread/closed issue here: https://github.com/erikras/redux-form-material-ui/issues/37
and can view tweets that start here: https://twitter.com/megkadams/status/804363887428665345 and continue here: https://twitter.com/megkadams/status/804369699693793280
<Field
name={`${service}.from_time`}
component={TimePicker}
format={(value, name) => {
if (fromTime) {
return value === '' ? null : new Date(fromTime)
} else {
return null;
}
}}
defaultValue={null}
pedantic
floatingLabelText="From"
className="textfield"
/>

Resources