ReactAddressShow
Overview
STATUS: This assignment needs work, but should be complete enough to allow students to finish the assignment. [Friday May 4, 6:14 PM].
This assignment builds on the AddressComponent assignment. We have several steps we want to complete. Among them are:
- Divide the Address Component up into:
- A component that owns the data: Address
- A component that displays the data: AddressShow
- Finishing refactoring the other components if we have not done so already
- Update Tests as described in this document. There will be a separate assignment where we will dig more deeply into how to test the code found in this assignment.
We should insure that clicks on the Button work but that Address does the real work. AddressShow is mostly about the interface and does little real work other than use JSX to create the appropriate HTML to display our interface.
Modules
In this exercise we break our code up into modules. The word module is a somewhat flexible term. JavaScript has specific ways to create single files that can be shared, that can be loosely linked, from other modules. They can also ensure that variables are kept out of the global scope:
- ES5:
- require
- requirejs
- ES6
- import
But sometimes people use the word in a broader sense, saying that a micro-service is a module or even that a Docker container is a module. But they are talking in a broad, very loose sense, while the term has a technical meaning in JavaScript:
As stated above, the purpose of a JavaScript module is to support loose coupling and to keep variables properly scoped, and particularly to keep you from accidentally putting them in global scope, as explained here:
The main effect of the by-value copy is that global variables of simple types won’t be shared across scopes. Always put variables in a wrapper class and export the wrapper (such as bar in the above example).
NOTE: Ruby allows inheriting modules, but in general, inheritance is about classes and objects, and not about modules. Read this description of C# modules. There is no mention of inheritance.:
Nor is there one in the JavaScript description from Mozilla:
State and Props
It is important to understand the difference between State and Props.
Divide
The first step is to split Address and AddressShow into two distinct components. In particular, we will create a new component called AddressShow and put the render method of Address in it:
- AddressShow contains only the render method from the original Address component
- Address contains the constructor, the setAddress method, and a one line render method that references
and passes it props. This render method will be explained later in this assignment.
The Address component will handle our data, AddressShow will display the data and the button.
NOTE: The button the user clicks on to display the address should be in AddressShow.
Create AddressShow Project
To get started, make a copy of Address Components assignment and call it address-show. For instance:
cp -r week03-react-address/ week05-address-show
Or, if you prefer use Pascal case for the new project:
cp -r week03-react-address/ Week05-AddressShow
There are many ways to do the same thing, and it doesn’t really matter which one you prefer.
Testing Address Show
We should write our tests first, then write code that makes the tests pass. This is called TDD or Test Driven Development.
The first step will be set up our (initially) failing tests for AddressShow in AddressShow.test.js:
// IT'S UP TO YOU TO INCLUDE THE RIGHT IMPORTS
describe('AddressShow Shallow Suite', function() {
const debug = false;
const addressTest = {
firstName: 'Patty',
lastName: 'Murray',
address: '154 Russell Senate Office Building',
city: 'Washington',
state: 'D.C.',
zip: '20510',
phone: '(202) 224-2621',
fax: '(202) 224-0238',
tollfree: '(866) 481-9186'
};
/*
* Display debug information about a DOM node of a component by index
* @param {object} wrapper - The HTML generated by your component
* @param {number} index - Index of HTML element you want to see.
* @param {boolean} talkToMe - Speak even if quiet is true
*/
const getIndex = (wrapper, index, talkToMe) => {
if (debug || talkToMe) {
const ninep = wrapper.find('div#addressShow').childAt(index).debug();
console.log('NINEP:', ninep);
}
};
const defaultFieldTest = (name, index, talkToMe) => {
const wrapper = shallow(<AddressShow address={AddressList[0]}/>);
const welcome = <p className="App-intro">{name}</p>;
getIndex(wrapper, index, talkToMe);
expect(wrapper.contains(welcome)).toEqual(true);
};
const afterClickFieldTest = (name, index, talkToMe) => {
const wrapper = shallow(<AddressShow address={AddressList[1]}/>);
const welcome = <p className="App-intro">{name}</p>;
getIndex(wrapper, index, talkToMe);
expect(wrapper.contains(welcome)).toEqual(true);
};
it('renders and displays the first name', () => {
defaultFieldTest('First Name: unknown', 0);
afterClickFieldTest('First Name: ' + addressTest.firstName, 0);
});
// EVENTUALLY YOU WILL NEED TO WRITE MORE TESTS LIKE THE FIRST NAME TEST.
// THE GOAL WILL BE TO TEST ALL THE PROPERTIES OF OUR COMPONENT.
// AT FIRST, HOWEVER, JUST KEEP THESE TWO TESTS. WHEN THEY START
// PASSING, THEN ADD TESTS FOR THE OTHER PROPERTIES SUCH AS LASTNAME...
});
Our goal now will be to get these tests to pass.
Testing Address
The address component with Enzyme shallow does not provide us much in the way of JSX to test. That means we will have to rewrite Address.test.js to work with the new structure we have created.
Here is the JSX we will create for our Address component:
<div>
<AddressShow address={this.state.address} setAddress={this.setAddress} />
</div>
We can show the JSX for our Address component with this line of enzyme debug code:
const wrapper = shallow(<Address/>);
wrapper.debug()
Here is the enzyme debug output from that command:
<div>
<AddressShow address={{...}} setAddress={[Function]} />
</div>
As mentioned earlier, it appears there is not much here to debug. But we can get at the properties of this.state.address with this line of Enzyme debug code:
wrapper.find('AddressShow').prop('address')
This will return the contents of our AddressList in JavaScript code.
Armed with that knowledge, we can start writing tests that look like this:
describe('Address tests', , function() {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<Address/>, div);
ReactDOM.unmountComponentAtNode(div);
});
it('renders and displays the default first name', () => {
const wrapper = shallow(<Address/>);
//console.log(wrapper.find('AddressShow').prop('address'));
expect(wrapper.find('AddressShow').prop('address').firstName).toEqual('unknown');
});
it('renders state of firstName after button click', () => {
const wrapper = shallow(<Address addressList={AddressList}/>);
wrapper.instance().setAddress();
setImmediate(() => {
wrapper.update();
expect(wrapper.find('AddressShow').prop('address').firstName).toEqual('Patty');
});
});
// YOU WRITE TESTS FOR THE OTHER PROPERTIES SUCH AS LASTNAME, CITY, ETC...
});
Our goal will be to get these tests to pass.
NOTE: I don’t provide much debug information here. You can however, uncomment the console.log statement in the default first name test shown above if you need some help crafting your tests. More specifically, here is the line to uncomment:
console.log(wrapper.find('AddressShow').prop('address'));
Like this:
it('renders and displays the default first name', () => {
const wrapper = shallow(<Address/>);
console.log(wrapper.find('AddressShow').prop('address'));
expect(wrapper.find('AddressShow').prop('address').firstName).toEqual('unknown');
});
Rename the React Component
The React component you created may be in a file called App.js or perhaps you have renamed it to Addresss.js. In either case, rename both the file and the class inside it to AddressShow.
This is a task the WebStorm can help you complete.
- Right click on the file in the Project pane and choose to copy it.
- Right click on the components directory and choose to paste it.
- Rename the new file to AddressShow.js
In AddressShow, rename the class from Address to AddressShow:
class Address extends Component { ... } <=== ORIGINAL
class AddressShow extends Component { ... } <=== EDITED VERSION
Then, down at the bottom of the file, make the same change:
export default Address; <=== ORIGINAL
export default AddressShow; <=== EDITED VERSION
Properly Refactor Our Components
If you have not done so already, you should now further refactor your components as follows:
Component | Task |
---|---|
Header | Display the Header |
GetFile | The simple React Component with a constructor that defines state.file, a method called getFile, and a render method. This component was first shown in week one or two. |
Address | constructor, setAddress and simple render method to be explained later. |
AddressShow | Only a relatively lengthy render method to display all the props such as firstName, lastName, etc. |
App | Only a relatively short render method to instantiate the Header, GetFile, Address, AddressShow |
index.js | Instantiates App |
Here is the way it looks in WebStorm:
Here is our new index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(
<div>
<App/>
</div>,
document.getElementById('root'));
registerServiceWorker();
Here is our new components/App.js:
import React, {Component} from 'react';
import '../App.css';
import Address from './Address'
import Header from './Header';
import GetFile from './GetFile';
class App extends Component {
render() {
return (
<div className="App">
<Header/>
<GetFile/>
<Address/>
</div>
);
}
}
export default App;
We don’t see AddressShow in App because it will be created by the Address component.
Data
We will no longer load AddressList in index.js. Instead, Address owns the data. Move the import statement from index.js to Address.js and straighten out the path:
import AddressList from '../address-list';
NOTE: Recall that our goal is to perform a complete mind-meld with the file system of our OS. Some part of your brain must become the file system, and you should take great joy and comfort from this fact. In particular, it should be intuitively obvious that the relative path part of the import statement must change after we move this line of code from a file in the src directory to a file in the src/components directory. You should also grok immediately the difference between a relative path that contains one dot and one that contains two dots. You shouldn’t have to think about it any more than you have to think how to tie your shoes. The knowledge should just be there, fully formed, in your brain without thought or effort. I should perhaps remind you that this isn’t a Linux thing, as you would have the same issue on Windows.
Lets also stop working with the individual fields in Address. Instead, we will work with a single record from the AddressList array.
import AddressList from '../address-list';
class Address extends Component {
constructor(props) {
super(props);
this.addressIndex=0;
this.state = {
address: AddressList[this.addressIndex] <=== HERE
};
this.debug = true;
}
}
We create an addressIndex property and use it to index into our array of AddressList. Our state tracks only a single address since that is all that AddressShow needs to know.
NOTE: I’m having some doubts here as to whether this is the best way to do this. Certainly it works, but there may be a more elegant solution. I’ll think about it.
We now radically strip down the render method for Address. Rather than render the address fields here, we will pass this.state.address to AddressShow and let that component render it:
render() {
if (this.debug) { console.log("ADDRESS RENDER"); }
return (
<div className="App">
<AddressShow address={this.state.address} />
</div>
);
}
AddressShow Receives the Address Data
The only thing that AddressShow really needs to do is display our Address record. It turns that at this time, AddressShow does not need a constructor, since there is no set up work need to get the object going. However, if it did need one, it might look like this:
constructor(props) {
super(props);
this.debug = true;
if (this.debug) {
console.log('SHOW ADDRESS CONSTRUCTOR', this.props.address);
}
}
As you can see, the constructor is passed props. As you know, props is the state passed to the object by its parent, which in this case is Address. This means that this.props.address in AddressShow is “the same thing” as this.state.address in the Address component. The data is passed from the Address render method to the AddressShow constructor.
You can keep the above constructor, but you should strip everything else from AddressShow except the render method. In the render method’s JSX, reference the fields of the address object with props rather than state.
NOTE: Since the constructor for AddressShow doesn’t do anything, the only thing we really need in AddressShow is the render method, but we are keeping the constructor for pedagogical purposes. Or not, it is up to you. It might be simpler just to open the Developer tools at runtime and put a breakpoint on the AddressShow render method to confirm that your props are being passed as expected.
Logging
We don’t really need a constructor in ShowAddress, and yet we have implemented it anyway to help illustrate a point. We can use it to give us a view of the props passed in from Address.
As you probably know, console.log is both curse and blessing. Let’s try to emphasize the blessing and mitigate the curse by creating a single place where we call the offending method:
constructor(props) {
super(props);
this.debug = true;
this.log('ADDRESS SHOW CONSTRUCTOR', this.props.address);
}
log(message, message2 = '', message3 = '') {
if (this.debug) {
console.log(message, message2, message3);
}
}
Our logger is a bit deficient at this point as it will often fail to display objects correctly. One solution is to transform objects into strings before we send them:
const ObjectAsString = JSON.stringify(this.props.address, null, 4));
this.log('ADDRESS SHOW CONSTRUCTOR', objectAsString);
A second solution is to check the second parameter to see if it is an object and to transform it there:
log(message, message2 = '', message3 = '') {
if (typeof message2 === 'object') {
message2 = JSON.stringify(message2, null, 4);
}
if (this.debug) {
console.log(message, message2, message3);
}
}
We should probably do this for all three parameters.
Now we can toggle a single variable, this.debug, whenever we want to turn down the volume. For instance, we might want to log to the console in our render method. In the new system we would do it like this:
render() {
this.log("SHOW ADDRESS RENDER", this.props);
return ( ... );
}
Now we can change the this.debug property from true to false to toggle the use of console.log throughout the object. There are other solutions, and better loggers, but this is a bit of a start on understanding the subject.
NOTE: I state the name of the object in logging message to help me track down where it is coming from. Fancy loggers can give us more information in simpler ways, but we could do this:
constructor(props) {
super(props);
this.debug = false;
this.log('CONSTRUCTOR', this.props.address);
}
log(message, message2 = '', message3 = '') {
if (this.debug) {
const label = this.constructor.name + ': '; < === HERE
console.log(label, message, message2, message3);
}
}
render() {
this.log("RENDER", this.props);
}
We are using this.constructor.name to get the name of our component.
Logging in Address.js
Suppose we used the logger in Address.js:
The output, when the component is first loaded, would look like this:
Rendering the Data
It should come as no surprise that AddressShow can, at least for now, render an address with almost the same code that we used in Address. The change will be simply to work with props rather than state:
render() {
this.log("SHOW ADDRESS RENDER");
return (
<div className="App">
<p className="App-intro">
firstName: {this.props.address.firstName}
</p>
// CODE OMITTED HERE
</div>
);
}
Note that we don’t write this.props.firstName but this.props.address.firstName. This is because we pass in the whole address object, rather than a set of individual properties representing each field. One could argue the merits of each technique, but I like this one because it is relatively simple.
AddressShow Modify View with Button Click
The next step will be to set up a button click for AddressShow. Recall that we deleted all the code except the render method a nominal constructor from AddressShow. Thus our button click no longer works, even if the button itself is still there.
We said earlier that AddressShow would display our data, and that Address would manipulate, modify and save our data. So our code for handling the button click belongs in Address. It is essentially the same code we had before, but now it is somewhat simpler since we are working on with the piece of data in our state:
setAddress = (event) => {
this.addressIndex = 1;
this.setState({
address: addressList[this.addressIndex]
})
};
So now we have the method for changing our state. But that method is in Address and in our button is in AddressShow. How do we connect them?
The solution is to pass the setAddress function object to AddressShow in the render method of Address:
<AddressShow
address={this.state.address}
setAddress={this.setAddress}
/>
As you can see, we are now passing not one, but two props to AddressShow.
And here is all we need to do in AddressShow. At the bottom of the render method, modify the button to look like this:
<button id="showAddressClick" onClick={this.props.setAddress}>Show Address</button>
Note that I am both setting the onclick method to the function object passed from the Address object, and also being sure to create a unique id which is a portmanteau derived from the object’s name and the button’s purpose.
PropTypes
Near the top of AddressShow link in PropTypes:
import PropTypes from 'prop-types';
Then define your proptypes. I don’t list them all here, but I include enough to get your started.
AddressShow.propTypes = {
address: PropTypes.shape({
firstName: PropTypes.string,
lastName: PropTypes.string,
// YOU WRITE THE REST
}),
setAddress: PropTypes.func
};
If you don’t have this right, then ESLint will not work correctly.
Turn it in
Add, commit then push. Then Tag. Push you tag. When you turn in the assignment, give me the tag and the directory in which you did your work. I may just look at your most recent code, but being able to go back to where you were when you submitted the assignment can be helpful.
Your tests should look something like this:
PASS src/tests/AddressShow.test.js
PASS src/tests/Address.test.js
PASS src/tests/Header.test.js
Test Suites: 3 passed, 3 total
Tests: 30 passed, 30 total
Snapshots: 0 total
Time: 0.545s, estimated 1s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.
Hints about Tagging
Hints about Breakpoints
The debugger and breakpoints are essential tools. I personally can do little without them.
- Start your program.
- Go to Chrome or Chormium
- F12 to start the developer tools.
- Click the Sources page
- Click webpack in the “project view” on the left
- Select a file
- Click in the gutter to set a breakpoint.
- In this case I choose App.js and put the breakpoint in the constructor on line 7
- You could pick any file and put the break point anywhere there is an active line.
- Press F5 to rerun to your breakpoint.