If your workshop did not do Lab 04 please go back and run the “Checkout the API repository” section of Lab 04 to start up the Rest API
cd ../ # presuming still in previous lab
cd lab-05
yarn start
Before proceeding, let’s look at the progress that has been made:
<Route exact path="/projects" component={Projects} />
<Route exact path="/projects/detail/:_id?" component={ProjectsDetail} />
<Route exact path="/employees" component={Employees} />
<Route exact path="/employees/detail/:_id?" component={EmployeeDetail} />
<Route exact path="/timesheets" component={Timesheets} />
<Route exact path="/timesheets/detail/:_id?" component={TimesheetsDetail} />
<Route
exact
path="/timesheets/detail/:timesheet_id/timeunits/detail/:_id?"
component={TimeunitsDetail}
/>
<Redirect to="/employees" />
Take the time to check out the path declarations and how they are adding route params that are dynamically replaced.
If we really wanted to, we could absolutely create our own user input/validation framework from scratch in React, but why reinvent the wheel when there’s awesome libraries out there that do the heavy lifting for us. In the real world, you’ll almost certainly use one of several popular User Input/Form libraries such as Redux Form, React Final Form, or Formik. For this example application, we’re using Formik.
Formik is a wrapper around Forms that takes care of some basic considerations like setup/reset/teardown, validation, and state management. It’s a very lightweight solution which means it’s easy to setup and very fast, but it doesn’t do as much for you as others like redux-form
which can lead to some boilerplate (but way less than you’d have doing it all yourself!)
Now let’s set up a way to edit an employee.
The first component we need is a form to contain all of our employee’s properties.
Open src/employees/EmployeeForm.js.
Let’s review the props
of this component:
We need to define the Form element and integrate it with Formik so we get all of the helpful validation and support mechanisms.
render() {
const { employee } = this.props;
return (
<Formik
initialValues={{}}
validate={ this.validate }
onSubmit={ this.handleSave }
enableReinitialize
>
{ ({ isValid, errors, touched, handleReset, handleSubmit }) => (
<Form>
</Form>
)}
</Formik>
);
}
This block defines the
Formik
Higher-Order-Component (HOC) - this is a wrapper around a childForm
which adds behaviors to make it more capable than it is by itself. This component takes initial values to populate the form with (or reset the form to if the user wants to reset) as well as hooks to call functions for validation and submission.enableReinitialize
is a hint for Formik to refresh when we give it new data. Within theFormik
component is something called a “render prop” - this is a more advanced pattern that is used by some third-party libraries to allow you to define content to be nested inside third-party components while still retaining the ability to apply custom logic, styles, etc like you could in your own React code.
Note the five render props being passed down - ‘isValid’, ‘errors’, ‘touched’, ‘handleReset’, and ‘handleSubmit’. These are all values and functions provided by Formik. The first two give us access to the validation state of the form (true or false) and what errors exist in the form, ‘touched’ tells us what fields the user has interacted with, and the last two are event handlers that can be called to submit or reset the form. Formik provides a ton more props for more advanced scenarios, but we don’t need them here.
Great! We have an empty form…probably need to add some content in there.
A helpful member of your team has created a reusable component for Forms that takes care of wrapping an input element with an appropriate label and validation logic. Let’s just reuse that! Hooray for reusable shared components!
<FieldWrapper type="text" name="username" label="Username" invalid={errors.username} touched={touched.username} />
<FieldWrapper type="text" name="email" label="Email" invalid={errors.email} touched={touched.email} />
<FieldWrapper type="text" name="firstName" label="First Name" invalid={errors.firstName} touched={touched.firstName} />
<FieldWrapper type="text" name="lastName" label="Last Name" invalid={errors.lastName} touched={touched.lastName} />
<FieldWrapper type="checkbox" name="admin" label="Admin" invalid={errors.admin} touched={touched.admin} />
Here we’re defining five fields - the FieldWrapper
component takes a few props:
type
is used to define what type of form field to render - text, checkbox, select menu, etcname
is a unique name within the form for the value of each fieldlabel
is the text to show next to the field so the user knows what it isinvalid
is a hint to the field whether Formik’s validation has any problems with the value in that field. It will be an error message if a validation issue was foundtouched
is a hint to the field as to whether the user has interacted with it yetLastly we need to give the ability for the user to Save or Reset the form.
At the very bottom of the Form
element, we can add another nice reusable Form component somebody on our team created - the FormControls
component. This contains a submit and reset button.
<FormControls
allowSubmit={isValid}
onSubmit={handleSubmit}
onReset={handleReset}
/>
Note how we’re using another Formik-supplied value here isValid
- this tells us whether the entire form has passed validation. Once it has, we’ll enable the save button.
There, our Form is complete. However, we need to finish implementing what should happen when the Form detects a submission.
We’ve already told the Formik
component to call handleSave
in this situation. Here’s what the handleSave
function should look like:
handleSave = (values) => {
this.props.handleSave(values);
};
Notice that we are not actually implementing the save function. That is left for the component that uses this form to implement and pass it in as a prop.
validate()
yet, so let’s do that next. Here’s what it should look like:validate = (values) => {
const errors = {};
if (!values.username) {
errors.username = 'Required';
}
if (!values.email) {
errors.email = 'Required';
}
return errors;
};
EmployeeForm
is being used to edit an existing employee?employee
in its props, what we need to do is update the form to reflect that employee’s values.Formik
component gives us the initialValues
prop to do this. Replace the empty declaration you currently have with the following:initialValues={ employee && {
username: employee.username || '',
email: employee.email || '',
firstName: employee.firstName || '',
lastName: employee.lastName || '',
admin: employee.admin || '',
_id: employee._id
} }
state = {
employee: null
};
axios
library to help us make HTTP requests. async componentDidMount() {
const { match } = this.props;
const { _id } = match.params;
const { data: employee } = await Axios.get(url(_id));
this.setState({ employee });
}
We use react-router to give us the parameters that match
-ed from the Route
- in this instance, we’ll get the employee ID from the URL. We use that to fetch the corresponding employee record.
Next, let’s update the render function to include the EmployeeForm
, and pass it all the props it needs:
<div>
<h1>Employee Detail</h1>
<EmployeeForm
employee={employee}
handleSave={this.handleSave}
/>
</div>
EmployeeForm
a reference to this.handleSave
, which we still need to define:handleSave = (values) => {
const { history } = this.props;
const result = values._id ? this.onUpdate(values) : this.onCreate(values);
result.then(() => {
history.push('/employees');
});
};
When handleSave is called we’ll:
Finally, we’ll implement the onUpdate
and onCreate
functions, to persist the employee with our PUT and POST APIs.
onUpdate = async employee => {
const response = await Axios.put(url(employee._id), employee);
return response.data;
};
onCreate = async employee => {
const response = await Axios.post(url(employee._id), employee);
return response.data;
};
jest.mock('axios', () => ({
get: jest.fn(),
put: jest.fn(),
post: jest.fn()
}));
describe('<EmployeeDetail />', () => {
it('should instantiate the Employee Detail Component', () => {
const component = mount(<EmployeeDetail />);
component.setState({ employee: { _id: 1 } });
expect(component).toIncludeText('Employee Detail');
});
});
We have an Employee Detail route, but there’s no way to get to it yet.
We’re going to add functionality so that when you click an EmployeeRow, the router will transition to the appropriate detail route for the employee.
Open /src/employees/EmployeeRow.js
Add the showDetail()
method to the EmployeeRow
showDetail = () => {
const { history, employee } = this.props;
if (employee.deleted) {
console.log('You cannot edit a deleted employee.');
return;
}
history.push(`/employees/detail/${employee._id}`);
};
This first checks to see if the employee has been deleted and prevents viewing it if so
Then it uses the history prop provided by react-router
to change the current URL programmatically (and thus change the matched route)
Now add an onClick()
handler to the <tr/>
in the render()
method.
<tr className={employee.deleted ? 'deleted' : ''} onClick={this.showDetail}>
EmployeeTable
:<Link to="/employees/detail">
<Button bsStyle="primary">
New Employee
</Button>
</Link>
What’s going on here? We’re using a react-router Link
element which, when clicked, will send the user to the Route that causes the EmployeeDetail component to render. Inside the Link we specify a component to render that is clickable.
That’s it! We already made our EmployeeDetail component smart enough to know that, if it wasn’t given an Employee object with an ID set, that we were creating a new one. Cool!
The ProjectDetail component has been implemented already. Can you get it hooked up to allow Project create/update? How about adding Delete/Restore to the ProjectTable?
Are you a true champion? Figure out how to add validation to prevent the user from creating an Employee with the same name or username as an existing Employee.
Hint: You might be tempted to drag data down to use it in child components, but it’s sometimes easier to leave data up high and pass down worker functions from a parent to a child component.
git add .
git commit -m "We are validating forms"