I have a React Component (originally written by someone else) that displays a form for person (redux-form). Recently, I've changed the component to be a FieldArray (component of redux-form).
I have a validation for the email Field that impacts className for the email Field (red colored when email is incorrectly formatted, black colored otherwise). It worked fine when it wasn't a FieldArray but now it validates all email Fields at once because of
// (in constructor)
this.email = React.createRef();
// (in Field)
ref={props.parent.email}
, i.e. props.parent.email is a global/static ref.
Example: There are two persons. One of them has an incorrectly formatted email, but both emails are displayed in red.
As I understand it, I'd need to have a dynamic ref but that didn't work the way I tried.
ref={`${person}.email.ref`}
Error is
"Function components cannot have refs. Did you mean to use React.forwardRef()?"
I didn't find anything helpful on forwardRef regarding FieldArray, besides the fact that it is a valid prop.
My objective is: When several persons are created by the user and/or loaded from Redux store, be able to show every correctly formatted email in black, and every incorrectly formatted email in red.
Any help is greatly appreciated!
import React from "react";
import { Field, reduxForm, FieldArray } from "redux-form";
import { connect } from "react-redux";
import classNames from 'classnames'
import { MAIL_PATTERN } from "../some_file";
import MoreGeneralComponent from "../some_other_file";
const renderField = ({ input, label, type, options, className, meta: { touched, error }, style, disabled, hidden }) => (
<div style={style}>
<label>{label}</label>
<div>
<input {...input} disabled={disabled ? true : false} hidden={hidden ? true : false} type={type} placeholder={label} className={className} />
</div>
</div>
);
const renderPerson = ({ fields, meta: { error, submitFailed }, ...props }) => {
setTimeout(props.validate(fields));
return (
<ul className="personlist">
{fields.map((person, index) => (
<li key={index} className="person">
<h4>Person #{index + 1}</h4>
<Field
component={renderField}
type="text"
ref={props.parent.email}
className={classNames({invalid: !props.parent.state.validEmail})}
validate={props.parent.validateEmail}
name={`${person}.email`}
label="Email"
key={`${person}.email`}
></Field>
<button type="button" onClick={() => fields.remove(index)}>
Remove
</button>
</li>
))}
{(!(fields.length >= props.max)) && (
<li className="addperson">
<button
type="button"
onClick={() => fields.push({})}
disabled={fields.length >= props.max}
>
Add Person
</button>
{submitFailed && error && <span>{error}</span>}
</li>)}
</ul>
);
};
class Person extends MoreGeneralComponent {
constructor(props) {
super(props);
if (this.state.ready) {
this.max = 4;
this.data = ["email"];
this.email = React.createRef();
}
}
validate = fields => {
if (!fields || !fields.getAll()) {
return;
}
let valid = true;
fields.getAll().forEach(field => {
for (let d of this.data) {
if (!field[d] || field[d].length < 2) {
valid = false;
return;
} else if (d === "email") {
valid = field[d] && MAIL_PATTERN.test(field[d]) ? valid : undefined;
}
}
});
if (valid !== this.state.valid) {
this.setState({
valid: valid
});
}
};
validateEmail = (value) => {
const valid = value && MAIL_PATTERN.test(value) ? value : undefined;
this.setState({validEmail: !!valid});
return valid
}
renderQuestion() {
return (
<div className={style.question}>
<fieldset>
<FieldArray
name="persons"
component={renderPerson}
props={{ max: this.max, validate: this.validate, parent: this }}
rerenderOnEveryChange={true}
/>
</fieldset>
</div>
);
}
}
const mapStateToProps = s => {
const persons = s.persons
var initialValuesPersons = []
persons.map(item => initialValuesPersons.push({
"email": item.email || ""
}))
var initialValues = { "persons": initialValuesPersons}
return {
initialValues,
formdata: s.form
}
}
export default connect(mapStateToProps, null)(reduxForm(
{
form: 'person',
destroyOnUnmount: false,
enableReinitialize: true,
keepDirtyOnReinitialize: true
})(Person))
With the help of a colleague I came up with a work-around that just uses JS (working around using setState).
toggleClass = (elementName, addInvalid) => {
const element = document.getElementsByName(elementName);
element.forEach(item => {
// remove class and add later, if necessary
item.classList.remove("invalid");
if (addInvalid) {
item.classList.add("invalid");
}
})
}
validateEmail = (value, person, form, nameField) => {
const valid = value && MAIL_PATTERN.test(value) ? value : undefined;
this.toggleClass(nameField, !valid);
return valid;
}
Related
I'm using the liowebrtc react library to create a chat app. This library assigns a unique identifier (peer.id) to each peer (people not you in your chatroom). However, you can also assign yourself a nickname which then gets passed as peer.nick. At least, that's how it's supposed to work. But the peer.nick value always returns undefined, even though it's present in the Peer object. For instance, I pass a nickname to the Liowebrtc component as shown below:
<LioWebRTC
options={{ debug: true,
dataOnly: true,
nick: "Pippin" }}
onReady={join}
onCreatedPeer={handleCreatedPeer}
onReceivedPeerData={handlePeerData} >
If I print the peer object to console in handleCreatedPeer (the function that gets called when the app is informed of the presence of someone other than yourself in the chat room), I can see peer has a nick with value "Pippin." However, trying to call peer.nick or peer["nick"] always produces undefined.
For instance,
console.log("Peer id: ", peer.id); // valid value
console.log("Peer nick: ", peer.nick); // undefined
console.log("Peer one: ", peer.oneway); // valid value
even though if I print the Peer object in console, I can see:
Strangely though, I I print the keys of the Peer object in console, I see:
As you can see, nick is missing from the key list, even though it's there when you log the object in its entirety.
Why is peer.nick always undefined? Is there a different way I'm supposed to call it?
Here is the ChatWrapper class in its entirety:
// Code modified from https://medium.com/#leontosy/building-a-p2p-web-app-with-react-and-liowebrtc-6a7e8c621085
import React, { useState } from 'react';
import { LioWebRTC } from 'react-liowebrtc';
import ChatBox from './ChatBox';
const ChatWrapper = props => {
const [chatLog, setChatLog] = useState([])
const addChat = (name, message, alert = false) => {
setChatLog( chatLog => {
return [ ...chatLog,
{
name,
message: `${message}`,
timestamp: `${Date.now()}`,
alert
}
];
})
};
const join = (webrtc) => webrtc.joinRoom(props.roomID.toString());
const handleCreatedPeer = (webrtc, peer) => {
console.log(peer.nick)
addChat(`Peer-${peer.id.substring(0, 5)} joined the room!`, ' ', true);
}
const handlePeerData = (webrtc, type, payload, peer) => {
switch(type) {
case 'chat':
addChat(`Peer-${peer.id.substring(0, 5)}`, payload);
break;
default:
return;
};
}
return (
<div>
<p>Room: {props.roomID}</p>
<LioWebRTC
options={{ debug: true,
dataOnly: true,
nick: "Pippin"}}
onReady={join}
onCreatedPeer={handleCreatedPeer}
onReceivedPeerData={handlePeerData} >
<ChatBox
chatLog={chatLog}
onSend={(msg) => msg && addChat('Me', msg)}
/>
</LioWebRTC>
</div>
);
}
export default ChatWrapper;
and here is the ChatBox class in its entirety:
// Code from https://medium.com/#leontosy/building-a-p2p-web-app-with-react-and-liowebrtc-6a7e8c621085
import React, { Component } from 'react';
import './ChatBox.css';
import { withWebRTC } from 'react-liowebrtc';
class ChatBox extends Component {
constructor(props) {
super(props);
this.state = {
inputMsg: ''
};
}
generateChats = () => {
if(this.chatBox) {
setTimeout(() => { this.chatBox.scrollTop = this.chatBox.scrollHeight; }, 2);
}
return this.props.chatLog.map((item) => (
<div className="chat" key={`chat-${item.name}-${item.timestamp}`}>
<b className="name" style={{ color: item.alert ? '#888' : '#333' }}>{item.name}</b> <span className="msg">{item.message}</span>
</div>
));
}
handleSend = (chatMsg) => {
this.props.webrtc.shout('chat', chatMsg);
this.props.onSend(chatMsg);
}
handleKeyUp = (evt) => {
if (evt.keyCode === 13) {
this.handleSend(this.state.inputMsg);
this.setState({ inputMsg: '' });
}
}
handleInputChange = (evt) => this.setState({ inputMsg: evt.target.value });
render() {
const { chatLog } = this.props;
return (
<div className="container">
<div className="chatHeader">
<h1 className="title">P2P Chat Example</h1>
<hr />
</div>
<div className="chatBox" ref={(div) => this.chatBox = div}>
{chatLog.length ? this.generateChats() : (
<div className="info">
<p>To test this component out, open this page in a new tab or send it to a friend.</p>
</div>
)}
</div>
<hr />
<div className="bottomBar">
<input className="chatInput" type="text" placeholder="Type a message..." onKeyUp={this.handleKeyUp} onChange={this.handleInputChange} value={this.state.inputMsg} />
</div>
</div>
);
}
}
export default withWebRTC(ChatBox);
How can I convert a Class Component which extends another Class component in a Functional Component in ReactJS?
input.jsx [Functional Component]
const Input = ({ name, label, error, ...rest }) => {
return (
<div className="mb-3">
<label htmlFor={name} className="form-label">
{label}
</label>
<input
autoFocus
{...rest}
id={name}
name={name}
className="form-control"
/>
{error && <div className="alert alert-danger">{error}</div>}
</div>
)
}
export default Input
form.jsx [Class Component]
import React, { Component } from "react"
import Input from "./input"
import Joi from "joi"
class Form extends Component {
state = {
data: {},
errors: {}
}
validate = () => {
const options = { abortEarly: false }
const schemaJoi = Joi.object(this.schema)
const { error } = schemaJoi.validate(this.state.data, options)
if (!error) return null
const errors = {}
error.details.map(item => (errors[item.path[0]] = item.message))
return errors
}
validateProperty = ({ name, value }) => {
const obj = { [name]: value }
const schema = {
[name]: this.schema[name]
}
const schemaJoi = Joi.object(schema)
const { error } = schemaJoi.validate(obj)
return error ? error.details[0].message : null
}
handleSubmit = e => {
e.preventDefault()
const errors = this.validate()
console.log(errors)
this.setState({ errors: errors || {} })
if (errors) return
this.doSubmit()
}
handleChange = ({ currentTarget: input }) => {
const errors = { ...this.state.errors }
const errorMessage = this.validateProperty(input)
if (errorMessage) errors[input.name] = errorMessage
else delete errors[input.name]
const data = { ...this.state.data }
data[input.name] = input.value
this.setState({ data, errors })
}
renderButton = label => {
return (
<button disabled={this.validate()} className="btn btn-primary">
{label}
</button>
)
}
renderInput = (name, label, type = "text") => {
const { data, errors } = this.state
return (
<Input
name={name}
label={label}
error={errors[name]}
type={type}
value={data[name]}
onChange={this.handleChange}
/>
)
}
}
export default Form
loginForm.jsx [Class Component which extends the other]
import Joi from "joi"
import Form from "./common/form"
class LoginForm extends Form {
state = {
data: { username: "", password: "" },
errors: {}
}
schema = {
username: Joi.string().required().label("Username"),
password: Joi.string().required().label("Password")
}
doSubmit = () => {
console.log("Submitted")
}
render() {
return (
<div>
<h1>Login</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("username", "Username")}
{this.renderInput("password", "Password", "password")}
{this.renderButton("Login")}
</form>
</div>
)
}
}
export default LoginForm
I already know how to convert a simple Class Component to a Stateless Functional Component but what I don't know is how to convert a Class Component which extends another Class Component.
Please, may you explain me how?
I can insert the input value say "1,2,3" and when backspace it removes all but in the console "1" is still shown i.e., House{props{house{property{rent:1}}}}
I am providing the code here which has 3 files.
(1) house.js
import ValInput from "main/components/val-input";
class House extends Component {
state = {
rent:"",
};
componentDidMount() {
if (this.props.house.rent) {
const { rent} = this.props.house;
this.setState({ rent });
}
}
onChange = (e) => {
const rent = parseInt(e.target.value.replace(string);
this.setState({
rent,
});
};
render(){
const {house} = this.props;
const {rent} = this.state;
...
<ValInput
type="text"
value={ rent }
onChange={e => {
this.onChange(e);
}}
/>
}
(2) val-input\index.js
import React from "react";
import Input from "main/components/input";
const ValInput = props => (
<Input
{...props}
type={props.type ? props.type : "text"}
/>
);
export default valInput;
(3) components/input/index.js
import React from "react";
const noOp = () => {};
const Input = ({
onBlur = xP,
...otherProps
}) => (
<input
onBlur={e => {
e.target.placeholder = placeholder;
onBlur(e);
}}
{...otherProps}
/>
);
export default Input;
The expected result should be, after emptying the value say with backspace, and visit the page next time, the input field should be empty and should not show old value.
Check this CodeSandbox out I replicated your code and if I understood the problem right then fixed it
https://reactjs.org/docs/cross-origin-errors.html
For updating #NaderZouaoui, has given me an example how to do Call back :
1. Child file :
onChange={e => {
this.onChange(e);
}}
onChange = e => {
this.setState({
rent
});
this.props.callback(rent);
};
2. Parent file :
state = {
rent: ""
};
handleChangeRent = newRent => {
this.setState({ rent: newRent });
};
render(){
return(
<House house={{ rent }} callback={this.handleChangeRent} />
);
}
I am adding a user via phone number.
The submit button is disabled until all 10 digits are entered, that works fine. When I click the submit button and try to add a new user with a different phone number, the submit button is enabled until I hit the first key then the button becomes disabled again until the rest of the 9 digits are entered into the field.
When I tried to create a componentDidMount function the app completely breaks. Here is a bit of my code:
import React from 'react'
import { Dialog, Button, TextField } from 'material-ui'
import { connect } from 'react-redux'
import { inviteAdminByPhone } from '../db'
import MaskedInput from 'react-text-mask'
import { phoneNumber } from '../masks'
import SpinnerButton from '../components/spinner-button'
import { CircularProgress } from 'material-ui/Progress'
const byId = id => document.getElementById(id)
class AddManagerModal extends React.Component {
state = {
disabled: true,
phone: ''
}
componentDidUpdate() {
if ({ disabled: false && this.state.phone < 10 }) {
this.checkPhoneFields()
}
}
handleChange = (name, id) => {
this.setState({
[name]: document.getElementById(id).value
})
}
PhoneMask = () => {
return (
<MaskedInput
mask={phoneNumber}
placeholderChar={'\u2000'}
placeholder="Phone Number"
id="add-manager-phone"
className="masked-input"
onChange={e => {
this.handleChange('phone', 'add-manager-phone')
console.log(e.target.value)
if (e.target.value.replace(/[(.)\s]/g, '').length === 10) {
this.setState({ disabled: false })
} else {
this.setState({ disabled: true })
}
}}
/>
)
}
handleOpen = () => {
this.props.dispatch({
type: 'ADD_MANAGER_MODAL',
payload: !this.props.open
})
}
checkPhoneFields = () => {
if (byId('login-phone').value.replace(/[(.)\s]/g, '').length === 10) {
this.setState({ disabled: false })
return
}
this.setState({ disabled: true })
}
render() {
const props = this.props
const show = props.show
const phone = this.state.phone
return (
<div>
<Dialog open={props.open} onBackdropClick={() => this.handleOpen()}>
<div className="dialog-padding max-width-300">
<h3 className="oswald font-light tal mb0">Managers Phone Number</h3>
<TextField
value={phone}
fullWidth
className="max-width-250"`enter code here`
InputProps={{ inputComponent: this.PhoneMask }}
/>
</div>
<div className="dialog-padding tar">
<Button
variant="flat"
color="secondary"
onClick={() => this.handleOpen()}
>
Cancel
</Button>
<SpinnerButton
id="invite-new-manager"
variant="raised"
color="primary"
disabled={this.state.disabled}
onClick={() =>
props.addAdmin(
byId('add-manager-phone').value,
show.eid,
'invite-new-manager'
)
}
label="Invite"
/>
</div>
</Dialog>
</div>
)
}
}
const mapStateToProps = state => {
return {
open: state.addManagerModal,
show: state.show
}
}
const mapActionToProps = dispatch => {
return {
dispatch,
addAdmin: (number, event_id, id) =>
dispatch(inviteAdminByPhone(number, event_id, id))
}
}
const connector = connect(mapStateToProps, mapActionToProps)
export default connector(AddManagerModal)
I think you need to initialize your state in a constructor. This will always initialize the state before any of the lifecycle methods are called. You shouldn't need the componentDidMount call then.
Propably you're getting this error, because byId cannot find requested element (with id "login-phone") and returns null.
Change condition in componentDidUpdate, because object is resolved to true when it's converted to boolean.
if ({}){
console.log("Condition satisfied");
}
if ({disabled: false}){
console.log("This one too");
}
I think you need something like:
componentDidUpdate()
{
if (!this.state.disabled && this.state.phone < 10 )
{
this.checkPhoneFields()
}
}
or (in case when this.state.phone is string and you need to check its length):
componentDidUpdate()
{
if (!this.state.disabled && this.state.phone && this.state.phone.length < 10 )
{
this.checkPhoneFields()
}
}
It seems like disabled variable doesn't exist. Should it be this.state.disabled? Because I saw you declare one in the constructor.
componentDidUpdate()
{
if ({ disabled: false && this.state.phone < 10 })
{
this.checkPhoneFields()
}
}
i have a component which based upon props renders a form with different components.
class Feedback extends Component {
submitMyForm(data) {
const { onSubmit, reset } = this.props;
reset();
return onSubmit(data);
//
// do other success stuff
}
render() {
const { handleSubmit } = this.props;
let component;
if(this.props.data.feedbackType == "likert")
component = Likert;
else if(this.props.data.feedbackType == "single choice")
component = SingleChoice;
else if(this.props.data.feedbackType == "multiple choice")
component = MultipleChoice;
return (
<div>
<h1>Feedback zu Aufgabe {this.props.id}</h1>
<form onSubmit={handleSubmit(this.submitMyForm.bind(this))}>
<Field
name="feedback"
component={component}
heading={this.props.data.description}
items={this.props.data.options}
required={true}
/>
<button type="submit">Submit</button>
</form>
</div>
);
}
}
// Decorate the form component
Feedback = reduxForm({
form: 'feedback', // a unique name for this form,
validate,
enableReinitialize:true
})(Feedback);
function validate(formProps) {
const errors = {};
if (!formProps.feedback) {
errors.feedback = 'please select an option';
}
return errors;
}
export default Feedback;
import React, { PropTypes } from 'react';
const SingleChoice = ({ input, disabled, heading, required, className, items, name, meta: { touched, error } }) => (
<fieldset className={`form__field ${className || ''}`}>
<legend className="form__label">
{heading}{required ? (<span>*</span>) : null}
{ (touched && error) ? (
<span className="form__error"> {error}</span>
) : null }
</legend>
<div>
{ items.map((item, i) => (
<div className="form__segmented-control width-1/2#small" key={ i }>
<input
{...input}
name={ name }
type="radio"
value={ item.value }
disabled={ disabled }
className="segmented-control__input u-option-bg-current"
id={ `${name}-${item.value}` }
/>
<label className="segmented-control__label u-adjacent-current" htmlFor={ `${name}-${item.value}` }>
{item.label}
</label>
</div>
))
}
</div>
</fieldset>
);
SingleChoice.propTypes = {
input: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
className: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
})).isRequired,
heading: PropTypes.string,
meta: PropTypes.object,
required: PropTypes.bool,
disabled: PropTypes.bool,
};
export default SingleChoice;
The first time the form renders everything is fine. All radio buttons are unchecked and if i try to submit it i get an validation error as intended. But when my Feeback component receives new props and the form is updated. The old values still remain selected when the form component for the new props is the same as the one for the old props.
When the form component for the new props is different all values are not selected as intended, but i can submit the form without selecting anything, which should be prevented by validation.
I hope you got any suggestions, i am totally out of ideas at this point.
I searched for hours trying to find a resolution to this problem. The best way I could fix it was by using the plugin() API to teach the redux-form reducer to respond to the action dispatched when your submission succeeds. Exactly like the first step here How can I clear my form after my submission succeeds?
const reducers = {
// ... your other reducers here ...
form: formReducer.plugin({
nameOfTheForm: (state, action) => { // <- 'nameOfTheForm' is name of form
switch(action.type) {
case ACCOUNT_SAVE_SUCCESS:
const values = undefined;
const fields = {
fields: {
input_field_name: {
visited: false,
touched: false
}
// repeat for each input field
}
};
const newState = { ...state, values, fields };
return newState;
default:
return state;
}
}
})
}
You will have to change a couple things in your component.
onSubmit(values) {
this.props.postForm(values, () => {});
}
render(){
const { handleSubmit } = this.props;
}
return (
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}></form>
)
In your actions file:
export function postForm(values, callback) {
const request = axios.get(`${ROOT_URL}`, config).then(() => callback());
return {
type: ACCOUNT_SAVE_SUCCESS,
payload: request
};
}
The best way I found:
import 'initialize'...
import {initialize} from 'redux-form';
and then when you call the action, call another action right after passing an empty object to the 'initialize' function...
yourFunction = (data) => {
this.props.dispatch(yourAction(data))
.then(
result => {
if (result) {
this.props.dispatch(initialize('nameOfTheForm', {}));
}
}
);
when my Feeback component receives new props and the form is updated, the old values still remain selected when the form component for the new props is the same as the one for the old props.
This is because the values for the feedback form are stored in your Redux store.
You should implement componentWillReceiveProps and test whether your form should be reset or not.
class Feedback extends Component {
componentWillReceiveProps ( nextProps ) {
if ( nextProps.blabla !== this.props.blabla ) {
// oh cool, props changed, let's reset the form
// checkout more available form props at http://redux-form.com/6.4.3/docs/api/ReduxForm.md/
this.props.reset();
}
}
render () {
// your normal rendering function
}
}