Part 8 of the course, video 1
Add the CSS from the resources to your code.
Now, we want to add the comments functionality. We actually want to make it such that when we click on a picture (/post), a new pages opens up with that picture, along with a form that allows us to enter comments.
We first want to implement the 'move to new page upon picture click' functionality.
So we have to go to Photo (because moving to new page is done on indidual Photo basis, the new rendered component depends on which photo was clicked). So go to Photo and import
import {Link} from 'react-router-dom'
Then you declare a <Link>
element around your img element as follows:
Photo
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {Link} from 'react-router-dom'
class Photo extends Component {
render(){
const post = this.props.post
return (
<figure className="figure">
<Link to = {'single'}><img className="photo" src={post.imageLink} alt={post.description}></img></Link>
<figcaption><p>{post.description}</p></figcaption>
<div className = "button-container">
<button className="remove-button" onClick={() => {
this.props.removePost(this.props.index);
}}>Remove</button>
</div>
</figure>
)
}
}
Photo.propTypes ={
post: PropTypes.object.isRequired,
}
export default Photo;
Now, if you go to your UI and click on a certain photo, ti will go to localhost:3000/single -> however, since we haven't yet assigned anything to that path, it renders nothing (just the title that is rendered everywhere).
So remember, to create a functional link, you create both the Link, the Route, and its destination. We will created the destination next.
Crate a new folder inside components and name it Single.js. These are the intial contents:
Single
import React, {Component} from 'react';
class Single extends Component {
render(){
}
}
export default Single;
Now go to Main. We will declare a new Route there. Make sure to import Single in the header:
Main
import React, {Component} from 'react';
import Title from './Title';
import PhotoWall from './PhotoWall';
import AddPhoto from './AddPhoto';
import {Route} from 'react-router-dom'
import {Link} from 'react-router-dom'
import {removePost} from '../redux/actions'
import Single from './Single'
class Main extends Component {
constructor(){
console.log('constructor');
super();
}
render() {
console.log(this.props)
return (
<div>
<h1><Link to="/">Photowall</Link></h1>
<Route exact path = '/' render={() => (
<div>
<PhotoWall {this.props} />
</div>
)} />
<Route path = '/AddPhoto' render={() => (
<AddPhoto {this.props}/>
)} />
<Route path = '/single/:id' render={() => (
<Single/>
)} />
</div>
)
}
}
export default Main
Changes: new import, and new Route to /single/:id. Note that we are giving a parameter to the path: because the new rendered component will depend on which picture we are clicking on.
However, we now need to go back to Photo and alter the path we were giving.
Change this: Photo
<Link to = {'single'}><img className="photo" src={post.imageLink} alt={post.description}></img></Link>
To this:
<Link to = {`/single/${post.id}`}><img className="photo" src={post.imageLink} alt={post.description}></img></Link>
Now check your UI. Whenever we click on a pic it goes to the single/id path. Working beautifully.
Now go back to Main, to the Route of /single/:id. Since we are passing /single/:id to the path of <Route>
, then the id is accessible in the render of <Route>
as params
. Params contain a whole bunch of stuff, but we are currently interested in the id.
Example: the history
that we worked with earlier [In AddPhoto Route] is a part of params. We passed it directly as history, but we could also have used params up and then this.params.history in the bottom.
Ex:
//we can change this:
<Route path = '/AddPhoto' render={({history}) => (
<AddPhoto {this.props} onHistory = {history}/>
)} />
//to this:
<Route path = '/AddPhoto' render={(params) => (
<AddPhoto {this.props} onHistory = {params.history}/>
)} />
//currenlty the history is removed, this is an old example.
//History is a property of params, that's why we place it in {}
So back to the single Route. We edited it as follows:
Main - last Route
<Route path = '/single' render={(params) => (
<Single {params}/>
)} />
So what did we do above? We added {...params} to the Single element - which means we are passing params in the props. But why didn't we specify a name for the props? Because that is what the spread operator is there for. When I use the spread operator, it automatically maps all the contents of params into name/value props that are passed down. So instead of writitg history = params.history
, and id = params.id
, etc, I actually shortened everything and just used the spread operator (which produces the above history, id, etc, props).
Now let's go back to Single. We slightly edited it as follows:
Single
import React, {Component} from 'react';
class Single extends Component {
render(){
return(
const id = this.props.match.params.id;
<div className="single-photo">
</div>
)
}
}
export default Single;
In this line: this.props.match.params.id
,
'match' is simply an object that came from {...params}
. It is as if I am saying, this.props.[find the object in props that matches:].params.id
--> I think we should use match
whenever we use a spread operator to pass props. In the case when we have two spread operators passing down props, 'match' will belong to the second one. That's why when we added {...this.props}
below, we had to pass {...params}
after {...this.props}
, or else match would have belonged to {...this.props}
. [see Router in Main below].
So now, inside Single, we have access to the id of the post that was clicked in order to reach Single. However, we don't have other information related to that post (because the state is not injected into Single). So we have to pass the state down throught props from Main just like we always do.
Main - edited [just the Route of Single part]:
//was:
<Route path = '/single' render={(params) => (
<Single {params}/>
)} />
//with the edit (passing props):
<Route path = '/single' render={(params) => (
<Single {this.props} {params}/>
)} />
//{...this.params} should be passed second if we want to access its 'match'.
Note: when you want to console.log an object and see it (see properties and stuff in the console), don't concatinate it with a string. Don't do:
console.log("the object is:" + this.props
. Write directlyconsole.log(this.props)
.
Now, we want to start building Single UI. We know that components are reausable, so we are going to reuse the Photo component inside the Single component.
So in Single, import Photo from './Photo'
and then do the following edits:
Single
import React, {Component} from 'react';
import Photo from './Photo'
// import PropTypes from 'prop-types';
// import {Link} from 'react-router-dom'
class Single extends Component {
render(){
const {match, posts} = this.props;
//what does the above means? It means, since match can be accessed throug this.props.match,
//and posts can be accessed through this.props.posts, I directly assigned them (bi darbe wa7de)
//to their values. It is a concise manner of writing the following two lines:
//const match= this.props.match;
//const posts = this.props.post;
const id = Number(match.params.id) //transforming id to a number (it was passed down from url as a string)
//now, we need to find the specific post with the above id in our store:
const post = posts.find((post) => post.id === id);
return(
<div className="single-photo">
<Photo post = {post}/>
</div>
)
}
}
export default Single;
Now, when we click on a Photo in the main page, it will take us to the page to enter the content (/single) -> not fully constructed yet, only the photo appears in new page.
Keep in mind that if you currently press on Remove, it will give an error. We will deal with it later.
Now we will create a new component for comments (in components folder, like always). Create an empty class component, import it to Single. We will work back and forth between Single and Comment components and then show the final result here.
Single
import React, {Component} from 'react';
import Photo from './Photo'
import Comments from './Comments';
class Single extends Component {
render(){
const {match, posts} = this.props;
const id = Number(match.params.id)
const post = posts.find((post) => post.id === id);
return(
<div className="single-photo">
<Photo post = {post}/>
<Comments/>
</div>
)
}
}
export default Single;
Comments
import React, {Component} from 'react';
class Comments extends Component {
render(){
return(
<div className = "comment">
<form className="comment-form">
<input type="text" placeholder="comment"/>
<input type="submit" hidden/>
</form>
</div>
)
}
}
export default Comments
Note that we made the button hidden in the Comments form. That makes the form submit on hitting enter (it subits on hittin enter anyway - I guess). So we have to have a button to allow the form to submit, and we made it hidden.
Note that the page refreshes on submit -> Normal behaviour.
We currently have one piece of state called posts (note how we used to inject it into Main, etc). Now we will create another piece of state called comments (more like an attribute to the state).
Before, we will create the comment reducer then combine existing reducers into one. Go to the reducer file and edit as follows:
Reducer
import posts from '../data/posts'
import {combineReducers} from 'redux'
const commentReducer = (state = [], action) => {
return state
};
const postReducer = (state = posts, action) => {
console.log(action.index)
switch(action.type){
case 'REMOVE_POST': return [state.slice(0, action.index), state.slice(action.index + 1)]
case 'ADD_POST': return [state, action.post]
default: return state
}
};
const rootReducer = combineReducers({
posts: commentReducer,
comments: postReducer
});
export default rootReducer
We already exported rootReducer as rootReducer in Index.
Now go to App and add another piece of state as follows: (note the before and after):
App
//before
function mapStateToProps(state){
return{
posts: state
}
}
//After
function mapStateToProps(state){
return{
posts: state.posts,
comments: state.comments
}
}
Make sure that the 'pieces of state' match the names of the reducer (or properties given in reducer). Now each reducer will update the piece of state relevant to itself.
Now we will create the actions for the comments:
Action (only new action)
export function addComment(comment){
return{
type: 'ADD_COMMENT',
comment //remeber, same as comment: comment
}
}
Remember: comment here is a 'payload' that will passed to the reducer when the action is dispatched.
The addComment action is already in our props, since in App we bound all the actions in actions.js to dispatch (see mapDispatchToProps in App).
And since we are passing all possible props from Main down to other components, we are good to go.
So go to Single, it is receiving props up from Main -> it has received the addComment action as a prop. Now we need to pass it further down to <Comments>
so that we can dispatch it once a comment is clicked.
Now I can pass all the props down to Comments (with {...this.props}) but since Comments is only going to be needing the action, it is better to just pass the action to it:
Single (return statement only)
return(
<div className="single-photo">
<Photo post = {post}/>
<Comments addComment = {this.props.addComment}/>
</div>
)
Now we can use the action inside Comments by writing this.props.addComment. We will start by declaring a function handleSubmit in Comments.
Time out:
For some reason, this code in the reducers seem to create a problem.
Problematic code
import posts from '../data/posts'
import {combineReducers} from 'redux'
commentReducer = (state = [], action) => {
return state
};
postReducer = (state = posts, action) => {
console.log(action.index)
switch(action.type){
case 'REMOVE_POST': return [state.slice(0, action.index), state.slice(action.index + 1)]
case 'ADD_POST': return [state, action.post]
default: return state
}
};
const rootReducer = combineReducers({
posts: commentReducer,
comments: postReducer
});
export default rootReducer
Following what the guy did in the tutorial worked, so we're staying with this for now:
import _posts from '../data/posts'
import {combineReducers} from 'redux'
function comments (state = [], action){
return state
};
function posts (state = _posts, action) {
switch(action.type){
case 'REMOVE_POST': return [state.slice(0, action.index), state.slice(action.index + 1)]
case 'ADD_POST': return [state, action.post]
default: return state
}
};
const rootReducer = combineReducers({posts, comments});
export default rootReducer
Back to handleSubmit:
Here is the code of Comments:
Comments
import React, {Component} from 'react';
class Comments extends Component {
constructor(){
super()
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(event){
event.preventDefault()
const comment = event.target.elements.comment.value
this.props.addComment(comment) //calling the action (which will be dispatched somewhere else)
}
render(){
return(
<div className = "comment">
<form className="comment-form" onSubmit={this.handleSubmit}>
<input type="text" placeholder="comment" name="comment"/>
<input type="submit" hidden/>
</form>
</div>
)
}
}
export default Comments
Note: we did a nice thing where we console.log a sentence in each reducer and then dispatched an action unique to one of the reducers. We realized that both of the reducers where called -> that is because all reducers get triggered by actions, the one that will deal with it is the one that actually has that action in its cases.
Now go to update the comment reducer:
Reducer - comments reducer only
function comments (state = [], action){
switch(action.type){
case 'ADD_COMMENT': return [state, action.comment]
default: return state
}
};
Now, it seems we also want to display comments written by user.
So now we also want to access the state comment array from our components. Go to Single. We already have access to our store there. So simpl
Single
import React, {Component} from 'react';
import Photo from './Photo'
import Comments from './Comments';
class Single extends Component {
render(){
const {match, posts} = this.props;
const id = Number(match.params.id)
const post = posts.find((post) => post.id === id);
const comments = this.props.comments
return(
<div className="single-photo">
<Photo post = {post}/>
<Comments addComment = {this.props.addComment} comments={comments}/>
</div>
)
}
}
export default Single;
Updates: we accessed comments from the props and passed them down again as props to Comments.
Now go to Comments. We will map over the array of comments:
Comments
import React, {Component} from 'react';
class Comments extends Component {
constructor(){
super()
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(event){
event.preventDefault()
const comment = event.target.elements.comment.value
this.props.addComment(comment) //calling the action (which will be dispatched somewhere else)
}
render(){
return(
<div className = "comment">
{
this.props.comments.map((comment, index) => {
return(
<p key = {index}> {comment} </p>
)
})
}
<form className="comment-form" onSubmit={this.handleSubmit}>
<input type="text" placeholder="comment" name="comment"/>
<input type="submit" hidden/>
</form>
</div>
)
}
}
export default Comments
Updates: inside the comments container, we mapped through the comment array and created a <p>
for each of them, and included the index as key so that the compiler doesn't start yelling at us.
Everything is working properly, except that the comments that I write for one post show up for all other posts. We will fix this in the next section.