ReactGitMenu
React Git Menu
The goals of this assignment are:
- Add material-ui to our application.
- Create a material-ui menu that uses react-router
- Use the menu to display and hide the GetGist code.
NOTE I heavily rewrote this assignment on May 8, 2019. (The last commit before: 21246).
Menu
In this serious of screen shots you can see the menu, and the various views it provides. In particular, each view features one component, or one component and its child components. This is a SPA, or Single Page App.
TODO Add screenshots
IMAGE: The components folder
TODO Add screenshots
Tag It
Before performing major surgery, I like to tag my code. You should push your work, then tag it with elf-tagger:
elf-tagger "Before router dom menu" "week03-rest-basics"
Or:
tag -a v0.X.X -m "Before building router dom menu in week04-docker-compose"
I often also start a test branch based on my working branch. I do my work there, and if I’m happy, I merge the changes back into my main working branch. If I got into to my trouble, I switch back to my working branch, delete the botched test branch, then start a new test branch and try again. If it is good this time, I merge it back into my working branch.
Delete: git branch -d test
Install React Router
Read the React Router Dom Install section from Elvenware then return to this document and install react-router-dom in the client directory.
npm i react-router-dom
Be sure you have prop types:
npm i prop-types;
Optionally, consider installing a CSS library (I’m not doing this yet, myself)
Also install material-ui:
npm i @material-ui/core @material-ui/icons
Insert the new ElfHeader
Replace ElfHeader with the code in this gist.
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.
Create a new file called client/components/tileData.js with the content from this gist
App
We are going to fundamentally change the structure of our program. This means making changes to this JSX in App.js:
<div className="App">
<ElfHeader/>
<GitUser/>
<Micro/>
<ApiFoo appInit={appInit}/>
</div>
Defining Routes
The final step involves defining what the application will do when the user clicks on a particular menu item. Here is the basic structure with everything else temporarly stripped away from App.js:
NOTE: _Don’t actually strip things out. Just look at what I have done here and leave your code alone.__
import {BrowserRouter, Route} from "react-router-dom";
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<ElfHeader/>
<Route path="/git-user" component={GitUser}/>
// YOU WRITE APIFOO AND MICRO
</div>
</BrowserRouter>
);
}
}
And we will, in time do it like this for the home menu, where we use exact path:
<Route exact path="/" component={Home}/>
Note the user of the flag exact. This is necessary because a simple match on ‘/’ will pass both ‘/’ and ‘/api-foo’. In fact, it will match any URL beginning ‘/’. So we tell the router that we want an exact match.
I want you to have to figure out at least one of these routes on your own, so I will let you discover the solution for creating the Route to GetFoo. It isn’t hard, so try not to over-complicate it.
The normal pattern is to define the path, and then the component:
<Route path="/api-foo" component={ApiFoo}/>
You cannot, however, pass props to ApiFoo the same way you can elsewhere in a React application. Instead, you use render in place of component, and the syntax looks like this in our hypothetical case:
<Route
path="/api-foo"
render={(props) => (
<ApiFoo {...props} appInit={appInit} />
)}
/>
We use this syntax because react-router-dom passes a certain number of props to a component by default, and we don’t want to lose them. Here we use the spread-operator to pass the Router props and then we pass our own props.
This transformation is tricky. Do it bit by bit. Start here with this familiar code:
<ApiFoo appiInit={appInit} />
Wrap it in the react-route-dom Route:
<Route
<ApiFoo appiInit={appInit} />
/>
Add in path and the spread props:
<Route
path="/api-foo"
<ApiFoo {...props} appiInit={appInit} />
/>
So far so good. Now here is the tricky part: add the render method:
<Route
path="/api-foo"
render={(props) => (
<ApiFoo {...props} appInit={appInit} />
)}
/>
If you need to pass additional props, do it like this:
<Route
path="/api-foo"
render={(props) => (
<ApiFoo
{...props}
appInit={appInit}
foo={fooInit}
bar={barInit}
/>
)}
/>
It looks a horror, but if you take it step by step it makes sense.
Here is what it looks like in the debugger when working with a different component than the one we use in program:
IMAGES: You can see the Sources tab of Chrome Developer Tools
I’ve run to a breakpoint on line 10 of SmallNumbers.js. At the bottom right we can see that props for SmallNumbers is in the Local Scope. The debugger displays our Numbers prop, along with three properties passed by the react-router-dom:
- history
- location
- match
They can come in useful in more advanced scenarios than the one we see here. In particular, they can help preserve history so the user can move back and forth through your site.
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.
Our create-react-app handles loading images for us, but if we were in an elf-experss app, to load the image, you need to add a new rule to webpack. The rule looks like this:
{
test: /\.(png|jpe?g|gif)$/,
use: [
{
loader: 'file-loader',
options: {},
},
],
}
The symtax in Webpack is tricky. 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: {},
},
],
}
]
}
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"/>
Material UI Buttons
Assuming that GetGist.js is a React Class Component, then a button looks like this:
import React from 'react';
import Button from '@material-ui/core/Button';
// Code omitted here. Then inside of render:
<Button
variant="contained"
color="primary"
data-url="/git-gist-you-rang"
onClick={this.props.queryServer}>
Ring Git Gist
</Button>
The properties are pretty self explanatory.
When using a material ui button, our fetch calls in App.js should use event.currentTarget rather than event.target. Fortunately, currentTarget works for regular buttons as well. For instance:
queryServer = (event) => {
const url = event.currentTarget.dataset.url;
// Now call fetch
}
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: Qux selected from menu
IMAGE: Test Routes selected from menu
Style the Menu
Read about it here
Show All Fields of Git User
Run the tests here to be sure that you are displaying a minimum number of fields from the User. These test check that you have created an element of some kind with an ID that corresponds to each of the fields that should be displayed. For instance, I’m checking that at minimum, you have something like this:
<p id="login">Login: {body.login}</p>
The test is fairly loose. It does not check if the content of the node is correct. That would be difficult or impossible since everyone will have different content. But it does check that you have an element with the proper ID. You are, of course, expected to actually display the contents of the field in that element.
NOTE: I’m deliberately keeping the type of the component unspecified to give you the freedom to design the page as you wish. I just want it to contain an element with a specific ID and I’m expecting you to use the element to display the appropriate field. For instance, the element could be a span, but the span should contain the relevant field from the User object returned by GitHub.
Something like this, but you are free to design it as you wish:
Turn it in
Add, commit, push, tag and/or branch. Let me know the tag and/or branch as well as the directory for your work. Make sure all your tests pass. For now.
Testing ElfHeader
I need to review this code. Don’t try it yet but look for updates. We should wrap our ElfHeader in a MemoryRouter when doing the renders without crashing test:
There are two ways to test ElfHeader. Method one:
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<ElfHeader/>, div);
//ReactDOM.render(<MemoryRouter><ElfHeader /></MemoryRouter>, div);
ReactDOM.unmountComponentAtNode(div);
});
More complex, but apparently no longer needed:
import {MemoryRouter} from "react-router-dom";
// CODE OMITTED
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<MemoryRouter><ElfHeader /></MemoryRouter>, div);
ReactDOM.unmountComponentAtNode(div);
});