The use case is there will be add topic button which when clicked should show a form for adding the topic. When user fills the topic form and hits the save button, that topic should be shown in the input box with edit button instead of add. There can be multiple topics. For example, if I have 4 topics already or saved them after adding then they should be displayed with edit button. The way I am doing is not even triggering handleChange.
I have created a sandbox for this and here it is
https://codesandbox.io/s/koqqvz2307
The code
class FieldArray extends React.Component {
state = {
topics: [],
topic: ""
};
handleChange = e => {
console.log("handleChange", e);
this.setState({ topic: { ...this.state.topic, topic: e.target.value } });
};
handleSubmit = e => {
e.preventDefault();
console.log("state of topics array with multiple topics");
};
render() {
return (
<div>
<FieldArrayForm
topics={this.state.topics}
topic={this.state.topic}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
/>
</div>
);
}
}
export default FieldArray;
const renderField = ({ input, label, type, meta: { touched, error } }) => (
<div>
<label>{label}</label>
<div>
<input {...input} type={type} placeholder={label} />
{touched && error && <span>{error}</span>}
</div>
</div>
);
const renderTopics = ({
fields,
meta: { error },
handleChange,
handleSubmit,
topic
}) => (
<ul>
<li>
<button type="button" onClick={() => fields.push()}>
Add Topic
</button>
</li>
{fields.map((topicName, index) => (
<li key={index}>
<span>
<Field
name={topicName}
type="text"
onChange={handleChange}
component={renderField}
label={`Topic #${index + 1}`}
/>
<span>
<button
type="button"
title="Remove Hobby"
onClick={() => fields.remove(index)}
>
Remove
</button>
{topic ? (
<button type="button" title="Add" onSubmit={handleSubmit}>
Edit
</button>
) : (
<button type="button" title="Add" onSubmit={handleSubmit}>
Add
</button>
)}
</span>
</span>
</li>
))}
{error && <li className="error">{error}</li>}
</ul>
);
const FieldArraysForm = props => {
const { handleSubmit, pristine, reset, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<FieldArray name="topic" component={renderTopics} />
</form>
);
};
export default reduxForm({
form: "fieldArrays", // a unique identifier for this form
validate
})(FieldArraysForm);
How do i save and show multiple topics when using redux-form? I tried to take the concept from fieldarray but i could not do it yet.
Your handleChange is undefined, and this is why your function isn't being called.
If you are willing that renderTopics receive a handleChange function, you should pass the handleChange prop to the FieldArray component (according to redux-form docs):
const FieldArraysForm = props => {
const { handleChange, handleSubmit, pristine, reset, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<FieldArray name="topic" component={renderTopics} handleChange={handleChange} />
</form>
);
};
Alternatively, you can simply pass all props from FieldArraysForm to the FieldArray component:
const FieldArraysForm = props => (
<form onSubmit={handleSubmit}>
<FieldArray name="topic" component={renderTopics} {...props} />
</form>
);
Related
I have a component call ExpenseList.js which does look like below. But my problem is when I tried to edit item and click save, setting isEditable inside "Save" button event handler does not trigger re-render.
import { useState } from "react";
import { useExpenses, useExpenseDispatch } from "./BudgetContext.js";
export default function ExpenseList() {
const expenses = useExpenses();
return (
<ul>
{expenses.map((expense) => (
<li key={expense.id}>
<Expense expense={expense} />
</li>
))}
</ul>
);
}
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
const dispatch = useExpenseDispatch();
let content;
if (isEditing) {
content = (
<>
<input
value={expense.description}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
description: e.target.value
}
});
}}
/>
<input
value={expense.amount}
onChange={(e) => {
dispatch({
type: "changed",
expense: {
...expense,
amount: e.target.value
}
});
}}
/>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<span>{expense.description}</span> : <span>{expense.amount}</span>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<label>
{content}
<button
onClick={() => {
dispatch({
type: "deleted",
id: expense.id
});
}}
>
Delete
</button>
</label>
);
}
I was dabbling with this for hours, I think extra pair of eyes could point out what is going wrong?
Sandbox: https://codesandbox.io/s/clever-keller-l5z42e?file=%2FExpenseList.js%3A0-1614
Documentation reference
Content model:
Phrasing content, but with no descendant labelable elements unless it is the element's labeled control, and no descendant label elements.
As mentioned above, label has 2 different labelable elements unless it is the element's labeled control. When you are in edit mode, you have 3 different labelable elements (input-description, input-amount and button-save) which causes problems with event propagation.
But when you are not in edit mode, it just has 1 labelable element which is the edit button and hence, it works.
For solving your issue, you can swap the label at the root with something like div and then use labels explicitly for each of the inputs in content.
function Expense({ expense }) {
const [isEditing, setIsEditing] = useState(false);
let content;
if (isEditing) {
content = (
<>
<label>
Description:
<input
value={expense.description}
onChange={...}
/>
</label>
<label>
Amount:
<input
value={expense.amount}
onChange={...}
/>
</label>
<button onClick={() => setIsEditing(false)}>Save</button>
</>
);
} else
content = (
<>
<button onClick={() => setIsEditing(true)}>Edit</button>
</>
);
return (
<div>
{content}
<button>Delete</button>
</div>
);
}
Is it possible to substitute the value from useState with the one coming from input?
Or is there a way to do this using dispatch?
I have tried many ways, but none of them work.
const renderInput = ({
input,
label,
type,
meta: { asyncValidating, touched, error },
}) => {
const [value, setValue] = useState('default state');
const onChange = event => {
setValue(event.target.value);
// + some logic here
};
return (
<div>
<label>{label}</label>
<div className={asyncValidating ? 'async-validating' : ''}>
<input {...input} value={value} onChange={onChange} type={type} placeholder={label} />
{touched && error && <span>{error}</span>}
</div>
</div>
);
};
const SelectingFormValuesForm = props => {
const { type, handleSubmit, pristine, reset, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<div>
<Field
name="name"
component={renderInput}
type="text"
placeholder="Dish name..."
/>
</div>
</div>
</form>
);
};
SelectingFormValuesForm = reduxForm({
form: 'selectingFormValues',
validate,
asyncValidate,
// asyncBlurFields: ['name'],
})(SelectingFormValuesForm);
export default SelectingFormValuesForm;
This way, unfortunately, the value sent to the submit remains empty.
I am using React Hook Form. I've made a custom checkbox which looks like this:
const Checkbox = ({ text, className, setCheckbox, checkbox }) => {
const { register } = useFormContext();
const statute = register("statute");
return (
<Wrapper className={className}>
<StyledLabel>
<div>{text}</div>
<StyledInput
type="checkbox"
name="statute"
onChange={(e) => {
statute.onChange(e);
setCheckbox && setCheckbox(!checkbox);
}}
/>
<Checkmark />
</StyledLabel>
</Wrapper>
);
};
Checkbox (Checkbox.js) is nested in a parent component (Register.js):
const Register = ({ setRegisterView }) => {
const validationSchema = Yup.object().shape({
statute: Yup.bool().oneOf([true], "You need to accept the page statute."),
});
const methods = useForm({
mode: "onSubmit",
resolver: yupResolver(validationSchema),
});
const {
register: validate,
formState: { errors },
handleSubmit,
} = methods;
return (
<Wrapper>
<FormProvider {...methods}>
<Form onSubmit={handleSubmit(registerProcess)}>
<StyledCheckbox
text="Accept the page statute."
setCheckbox={null}
checkbox={null}
/>
{errors.statute && <Error>{errors.statute.message}</Error>}
<LoginButton type="submit">SIGN UP</LoginButton>
</Form>
</FormProvider>
</Wrapper>
);
};
export default Register;
The problem is that when I check the checkbox I get an information in errors.statute.message: statute must be a `boolean` type, but the final value was: `"on"`..
When I change this:
onChange={(e) => {
statute.onChange(e);
setCheckbox && setCheckbox(!checkbox);
}}
to this:
{...register("statute")}
then it works great and errors.statute.message shows correct message just when checked=false in checkbox input. But I need to have the extended version of onChange.
The problem is, that you not link the returned ref of the register call to your <StyledInput />. So just spread the return value of register - in the code example below i also omitted the name prop as it is included in statute. Here you can find all the props register will return.
<StyledInput
type="checkbox"
{...statute}
onChange={e => {
statute.onChange(e);
setCheckbox && setCheckbox(!checkbox);
}}
/>
I need to access a method handleCancelEdit() defined in parent component. But, the matter here is that every child component will have its own cancelEdit state. Now, what is happening is, if I call handleCancelEdit() from one child component, every other of the same child components is taking the state and updating themselves(the method is not completely defined yet). That's why, I have defined the cancelEdit state in the child component, thinking that it belongs to this child component only.
Now, how do I make the handleCancelEdit() method make changes to the only child component which called it?
The parent:
function Parent() {
const handleCancelEdit = () => {
setCancelEdit(!cancelEdit); // defined in child component
setEdit(!edit); // defined in child component
...
};
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
on_change={(e)=>setEmail(e.target.value)}
on_click={handleUserEmail}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
on_change={(e)=>setAbout(e.target.value)}
on_click={handleUserAbout}
/>
</div>
);
}
Child component:
function ChildComponent({fieldName, value, inputType, placeHolder, name, on_change, on_click}) {
const [ edit, setEdit ] = useState(false);
const [ cancelEdit, setCancelEdit ] = useState(false)
const isEdit = edit;
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={e=>setEdit(!edit)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={on_change}
/>
<button type="submit" onClick={on_click}>Save</button>
<button type="submit" onClick={handleCancelEdit}>Cancel</button>
</span>)
)}
</p>
);
};
I hope it could make it understandable that one child component should not make others to update. Now, how do I do it in this scenario?
EDIT
After making changes according to Linda Paiste:
The input field in the child component is not working even though the onChange in both parent and child is correct!
It is always more logical to pass state and data down rather than up. If the Parent needs to interact with the edit state then that state should live in the parent. Of course we want independent edit states for each child, so the parent can't just have one boolean. It needs a boolean for each child. I recommend an object keyed by the name property of the field.
In ChildComponent, we move isEdit and setEdit to props. handleCancelEdit is just () => setEdit(false) (does it also need to clear the state set by onChange?).
function ChildComponent({fieldName, value, inputType, placeHolder, name, onChange, onSubmit, isEdit, setEdit}) {
return (
<p>{fieldName}: {value === ''? (
<span>
<input type={inputType} placeholder={placeHolder}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Add</button>
</span>
) : ( !isEdit ? (<span> {value} <button onClick={() =>setEdit(true)}>Edit</button></span>) :
(<span>
<input type={inputType} value={value}
name={name} onChange={onChange}
/>
<button type="submit" onClick={onSubmit}>Save</button>
<button type="submit" onClick={() => setEdit(false)}>Cancel</button>
</span>)
)}
</p>
);
};
In Parent, we need to store those isEdit states and also create a setEdit function for each field.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const handleSetEdit = (name, isEdit) => {
setIsEditFields((prev) => ({
...prev,
[name]: isEdit
}));
};
/* ... */
return (
<div>
<ChildComponent
fieldName={"Email"}
value={email}
inputType={"text"}
placeHolder={"Enter email"}
name={"email"}
onChange={(e) => setEmail(e.target.value)}
onSubmit={handleUserEmail}
isEdit={isEditFields.email}
setEdit={(isEdit) => handleSetEdit("email", isEdit)}
/>
<ChildComponent
fieldName={"About"}
value={about}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
name={"about"}
onChange={(e) => setAbout(e.target.value)}
onSubmit={handleUserAbout}
isEdit={isEditFields.about}
setEdit={(isEdit) => handleSetEdit("about", isEdit)}
/>
</div>
);
}
You can clean up a lot of repeated code by storing the values as a single state rather than individual useState hooks. Now 5 of the props can be generated automatically just from the name.
function Parent() {
const [isEditFields, setIsEditFields] = useState({});
const [values, setValues] = useState({
about: '',
email: ''
});
const getProps = (name) => ({
name,
value: values[name],
onChange: (e) => setValues(prev => ({
...prev,
[name]: e.target.value
})),
isEdit: isEditFields[name],
setEdit: (isEdit) => setIsEditFields(prev => ({
...prev,
[name]: isEdit
}))
});
const handleUserEmail = console.log // placeholder
const handleUserAbout = console.log // placeholder
return (
<div>
<ChildComponent
fieldName={"Email"}
inputType={"text"}
placeHolder={"Enter email"}
onSubmit={handleUserEmail}
{...getProps("email")}
/>
<ChildComponent
fieldName={"About"}
inputType={"text"}
placeHolder={"Enter some details about yourself"}
onSubmit={handleUserAbout}
{...getProps("about")}
/>
</div>
);
}
I am trying to make a field validation in a formik. The problem is that I made a multi-step form, my approach is that I made an array of components and I use them based on pages[state] and I don't know how to send props to that.
I want to validate the field to take only two digit numbers, and to show a error message if it doesn't
const SignUp = () => {
const state = useSelector((state: RootState) => state);
console.log("state", state);
const dispatch = useDispatch();
return (
<Formik
initialValues={{ firstName: "", lastName: "", email: "" }}
onSubmit={values => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
}, 500);
}}
render={({ errors, touched, isValidating }) => (
<Form>
<div>{pages[state]}</div>
<button
type="button"
onClick={() => {
dispatch(decrement());
}}
>
Prev
</button>
<button
type="button"
onClick={() => {
pages.length - 1 <= state
? console.log("No more pages")
: dispatch(increment());
}}
>
Next
</button>
</Form>
)}
/>
);
};
const Page2 = () => {
return (
<>
<h1>
What is your <span id="idealWeightText">ideal weight</span> that you
want to reach?
</h1>
<Input
name={"firstName"}
htmlFor={"firstName"}
type={"number"}
validation={validateKilograms}
/>
</>
);
};
const Input: React.FC<FieldProps> = ({
name,
onChange,
htmlFor,
type,
validation,
placeholder
}) => {
return (
<>
<label htmlFor={name}>
<Field
type={type}
name={name}
placeholder={placeholder}
validate={validation}
onChange={onChange}
/>
kg
</label>
</>
);
};
There is a library called Yup, That You can use, You will get freedom to set conditional validations.
Yup is best suited with formik for validations.
After Every step You can enable a flag and set the validation accordingly.
Also I would suggest you to use withFormik. Instead of formik because there you can make a separate file for validations and can give its path. So that will also improve your folder structure.