Part 6 of the course: adding navigation (pages) to our application.
Explaining difference between single page and non-single page applications
Created a button to add new photos inside PhotoWall. When clicked, the button is supposed to take us to a new page where we can add a new photo.
So now, we need to implement the 'multiple' page behaviour.
First we will create a new component for the new 'page' and call it 'AddPhoto' (export it and all).
In this video, he explains a certain way to do it, which does not seem to be the proper way. Ignore for now.
Now we are working with the proper way of creating pages in a one page application, and the proper way of linking to them.
Start by installing:
npm install react-router-dom
Note: these might be installed by default, but anyway.
Now go to index.js, and
import {BrowserRouter, Switch, Route} from 'react-router-dom'
actually, we just need the BrowserRouter here, we will use Route in a different file.
And modify your index.js code to include <BrowserRouter>
tags:
Index
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import Main from './components/Main';
import './styles/stylesheet.css';
import {BrowserRouter, Switch, Route} from 'react-router-dom'
ReactDOM.render(<BrowserRouter><Main/></BrowserRouter>, document.getElementById('root'));
//make sure no spaces
Now links:
Go to the PhotoWall component (where our add button is located), import Link:
import {Link} from 'react-router-dom'
And then change the button into Link component while specifying its as follows:
PhotoWall
import React, {Component} from 'react';
import Photo from './Photo';
import PropTypes from 'prop-types';
import {Link} from 'react-router-dom'
class PhotoWall extends Component {
render(){
return(
<div>
<Link className="addIcon" to="/AddPhoto"></Link>
<div className="photoGrid">
{this.props.posts.map((post, index) => <Photo key={index} post = {post} onRemovePhoto ={this.props.onRemovePhoto}/>)}
</div>
</div>
)
}
}
PhotoWall.propTypes ={
posts: PropTypes.array.isRequired,
onRemovePhoto: PropTypes.func.isRequired
}
export default PhotoWall;
not finished yet, continue below:
So far, we have a link that is supposed to take us to a path, and we have Main inside <BrowseRouter>
which prepares Main to allow for browse routing. However, we still don't have the individual pieces of UI (or pages) assigned to their respective paths.
To do that, go to Main and import: import {Route} from 'react-router-dom'
Then modify the code of Main as follows:
Main
import React, {Component} from 'react';
import Title from './Title';
import PhotoWall from './PhotoWall';
import AddPhoto from './AddPhoto';
import {Route} from 'react-router-dom'
class Main extends Component {
constructor(){
console.log('constructor');
super();
this.state = {
posts: [{
id: "0",
description: "beautiful landscape",
imageLink: "https://image.jimcdn.com/app/cms/image/transf/none/path/sa6549607c78f5c11/image/i4eeacaa2dbf12d6d/version/1490299332/most-beautiful-landscapes-in-europe-lofoten-european-best-destinations-copyright-iakov-kalinin.jpg" +
"3919321_1443393332_n.jpg"
}, {
id: "1",
description: "Hello I am ghadir",
imageLink: "https://img.purch.com/rc/640x415/aHR0cDovL3d3dy5zcGFjZS5jb20vaW1hZ2VzL2kvMDAwLzA3Mi84NTEvb3JpZ2luYWwvc3BhY2V4LWlyaWRpdW00LWxhdW5jaC10YXJpcS1tYWxpay5qcGc=" +
"08323785_735653395_n.jpg"
}, {
id: "2",
description: "On a vacation!",
imageLink: "https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/2017/08/24/104670887-VacationExplainsTHUMBWEB.1910x1000.jpg"
}],
screen: 'photos'
}
this.removePhoto = this.removePhoto.bind(this);
// this.navigate = this.navigate.bind(this);
}
removePhoto(postRemoved){
console.log(postRemoved.description);
this.setState((state) => ({
posts: state.posts.filter( post => post !== postRemoved)
}))
}
// navigate(){
// this.setState({
// screen: 'addPhoto'
// })
// }
componentDidMount(){
console.log('componentDidMount')
}
componentDidUpdate(){
console.log('componentDidUpdate')
}
render() {
console.log('render');
return (
<div>
<Route exact path = '/' render={() => (
<div>
<Title title={'PhotoWall'}/>
<PhotoWall posts = {this.state.posts} onRemovePhoto ={this.removePhoto} /*onNavigate = {this.navigate}*//>
</div>
)} />
<Route path = '/AddPhoto' render={() => (
<div>
<AddPhoto/>
</div>
)} />
</div>
)
}
}
export default Main
In fact, you can further modify the render of Main and make it like this:
Main Render
render() {
console.log('render');
return (
<div>
<Route exact path = '/' render={() => (
<div>
<Title title={'PhotoWall'}/>
<PhotoWall posts = {this.state.posts} onRemovePhoto ={this.removePhoto} /*onNavigate = {this.navigate}*//>
</div>
)} />
<Route path = '/AddPhoto' component = {AddPhoto}/>
</div>
)
}
So since AddPhoto is a single component, we can Route to it just by calling its name (wheareas above, when we had a path to something with multiple components, we had to do it differently).
It is best however to always have one root component containing everything in the page, and link to it in the second concise way.
Dont' forget to add
exact path
to the default ('/') path.
Just created the html and added css. Here is the html:
AddPhoto
import React, {Component} from 'react';
class AddPhoto extends Component {
render(){
return(
<div>
<h1>Photowall</h1>
<div className ="form">
<form>
<input type="text" placehoder="Link"></input>
<input type="text" placehoder="Description"></input>
<button>Post</button>
</form>
</div>
</div>
)
}
}
export default AddPhoto
Now we want to alter the AddPhotos page in a way that allows me to enter an image link and a description, and they will be added to the original array of posts that is currently in Main.
In AddPhoto we created a method called handleSubmit (and added a constructor to the class, so that we can bind this to our method), and created a call to the function on form submission.
The handleSubmit function prevents the default behavior of page refresh, it also checks for whether inputs were empty, and it is also supposed to call a function that updates the original posts array in state. To do this, we have to create that function in Main, where the array lives.
So go to Main and create a function addPhoto, seen below. Now we need to pass it props. However, the way we are currently calling AddPhoto components (through the Route) does not allow for passing props, so we returned it the way it was (non-concise way seen above) to be able to pass the method in props.
We passed the method in props. We created a 'post' object inside handleSubmit (made from the submitted infor) and then called addPhoto(post) - which will add the created post to the array (seee cooooodeeeee).
We also used something called history + history.push in the Router part, which allows a go back to previous page once a certain action has been completed.
AddPhoto
import React, {Component} from 'react';
class AddPhoto extends Component {
//we are mostly declaring a constructor here just to bind 'this' to handleSubmit
constructor(){
super();
this.handleSubmit = this.handleSubmit.bind(this);
}
//note: on form submissions, we get a page refresh.
//we don't like page refresh.
//That's why we caught the event in the argument of handleSubmit and prevented it.
handleSubmit(event){
event.preventDefault();
//in the below, target refers to the submitted form7
//elements refer to elements inside the form
//we can access individual elements through their names
const imageLink = event.target.elements.link.value;
const description = event.target.elements.description.value;
const post = {
id:0,
description: description,
imageLink: imageLink
}
if (description && imageLink){
this.props.onAddPhoto(post); //post here is the object created above
}
}
render(){
return(
<div>
<h1>Photowall</h1>
<div className ="form">
<form onSubmit = {this.handleSubmit}>
<input type="text" placehoder="Link" name='link'></input>
<input type="text" placehoder="Description" name='description'></input>
<button>Post</button>
</form>
</div>
</div>
)
}
}
export default AddPhoto
Main
import React, {Component} from 'react';
import Title from './Title';
import PhotoWall from './PhotoWall';
import AddPhoto from './AddPhoto';
import {Route} from 'react-router-dom'
class Main extends Component {
constructor(){
console.log('constructor');
super();
this.state = {
posts: [{
id: "0",
description: "beautiful landscape",
imageLink: "https://image.jimcdn.com/app/cms/image/transf/none/path/sa6549607c78f5c11/image/i4eeacaa2dbf12d6d/version/1490299332/most-beautiful-landscapes-in-europe-lofoten-european-best-destinations-copyright-iakov-kalinin.jpg" +
"3919321_1443393332_n.jpg"
}, {
id: "1",
description: "Hello I am ghadir",
imageLink: "https://img.purch.com/rc/640x415/aHR0cDovL3d3dy5zcGFjZS5jb20vaW1hZ2VzL2kvMDAwLzA3Mi84NTEvb3JpZ2luYWwvc3BhY2V4LWlyaWRpdW00LWxhdW5jaC10YXJpcS1tYWxpay5qcGc=" +
"08323785_735653395_n.jpg"
}, {
id: "2",
description: "On a vacation!",
imageLink: "https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/2017/08/24/104670887-VacationExplainsTHUMBWEB.1910x1000.jpg"
}],
}
this.removePhoto = this.removePhoto.bind(this);
this.addPhoto = this.addPhoto.bind(this);
}
removePhoto(postRemoved){
console.log(postRemoved.description);
this.setState((state) => ({
posts: state.posts.filter( post => post !== postRemoved)
}))
}
addPhoto(postSubmitted){
this.setState(state => ({
posts: state.posts.concat([postSubmitted]) //we returned postSubmitted as an array. But why not push it normally?
}))
}
componentDidMount(){
console.log('componentDidMount')
}
componentDidUpdate(){
console.log('componentDidUpdate')
}
render() {
console.log('render');
return (
<div>
<Route exact path = '/' render={() => (
<div>
<Title title={'PhotoWall'}/>
<PhotoWall posts = {this.state.posts} onRemovePhoto ={this.removePhoto} />
</div>
)} />
<Route path = '/AddPhoto' render={({history}) => (
<div>
<AddPhoto onAddPhoto={(addedPost) =>{
this.addPhoto(addedPost);
history.push('/');
}}/>
</div>
)} />
</div>
)
}
}
export default Main
When testing make sure the url of image ends with something like a .jpg. You can check this website: https://homepages.cae.wisc.edu/~ece533/images/
Also , you had some issue with the placeholders not appearing. See into that.
We want to display the newly entered photo first (so sort posts acc to newly entered), and we will make use of the posts IDs to do that -> we will put into them the number of milliseconds since 1970.
So now, we will make the id property to become a date. Go back to the posts array and change the string ids into numbers (0, 1, 2) for consistency.
Now in the AddPhoto component, when creating the new post object, put a new Date
[actually, id: Number(new Date())
] instead of the 0 we had previsouly.
Now, we have to sort the array of posts by ID when we render it. This is done in the PhotoWall component, so alter the contents of the div element with className 'photoGrid' (see below).
AddPhoto / only object code
const post = {
id: Number(new Date()),
description: description,
imageLink: imageLink
}
Main / only posts array code
posts: [{
id: 0,
description: "beautiful landscape",
imageLink: "https://image.jimcdn.com/app/cms/image/transf/none/path/sa6549607c78f5c11/image/i4eeacaa2dbf12d6d/version/1490299332/most-beautiful-landscapes-in-europe-lofoten-european-best-destinations-copyright-iakov-kalinin.jpg" +
"3919321_1443393332_n.jpg"
}, {
id: 1,
description: "Hello I am ghadir",
imageLink: "https://img.purch.com/rc/640x415/aHR0cDovL3d3dy5zcGFjZS5jb20vaW1hZ2VzL2kvMDAwLzA3Mi84NTEvb3JpZ2luYWwvc3BhY2V4LWlyaWRpdW00LWxhdW5jaC10YXJpcS1tYWxpay5qcGc=" +
"08323785_735653395_n.jpg"
}, {
id: 2,
description: "On a vacation!",
imageLink: "https://fm.cnbc.com/applications/cnbc.com/resources/img/editorial/2017/08/24/104670887-VacationExplainsTHUMBWEB.1910x1000.jpg"
}],
PhotoWall
import React, {Component} from 'react';
import Photo from './Photo';
import PropTypes from 'prop-types';
import {Link} from 'react-router-dom'
class PhotoWall extends Component {
render(){
return(
<div>
<Link className="addIcon" to="/AddPhoto"></Link>
<div className="photoGrid">
{this.props.posts
.sort(function(x, y){
return y.id - x.id
})
.map((post, index) => <Photo key={index} post = {post} onRemovePhoto ={this.props.onRemovePhoto}/>)}
</div>
</div>
)
}
}
PhotoWall.propTypes ={
posts: PropTypes.array.isRequired,
onRemovePhoto: PropTypes.func.isRequired
}
export default PhotoWall;