I have a component in React that has required in its fields. The idea is onClick will trigger a separate function that adds a soccer player to two different stores (similar to a TODO app). However, it appears that the validation does not work -name for example is a required string, but the form seems to add a player even if the string is empty. The same goes for the two number inputs.
I don't want to use onSubmit because this seems to refresh the page every time, which causes me to lose data.
I'm using the react-modal library for my forms. To start with here's a function that opens the modal:
function renderAddButton() {
return (
<Row
horizontal='center'
vertical='center'
onClick={openModal}
>
Add Player
</Row>
);
}
Here's the modal and its hooks:
const [playerName, setPlayerName] = useState('');
const [totalGoals, setTotalGoals] = useState(0);
const [goalPercentage, setGoalPercentage] = useState(0);
function openModal() {
setIsOpen(true);
}
function closeModal() {
setIsOpen(false);
}
<Modal
isOpen={modalIsOpen}
onRequestClose={closeModal}
style={modalStyles}
contentLabel='Player Form Modal'
>
<h3>Create Player</h3>
<form>
<label>Name</label>
<input
type='string'
id='playerNameId'
name='playerName'
defaultValue=''
onChange={(e) => setPlayerName(e.target.value)}
required
/>
<label>Total Goals</label>
<input
type='number'
id='totalGoalsId'
name='totalGoals'
defaultValue='0'
min='0'
onChange={(e) => setTotalGoals(e.target.value)}
required
/>
<label>Goal Percentage</label>
<input
type='number'
id='goalPercentageId'
name='playerGoalPercentage'
defaultValue='0'
min='0'
step ='0.01'
max='1'
onChange={(e) => setGoalPercentage(e.target.value)}
required
/>
<button onClick={(e) => onAddButtonClick(e)}>Submit</button>
</form>
<button onClick={closeModal}>close</button>
</Modal>
And now when this function is triggered, the validations don't seem to work. Empty playerId and totalGoals and goalPercentage seem to go through fine. How do I validate the inputs and stop this function from running if the inputs are empty?
function onAddButtonClick(e) {
e.preventDefault();
setItems((prev) => {
const newItems = [...prev];
const uuid= uuidv4();
newItems.push({
name: playerName,
playerId:uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
playersStore.push({
name: playerName,
playerId:uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
return newItems;
});
}
The required attribute only works with default form actions. You'll need to do your own validation in the handler. You should also explicitly define the button type attribute as well since buttons by default are type="submit".
Create a validation function and pass your state values to it. Return true if input is valid, false otherwise.
const validateInput = ({ goalPercentage, playerName, totalGoals }) => {
if (!playerName.trim()) {
return false;
}
// other validations
return true;
};
Check the input in onAddButtonClick and only update state if input is valid.
function onAddButtonClick(e) {
e.preventDefault();
const validInput = validateInput({ goalPercentage, playerName, totalGoals });
if (!validInput) {
return null;
}
setItems((prev) => {
const newItems = [...prev];
const uuid= uuidv4();
newItems.push({
name: playerName,
playerId: uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
playersStore.push({
name: playerName,
playerId: uuid,
teamId: currentTeam[0].teamId,
totalGoals: totalGoals,
goalPercentage: goalPercentage
});
return newItems;
});
}
Update the button to have an explicit type.
<button
type="button"
onClick={onAddButtonClick}
>
Submit
</button>
you can do something like this
function onAddButtonClick(e) {
e.preventDefault();
if(playerName.trim()===''){ //you can make more validations here
return;
}
//down here use the rest of logic
}
Related
I am building an app where the flask rest API takes two strings and gives a floating value as a prediction. Now I am trying to connect to the react app so that the predictions can be shown on a webpage.
Goal: takes two strings from the frontend and does inference using restapi and gives the values of the prediction on the front end.
Below is the code used to fetch the rest API predictions in react app.
function App() {
const [state, setState] = useState([{}]);
useEffect(() => {
fetch("/predict?solute=CC(C)(C)Br&solvent=CC(C)(C)O")
.then((res) => res.json())
.then((data) => {
setState(data);
console.log(data);
});
}, []);
In fetch /predict?solute=CC(C)(C)Br&solvent=CC(C)(C)O here solute=CC(C)(C)Br and solvent=CC(C)(C)O are the inputs for the flask rest API to give predictions.
But I want to give it from the frontend rather than mentioned in the URL. How to do it?
Modified code to fetch results and display
function App() {
const [state, setState] = useState([{}]);
const [uri, setUri] = useState([{}]);
const [resultstate, setResultState] = useState([{}]);
function HandleSubmit() {
const uri = "http://127.0.0.1:3000/?${form.one}&${form.two}";
setUri(uri);
useEffect(() => {
fetch(uri)
.then((response) => {
if (response.status === 200) {
return response.json();
}
})
.then((data) => {
setResultState(data);
console.log(data);
});
});
}
function handleChange(e) {
const { nodeName, name, value } = e.target;
if (nodeName === "INPUT") {
setState({ ...FormData, [name]: value });
}
}
return (
<div className="App">
<state onChange={handleChange}>
<fieldset>
<legend>Solute</legend>
<input name="one" value={state.one} />
</fieldset>
<fieldset>
<legend>Solvent</legend>
<input name="two" value={state.two} />
</fieldset>
<button type="button" onClick={HandleSubmit}>
Submit
</button>
</state>
<Deploy />
</div>
);
}
Running the modified code I am getting this error
Create a new form state.
Create a form with some input elements, and a button to submit the form.
When the input elements are changed update the form state with their values.
When the form is submitted create a new URI with the information in form, and then do the fetch.
const { useState } = React;
function Example() {
const [ form, setForm ] = useState({});
function handleSubmit() {
const uri = `http://example.com/?${form.one}&${form.two}`;
console.log(`Current state: ${JSON.stringify(form)}`);
console.log(`Fetch URI: ${uri}`);
// fetch(uri)... etc
}
// Because the listener is attached to the form element
// (see "Additional documentation" below)
// check that the element that's been changed is an input
// and then update the state object using the name as a key, and
// adding the value of the input as the object value for that key
function handleChange(e) {
const { nodeName, name, value } = e.target;
if (nodeName === 'INPUT') {
setForm({ ...form, [name]: value });
}
}
// Each input has a name, and maintains its
// value from the form state
return (
<form onChange={handleChange}>
<fieldset>
<legend>Input one</legend>
<input name="one" value={form.one} />
</fieldset>
<fieldset>
<legend>Input two</legend>
<input name="two" value={form.two} />
</fieldset>
<button type="button" onClick={handleSubmit}>Submit</button>
</form>
);
};
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Additional documentation
Event delegation - that's what's happening when we attach one listener to the form rather than listeners to all the inputs. The form catches the events from the inputs as they bubble up the DOM. But it's also why we need to identify what element they are, hence the need for nodeName.
Destructuring assignment
I'm trying to make form validation with react-hook-form. It works fine exept one propblem: it doesn't check input type. I want user to input only URL address, but this thing validate it as a simpte text. Where did I make a mistake?
function EditAvatarPopup({ isOpen, onClose, onUpdateAvatar, submitButtonName }) {
const { register, formState: { errors }, handleSubmit } = useForm();
const [link, setLink] = useState('');
function handleInput(e) {
setLink(e.target.value)
}
function handleSubmitButton() {
console.log(link)
onUpdateAvatar({
avatar: link
});
}
return (
<PopupWithForm
name="change-avatar"
title="Update avatar"
submitName={ submitButtonName }
isOpen={ isOpen }
onClose={ onClose }
onSubmit={ handleSubmit(() => handleSubmitButton() ) }
>
<label htmlFor="userAvatar" className="form__form-field">
<input
id="userAvatar"
type="url"
{ ...register('userAvatar', {
required: "Enter URL link",
value: link
})
}
placeholder="url link"
className="form__input"
onChange={ handleInput }
/>
{ errors.userAvatar && (<span className="form__error">{ errors.userAvatar.message }</span>) }
</label>
</PopupWithForm>
);
}
It looks like type="url" doesn't work, but I can't figure out why
Here is a minimal working example: https://codesandbox.io/s/react-hook-form-js-forked-ml3zx?file=/src/App.js
I recommend to not overwrite onChange props, instead of using const [link, setLink] = useState(''); you can use const link = watch('userAvatar').
You can remove the value property, it's not necessary:
<input
id="userAvatar"
type="url"
{ ...register('userAvatar', {
required: "Enter URL link",
value: link //⬅️ Remove that line
})
}
placeholder="url link"
className="form__input"
onChange={ handleInput } //⬅️ Remove that line
/>
React Hook Form - Watch API
I'm new in ReactJS. I have a task - to do an app like Notes. User can add sublist to his notes, and note have to save to the state in subarray. I need to save sublist in the array inside object. I need to get state like this:
[...notes, { _id: noteId, text: noteText, notes: [{_id: subNoteId, text: subNoteText, notes[]}] }].
How can I to do this?
Sandbox here: https://codesandbox.io/s/relaxed-lamarr-u5hug?file=/src/App.js
Thank you for any help, and sorry for my English
const NoteForm = ({ saveNote, placeholder }) => {
const [value, setValue] = useState("");
const submitHandler = (event) => {
event.preventDefault();
saveNote(value);
setValue("");
};
return (
<form onSubmit={submitHandler}>
<input
type="text"
onChange={(event) => setValue(event.target.value)}
value={value}
placeholder={placeholder}
/>
<button type="submit">Add</button>
</form>
);
};
const NoteList = ({ notes, saveNote }) => {
const renderSubNotes = (noteArr) => {
const list = noteArr.map((note) => {
let subNote;
if (note.notes && note.notes.length > 0) {
subNote = renderSubNotes(note.notes);
}
return (
<div key={note._id}>
<li>{note.text}</li>
<NoteForm placeholder="Enter your sub note" saveNote={saveNote} />
{subNote}
</div>
);
});
return <ul>{list}</ul>;
};
return renderSubNotes(notes);
};
export default function App() {
const [notes, setNotes] = useState([]);
const saveHandler = (text) => {
const trimmedText = text.trim();
const noteId =
Math.floor(Math.random() * 1000) + trimmedText.replace(/\s/g, "");
if (trimmedText.length > 0) {
setNotes([...notes, { _id: noteId, text: trimmedText, notes: [] }]);
}
};
return (
<div>
<h1>Notes</h1>
<NoteList notes={notes} saveNote={saveHandler} />
<NoteForm
saveNote={saveHandler}
placeholder="Enter your note"
/>
</div>
);
}
The code in your saveHandler function is where you're saving your array of notes.
Specifically, this line:
setNotes([...notes, { _id: noteId, text: trimmedText, notes: [] }]);
But at the moment you're saving an empty array. What if you create another stateful variable called currentNote or something like that, relative to whatever note the user is currently working on within the application? While they are working on that note, the stateful currentNote object is updated with the relevant data, e.g. noteID, content, and parentID. Then, when the user has finished editing that particular note, by either pressing save or the plus button to add a new subnote, etc, that should fire a function such as your saveHandler to add the currentNote object to the "notes" array in the stateful "notes" variable. I'm not sure I like that the stateful variable notes contains an array within it called notes as well. I think this may cause confusion.
But in short, your setNotes line could change to something like (bear with me my JS syntax skills suck):
let newNotes= [...notes.notes];
newNotes.push(currentNote);
setNotes([...notes, { _id: noteId, text: trimmedText, notes: newNotes }]);
Wherein your stateful currentNote object is copied into the notes array on every save.
Introduction
Yesterday I followed an advanced tutorial from Kent C. Dodds where he explained how to connect an input to localstorage which then handles the setting of value, change of values etc and automatically sync with LocalStorage in react.
At the first place this works pretty well for normal components. However, for example the custom checkboxes which I have in my app do not work with the logics. I tried to alter the logics a bit but it seems that I didn't got far with it.
The Problem
Currently my custom checkbox component does not connect / work with the hoc LocalStorageFormControl.
Project info
I have made a CodeSandbox for you to play around with: https://codesandbox.io/s/eager-curie-8sj1x
The project is using standard bootstrap with scss stylings. The CustomCheckbox consists of two elements: the main div and the actual input itself. Currently the matching value in state will trigger className change in one of the elements to allow custom styling.
For any further questions please comment below. Thanks in advance for all the help.
Resources
Kent C. Dodds - Tutorial resource
CodeSandBox Project
The problems were:
The LocalStorageFormControl component didn't update the state when
it gets the initial value from localStorage.
The input didn't update the state onChange as it didn't have onChange
handler.
The CustomCheckboxGroup component didn't have a name prop which is used
as a part of the key in the localStorage
The solution is as following:
App.js
import React, { useEffect, useState } from "react";
// Bootstrap
import { Row, Col, Form } from "react-bootstrap";
import CustomCheckboxGroup from "./CustomCheckboxGroup";
// Function that calls all functions in order to allow the user to provide their own onChange, value etc
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args));
// Connect any <input /> to LocalStorage and let it manage value / onChange
function LocalStorageFormControl({
children,
formControl = React.Children.only(children),
lsKey = `lsfc:${formControl.props.name}`,
updateInitialState
}) {
const [hasChanged, setHasChanged] = useState(false);
const [value, setValue] = useState(() => {
return (
window.localStorage.getItem(lsKey) || formControl.props.defaultValue || ""
);
});
// Let the user control the value if needed
if (
formControl.props.value !== undefined &&
formControl.props.value !== value
) {
setValue(formControl.props.value);
}
useEffect(() => {
if (hasChanged) {
if (value) {
window.localStorage.setItem(lsKey, value);
} else {
window.localStorage.removeItem(lsKey);
}
} else {
if (value) {
// if hasChanged is false and there is value that means there was a value in localStorage
setHasChanged(true);
// update the state
updateInitialState(value);
}
}
}, [value, lsKey, hasChanged, updateInitialState]);
return React.cloneElement(React.Children.only(children), {
onChange: callAll(formControl.props.onChange, e => {
setHasChanged(true);
setValue(e.target.value);
}),
value,
defaultValue: undefined
});
}
const checkboxes = [
{
label: "Dhr",
name: "aanhef-dhr",
stateName: "salutation",
value: "De heer"
},
{
label: "Mevr",
name: "aanhef-mevr",
stateName: "salutation",
value: "Mevrouw"
}
];
export default function App() {
const [state, setState] = useState({});
function handleSubmit(e) {
e.preventDefault();
console.log("Handling submission of the form");
}
function onChange(e, stateName) {
e.persist();
setState(prevState => ({ ...prevState, [stateName]: e.target.value }));
}
// Log the state to the console
console.log(state);
return (
<Row>
<Col xs={12}>
<Form
id="appointment-form"
onSubmit={handleSubmit}
noValidate
style={{ marginBottom: 75 }}
>
<LocalStorageFormControl
updateInitialState={value => {
setState({ ...state, "test-textfield": value });
}}
>
{/* Add onChange handler to update the state with input value*/}
<input
type="text"
name="test-textfield"
onChange={e => {
setState({ ...state, "test-textfield": e.target.value });
}}
/>
</LocalStorageFormControl>
<LocalStorageFormControl
updateInitialState={value => {
setState({ ...state, salutation: value });
}}
>
<CustomCheckboxGroup
checkboxes={checkboxes}
key="salutation"
label="Salutation"
name="salutation"
onChange={(e, stateName) => onChange(e, stateName)}
required={true}
value={state.salutation}
/>
</LocalStorageFormControl>
</Form>
</Col>
</Row>
);
}
CustomCheckboxGroup.js
import React from "react";
// Bootstrap
import { Form, Row, Col } from "react-bootstrap";
export default ({ onChange, value, name, label, className, checkboxes }) => (
<Row>
<Col xs={12}>
<Form.Label>{label}</Form.Label>
</Col>
<Col>
<Form.Group className="d-flex flex-direction-column">
{checkboxes.map((checkbox, key) => {
return (
<div
key={key}
className={
checkbox.value === value
? "appointment_checkbox active mr-2 custom-control custom-checkbox"
: "appointment_checkbox mr-2 custom-control custom-checkbox"
}
>
<input
name={name}
type="checkbox"
value={checkbox.value}
onChange={e => onChange(e, checkbox.stateName)}
checked={value === checkbox.value}
id={"checkbox-" + checkbox.name}
className="custom-control-input"
/>
<label
className="custom-control-label"
htmlFor={"checkbox-" + checkbox.name}
>
{checkbox.label}
</label>
</div>
);
})}
</Form.Group>
</Col>
</Row>
);
I have some advice about your code:
Use radio buttons instead of checkboxes if you allow the user to choose one option only. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio
You can persist the whole state object if you would like to by replacing this:
const [state, setState] = useState({});
with this:
// Get the saved state in local storage if it exists or use an empty object
// You must use JSON.parse to convert the string back to a javascript object
const initialState = localStorage.getItem("form-state")
? JSON.parse(localStorage.getItem("form-state"))
: {};
// Initialize the state with initialState
const [state, setState] = useState(initialState);
// Whenever the state changes save it to local storage
// Notice that local storage accepts only strings so you have to use JSON.stringify
useEffect(() => {
localStorage.setItem("form-state", JSON.stringify(state));
}, [state]);
In a simple scenario like so
function onSubmit() { e.preventDefault(); /* Some Submit Logic */ }
<form data-testid="form" onSubmit={(e) => onSubmit(e)}>
<button type="submit" data-testid="submit-button">Submit</button>
</form>
How do I make sure that the form gets submitted when the submit button is clicked?
const { queryByTestId } = render(<LoginForm/>);
const LoginForm = queryByTestId("form")
const SubmitButton = queryByTestId("submit-button")
fireEvent.click(SubmitButton)
???
How do I test if onSubmit() has been called or maybe form has been submitted?
Basically, here is what I "solved" it:
// LoginForm.js
export function LoginForm({ handleSubmit }) {
const [name, setName] = useState('');
function handleChange(e) {
setName(e.target.value)
}
return (
<form data-testid="form" onSubmit={() => handleSubmit({ name })}>
<input required data-testid="input" type="text" value={name} onChange={(e) => handleChange(e)}/>
<button type="submit" data-testid="submit-button">Submit</button>
</form>
)
}
export default function LoginPage() {
function handleSubmit(e) {
// submit stuff
}
return <LoginForm handleSubmit={(e) => handleSubmit(e)}/>
}
Now the test's file:
// LoginForm.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import LoginPage, { LoginForm } from "./LoginPage";
it("Form can be submited & input field is modifiable", () => {
const mockSubmit = jest.fn();
const { debug, queryByTestId } = render(<LoginForm handleSubmit={mockSubmit}/>);
fireEvent.change(queryByTestId("input"), { target: { value: 'Joe Doe' } }); // invoke handleChange
fireEvent.submit(queryByTestId("form"));
expect(mockSubmit).toHaveBeenCalled(); // Test if handleSubmit has been called
expect(mockSubmit.mock.calls).toEqual([[{name: 'Joe Doe'}]]); // Test if handleChange works
});
getByRole('form', { name: /formname/i })
RTL is meant to move away from using id's. An ideal solution is to name your form which does two things. It allow you to uniquely name it making it useful to screen readers and also gets the browser to assign it a role of form. Without the name, getByRole('form') will do nothing.
MDN: <form> element
Implicit ARIA role - form if the form has an accessible name,
otherwise no corresponding role
I'd suggest to use nock to intercept request sending from form and return mocked response after.
For example:
nock('https://foo.bar').post('/sign-up', formValues).reply(201);
But I would like to know a better solutions tho.