ReactAddressMenu
React Address Menu
The goals of this assignment are:
- Create a menu, no matter how limited
- Switch views between our Go and First components.
A silent video showing what we want to do:
Use weekxx-address-simple as the basis for this project.
Tag
Before working on the assignment, do this:
$ git tag -a v6.0.0 -m "Add menu to address simple"
git push origin v6.0.0
Once you are ready to turn in assignment, do this to mark the commit that contains your finished code. First add, commit and push your repo, then tag it:
git tag -a v6.0.X -m "Address Menu Assignment Complete"
Here we are using version 6 to say we did this work in the sixth week of the quarter. The X starts at zero, and increments each time you create a new tag for week 6. For instance:
v6.0.0
v6.0.1
v6.0.2
etc
One you have created the tag, push it to your GitHub repo with this code:
git push origin v6.0.X
Again, substitute the exaction version number that you want to push for the letter X. For instance:
git tag -a v6.0.1 -m "Address Menu Assignment Complete"
git push origin v6.0.1
To view your tags, type this:
git tag -n
To go back to the tag later do this:
git co -b tagcheck v6.0.0
Install
Install material-ui:
npm i @material-ui/core @material-ui/icons
Also, be sure you have prop types:
npm i prop-types
Strategy
We want to be able to display either the Go component, or the First component.
Our current architecture has us showing either the Go or the First component from the control file. We want to be able to display two components at once. In particular, we want to be able to do something like this:
render() {
return (
<div>
<First/>
<Go/>
</div>
);
}
Create App
Let’s begin by creating a new component inside weekXX-address-simple called App.
-
WebStorm: select **File New JavaScriptFile**. -
Code: **File New**
Save the file as /source/App.js.
Insert a very simple component in it:
Function Component:
import React from 'react';
function App() {
return (
<div>
<h1>Address Simple Home</h1>
</div>
);
}
export default App;
Class component:
import React, {Component} from 'react';
class App extends Component {
render() {
return (
<div>
<First/>
<Go/>
</div>
);
}
}
export default App;
For the above code to work, you will need to add two import statements. I’ll leave that up to you.
Take a moment to study the code found in App. Notice that after that the return statement we have an open parenthesis. In general, we surround our JSX with parenthesis rather than curly braces. Also note that we wrap our two components in a DIV. As mentioned earlier, this is because the render method is expected to return a single entity. Rather than returning Go and First, which would be two items, we wrap them both in a DIV and then return the DIV. Thus we are returning a single item, even if that single DIV we return wraps two components.
Now turn to control.js. Instead of having ReactDOM render Go, change the code so that it renders our new App component. Again, I’ll leave that up to you.
Viewing several components on one “page”:
IMAGE: We see First and Go on a single page.
Here we have the App component displaying several other React components. To get this display, I added an H2 element with the text First Component to the First component. To do this, you have to obey the rules about returning only a single item that were discussed earlier in this section of the text. I also change the H1 element in Go to an H2.
Define a Menu
The code shown in screenshot shown above is nice enough, but now, we want to show our First and Go components one at a time. We want to allow the user to switch between them.
NOTE: Most React applications solve this problem by creating a Single Page App, or SPA. There are, however, a number of complications and problems that arise when we use a SPA. As a result, I teach a technique for creating multipage apps in this course, and reserve SPAs for my Bachelor level courses. It is good to know both techniques as they each have advantages. An important advantage of multipage apps is that they can help you get better scores from Google and other SEO drivers. In other words, they can help you market your website.
Our solution for this problem, and there are many possible solutions, is to use the use a menu from the material-ui library and a trick that helps us launch one page at a time when the user makes a selection from the menu.
Our menu will allow us to switch between the Go and First views. We will also modify App so that it becomes a nascent Home page.
We will do this in a new file called components/ElfHeader.js and in components/control.js.
NOTE: I call it ElfHeader rather than Header in part because the word Header is such a common word that it is likely to collide with some other name in our program or in the global name space. In particular, header is an element in HTML 5.
Create the Menu
First, install some tools so we can use material-ui to display a menu. We also add file-loader to help us load a PNG file.
npm install @material-ui/icons @material-ui/core file-loader
Let’s write code in a new file called source/ElfHeader.js to display the simple menu. Create the file and then put the boilerplate code from the MaterialUiElfHeader gist in it. Around line 55, change the title to include our app name and your last name:
GitExplorer => Simple Address LastName
NOTE: When trying to select code from a gist, it is often best to press the RAW button to get an unadorned view of the code.
Create a file called source/tileData.js and put the contents of the MaterialUiTileDataListItem gist in it.
Modify tileData.js so that it displays three menu items:
- Home
- Go
- First
Set the paths as follows:
Component | Path |
---|---|
Home | /worker?title=App |
Go | /worker?title=Go |
First | /worker?title=First |
For instance:
<ListItemLink button component="a" href="/worker?title=First">
In this code we are setting up a situation where we can pass a query to the server. In this case, we are stating that the query contain a parameter called title that is set to the value First. We will use this data on the server, in the file called routes/index.js.
Display Pages
This code will allow us to switch between the Go and First components when they are selected from the menu.
import React from 'react';
import ReactDOM from 'react-dom';
import ElfHeader from './ElfHeader';
import Go from './Go';
import First from './First';
import App from './App';
const APPS = {
Go,
First,
App
};
function renderAppInElement(choice) {
var AppTool = APPS[choice.dataset.app];
if (!AppTool) return;
// get props from elements data attribute, like the post_id
const props = Object.assign({}, choice.dataset);
ReactDOM.render(<AppTool {...props} />, choice);
}
window.onload = function() {
ReactDOM.render(
<ElfHeader/>,
document.getElementById('root')
);
document
.querySelectorAll('.__react-root')
.forEach(renderAppInElement);
};
And our code in views/worker.pug:
extends layout
block content
#root
.__react-root(id= title data-app= title)
script(src="bundle.js")
This code will set the id and data-app attributes of a DIV decorated with the class name .__react-root to the value of the title we set in routes/index.js. In other words, we are using templating to change this value so that our app will know which component to show.
Here is most of the code for our new endpoint in routes/index.js:
router.get('/worker', (request, response) => {
response.render('worker', {
title: WHAT_GOES_HERE
});
});
I have left one challenge for you. When trying to set the title, recall what you know about passing queries to a server. Recall that in tileData we defined worker route that took a query parameter called title. Use the request object to access the value of title and assign it to the title property in the second parameter of the call to render. As an FYI, I’ll remind you that the first parameter of the call to render is the name of the PUG file that we want to render into HTML.
Finally, let’s make a change to views/index.pug to allow us to bootstrap the app the first time it loads:
extends layout
block content
#root
.__react-root(id="App" data-app="App")
script(src="bundle.js")
This code is used by control.js to load the App component the first time it is loaded. We should also undo our previous work in App.js since we now have another way to load Go and First:
import React, {Component} from 'react';
import logo from './images/tree-of-life.png';
class App extends Component {
render() {
return (
<div>
<h2>Welcome Home</h2>
<img src={logo} className="App-logo" alt="logo"/>
</div>
);
}
}
export default App;
Clean up
To display the menu correctly we need to clean some things up.
Remove this code from views/index.pug:
h1= title
p Welcome to #{title}
Set the padding in public/stylesheets/style.css to 0:
body {
padding: 0px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
Add Material Button
Let’s style our button in Go.js the material-ui look and feel. Import the button from the material library and display it:
import Button from '@material-ui/core/Button';
// CODE OMITTED HERE
<Button
variant="contained"
color="primary"
data-url="/git-gist-you-rang"
onClick={event =>
this.elfQuery('/foo', this.setFooData, event)
}
>
Query Foo
</Button>
The properties of the button are pretty self explanatory.
Click Doesn’t Work
This line in your tests might begin to fail:
wrapper.find('button').simulate('click');
To fix the problem, add an id field in your buttons in Go.js:
<Button
id="elfQueryAction"
variant="contained"
color="primary"
data-url="/git-gist-you-rang"
onClick={event =>
this.elfQuery('/foo', this.setFooData, event)
}
>
Query Foo
</Button>
And then use the ID in your test:
wrapper.find('#elfQueryAction').simulate('click');
NOTE: Sometimes you can still find the Button, but you might need to do something like this: find(‘WithStyles(Button)’).
Load an Image
You can download the tree of life into a directory called source/images like this:
wget https://s3.amazonaws.com/bucket01.elvenware.com/images/tree-of-life.png
There are three steps involved:
- Create the images directory
- Navigate into it
- Issue the wget command
Note that the Tree of Life is a PNG file, not an SVG. You should, therefore replace the extension in the appropriate line near the top of ElfHeader and play with the relative path to its location.
import logo from './images/tree-of-life.png';
Here is the Tree of Life.
To load the image, you need to add a new rule to webpack. The rule looks like this:
{
test: /\.(png|jpe?g|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {},
},
],
}
The symtax in Webpack is tricker. Therefore I will show you the same code again, but this time in context. I’m trying to show you where in webpack.config.js you want to put the next rule. It belongs in the rules property of the module section. So we do it like this:
module: {
rules: [
{
test: /.js?$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react'
]
}
}
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: [
{
loader: 'file-loader',
options: {},
},
],
},
{
test: /\.svg$/,
use: [{
loader: 'svg-inline-loader',
options: {}
}]
}
]
}
Now there are two rules in webpack, one for loading babel and one for loading images.
The above needs to be done one time. After that, you can load images easily from your bundle. For instance, you can add this code to App.js:
import logo from './images/tree-of-life.png';
<img src={logo} className="App-logo" alt="logo"/>
After making the updates, if you program is running (npm start) stop it with Ctrl-C and start it again.
Tests
We should now make sure we have tests for the App component.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Adapter from 'enzyme-adapter-react-16';
import { configure, shallow } from 'enzyme';
configure({ adapter: new Adapter() });
describe('Go Tests', () => {
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App/>, div);
ReactDOM.unmountComponentAtNode(div);
});
it('renders and reads H1 text', () => {
const wrapper = shallow(<App/>);
// console.log(wrapper.debug());
const welcome = <h1>Address Simple Home</h1>;
expect(wrapper.contains(welcome)).toEqual(true);
});
});
Images in Tests
We have to take an extra step to test components that contain images. Create a new directory in the root of your project called __mocks__. That’s underbar, underbar, mocks, underbar, underbar.
In it, put two files:
- styleMock.js
- fileMock.js
In styleMock put only this:
module.exports = {};
In fileMock, put only this:
module.exports = 'test-file-stub';
Then add this to the bottom of your package.json file:
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
}
Now, whenever a test tries to load an image, it will load a mock empty file instance. This is fine in tests, since we don’t usually need to actually do anything with the image itself in a test.
Fill Menu
Your goal will be to fill in the menu for all the components we have created. When the program starts, none of them are visible, just the the area where we display data:
IMAGE: The menu. First item is sort of home, the rest point to various components. (We will do login later. You can ignore it.)
IMAGE: The home menu selected. (No components chosen)
IMAGE: Go with material-ui buttons
IMAGE: Go selected from menu
Turn it in
Add, commit, push, tag and/or branch. When you submit the assignment, let me know what tag and/or branch you used when submitting the assignment.
Images for Header
I found images here:
- https://pixabay.com/en/gold-fish-aquarium-goldfish-fins-30831/
- https://pixabay.com/en/goldfish-fins-tropical-animal-47022/
- https://commons.wikimedia.org/wiki/File:Small_SVG_house_icon.svg
- https://commons.wikimedia.org/wiki/File:Flower-of-Life-91circles36arcs.svg
- https://commons.wikimedia.org/wiki/File:Tree-of-Life_Flower-of-Life_Stage.svg
Try also, this search in Chrome/Chromium:
https://www.google.com/search?q=svg+free+small
In the browser, turn to the images page. Select tools, and select Labeled for non-commercial reuse or something similar.
The Main Index
Remember that we have fundamentally changed the structure of our program. Our src/index.js entry point file should no longer be responsible for showing Address, and GetFile. Instead, it should show only App.
Here is our modified index.js file:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(
<div>
<App />
</div>,
document.getElementById('root')
);
registerServiceWorker();
NOTE: A crucial point, in fact, probably the central point, of this class, is how easy it is for us to move classes and views around when we use the React architecture. Yes, it is hard to get up to speed on React, and yes, it is a fairly complex tool. But once you have everything set up, making relatively large changes to our program’s architecture are simple. The small, focused loosely coupled components that we have created give us the flexibility to accept changes in specifications with a minimum of disruption.