官术网_书友最值得收藏!

Adding events to a functional component

We're about to add state to our component. The component will show a button for each appointment. When the button is clicked, the component stores the array index of the appointment that it refers to. To do that, we'll use the useState hook.

Hooks are a feature of React that manage various non-rendering related operations. The useState hook stores data across multiple renders of your function. The call to useState returns you both the current value in storage and a setter function that allows it to be set.

If you're new to hooks, check out the Further learning section at the end of this chapter. Alternatively, you could just follow along and see how much you can pick up just by reading the tests!

Let's start by asserting that each li element has a button element:

  1. Add the following test, just below the last one you added. The second expectation is a little peculiar in that it is checking the type of the button to be button. If you haven't seen this before, it's idiomatic when using button elements to define its role by setting the type attribute, as I'm doing here:
it('has a button element in each li', () => {
render(<AppointmentsDayView appointments={appointments} />);
expect(
container.querySelectorAll('li > button')
).toHaveLength(2);
expect(
container.querySelectorAll('li > button')[0].type
).toEqual('button');
});
We don't need to be pedantic about checking the content or placement of the button element within its parent. For example, this test would pass if we put an empty button child at the end of  li . But, thankfully, doing the right thing is just as simple as doing the wrong thing, so we can opt to do the right thing instead. All we need to do to make this pass is wrap the existing content in the new tag.
  1. Make this test pass by modifying the AppointmentsDayView return value, as shown:
<ol>
{appointments.map(appointment => (
<li key={appointment.startsAt}>
<button type="button">
{appointmentTimeOfDay(appointment.startsAt)}
</button>
</li>))}
</ol>;
  1. We can now test what happens when the button is clicked. Back in test/Appointment.test.js, add the following as the next test. This uses the ReactTestUtils.Simulate.click function to perform the click action:
it('renders another appointment when selected', () => {
render(<AppointmentsDayView appointments={appointments} />);
const button = container.querySelectorAll('button')[1];
ReactTestUtils.Simulate.click(button);
expect(container.textContent).toMatch('Jordan');
});
React components respond to what it calls synthetic events. React uses these to mask browser discrepancies in the DOM event model. That means we can't raise standard events that we'd fire through JSDOM. Instead, we use the ReactTestUtils.Simulate object to raise events.
  1. Include the following import at the top of test/Appointment.test.js:
import ReactTestUtils from 'react-dom/test-utils';
  1. Go ahead and run the test:
  ● AppointmentsDayView ? renders appointment when selected

expect(received).toMatch(expected)

Expected value to match:
"Jordan"
Received:
"12:0013:00Ashley"

We're getting all of the list content dumped out too, because we've used container.textContent in our expectation rather than something more specific.

At this stage, I'm not too bothered about where the customer name appears on screen. Testing container.textContent is like saying I want this text to appear somewhere, but I don't care where. Later on, we'll see techniques for expecting text in specific places.

There's a lot we now need to get in place in order to make the test pass: we need to introduce state and we need to add the handler. But, first, we'll need to modify our definition to use a block with a return statement:

  1. Set the last test to skip, using it.skip.
We never refactor on red. It's against the rules! But if you're on red, you can cheat a little by rewinding to green by skipping the test that you've just written.

It may seem a little pedantic to do that for the very tiny change we're about to make, but it's good practice.
  1. Wrap the constant definition in curly braces, and then return the existing value. Once you've made this change, run your tests and check you're all green:
export const AppointmentsDayView = ({ appointments }) => {
return (
<div id="appointmentsDayView">
<ol>
{appointments.map(appointment => (
<li key={appointment.startsAt}>
<button type="button">
{appointmentTimeOfDay(appointment)}
</button>
</li>))}
</ol>
<Appointment customer={appointments[0].customer} />
</div>
);
};
  1. Unskip the latest test by changing it.skip to it, and let's get to work on making it pass.
  2. Update the import at the top of the file to pull in the useState function:
import React, { useState } from 'react';
  1. Add the following line above the return statement:
const [selectedAppointment, setSelectedAppointment] = useState(
0
);
  1. We can now use this selectedAppointment rather than hard-coding an index selecting the right appointment. Change the return value to use this new state value when selecting an appointment:
<div id="appointmentsDayView">
...
<Appointment {...appointments[selectedAppointment]} />
</div>
  1. Then, change the map call to include an index in its arguments. Let's just name that i:
{appointments.map((appointment, i) => (
<li key={appointment.startsAt}>
<button type="button">
{appointmentTimeOfDay(appointment.startsAt)}
</button>
</li>
))}
  1. Now call setSelectedAppointment from within the onClick handler on the button element:
<button
type="button"
onClick={() => setSelectedAppointment(i)}>
  1. Run your tests, and you should find they're all green:
PASS test/Appointment.test.js
Appointment
? renders the customer first name (18ms)
? renders another customer first name (2ms)
AppointmentsDayView
? renders a div with the right id (7ms)
? renders multiple appointments in an ol element (16ms)
? renders each appointment in an li (4ms)
? initially shows a message saying there are no appointments today (6ms)
? selects the first element by default (2ms)
? has a button element in each li (2ms)
? renders another appointment when selected (3ms)

Our component is now complete and ready to be used in the rest of our application. That is, once we've built the rest of the application!

主站蜘蛛池模板: 雷波县| 桐城市| 隆林| 贵溪市| 观塘区| 甘南县| 丁青县| 泰来县| 临湘市| 永清县| 怀化市| 新源县| 东台市| 荥经县| 沾益县| 博兴县| 中方县| 安化县| 喜德县| 安化县| 富平县| 三亚市| 灵武市| 灵武市| 安化县| 太康县| 大埔县| 紫阳县| 昭苏县| 尼玛县| 竹山县| 宁晋县| 顺平县| 晋州市| 兴安县| 兰坪| 平昌县| 鄂托克前旗| 乌拉特前旗| 凤庆县| 青阳县|