Now, let’s create the data structure to store list of movies, which comes in an array of objects:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const movies = [ { _id: "5b21ca3eeb7f6fbccd471815", title: "Terminator", genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, numberInStock: 6, dailyRentalRate: 2.5, publishDate: "2018-01-03T19:04:28.809Z" }, { _id: "5b21ca3eeb7f6fbccd471833", title: "Shaolin Soccer", genre: { _id: "5b21ca3eeb7f6fbccd471814", name: "Comedy" }, numberInStock: 7, dailyRentalRate: 3.5 }, .. .. |
First, we create GPMovieDataClass.js class component. We put this in utils folder.
1 2 3 4 5 6 7 8 |
class GPMovieDataClass { constructor(array) { } } export default GPMovieDataClass; |
Our test data list of movies will have genre objects. Many movies will have the same genres. So let’s build a unique genre dictionary first.
The genre dictionary will have the genre name as its key, and the genre object (name, _id) as the value.
We create private dictionary _genres:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
... ... constructor(array) { let data = new Map(); const _genres = {}; function buildGenres(callback) { for (let i = 0; i < array.length; i++) { let genreName = array[i].genre.name.toUpperCase(); if (!_genres[genreName]) { _genres[genreName] = array[i].genre; } } callback(_genres); } ... ... |
Notice we also create private Map object data. This is so that each genre can point to the movies that belongs to this genre:
{ _id: “5b21ca3eeb7f6fbccd471814”, name: “Comedy” } –> [ {name: “Airplane”..}, {name:’Baseball’, …}, {…} … ];
We then execute buildGenres so that _genres dictionary is filled in and complete.
Each genre in _genres has an array. We place the specified genre movie in those arrays.
1 2 3 4 5 6 7 8 9 10 11 |
buildGenres(function(allGenres) { for (let i = 0; i < array.length; i++) { let genreName = array[i].genre.name; let genreObj = allGenres[genreName.toUpperCase()] if (data.get(genreObj)) { // append data.get(genreObj).push(array[i]); } else { // create new data.set(genreObj, [array[i]]); } } }); |
In the callback, we fill up the genre arrays with movies. When it has finished, we would have all movies with their genre objects as keys.
Now, one of the functionalities is to get all genre names.
We do this with function getGenres to return an array of genre names.
1 2 3 4 5 6 7 |
this.getGenres = () => { let namesArr = []; for (const item of data.keys()) { namesArr.push(item.name); } return namesArr; } |
We also need our list of genres as objects:
1 2 3 4 5 6 7 8 9 |
this.getGenreAsArrayOfObjects = () => { let total = []; let genreStrings = Object.keys(_genres); for (let i = 0; i < genreStrings.length; i++) { let genreObj = _genres[genreStrings[i]]; total.push(genreObj); } return total; } |
Get all movies by genre name:
1 2 3 4 |
this.getMoviesByGenreName = genreName => { let genreObj = _genres[genreName.toUpperCase()]; return data.get(genreObj); } |
Finally, get all movies. We basically through every genre available, and get their movies.
movies.jsx
1 2 3 4 5 6 7 8 9 10 11 12 |
this.getAllMovies = () => { let total = []; let allGenres = this.getGenres(); for (let i = 0; i < allGenres.length; i++) { total = total.concat(this.getMoviesByGenreName(allGenres[i])); } return total; } } } export default GPMovieDataClass; |
Incorporate GPMovieDataClass into Movies component
We first import it and then create an instance. We stick the movies data into it.
1 2 |
import GPMoviesDataClass from '../utils/GPMoviesData'; let moviesData = new GPMoviesDataClass(getMovies()); |
moviesData.getGenreAsArrayOfObjects() will return array of genre objects
moviesData.getMoviesByGenreName(genre.name) will return array of movies by genre name
We will be using them in our React code.
We use genres in our state object. We simply use getGenreAsArrayOfObjects to give our state’s genres an array of Genres to display.
Notice that by default, we initialize our state’s movies to all the movies.
State property showingArr maintains the array to be shown on the UI. We can’t just show ALL of the movies. We have to show the paginated portion of the movies.
movies.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Movies extends Component { state = { movies: getMovies(), pageSize: 4, currentPage: 1, showingArr: [], genres: [], selectedGenre: null, } componentDidMount() { this.setState({ showingArr: this.#paginateMovies(), genres: moviesData.getGenreAsArrayOfObjects() }); } |
Notice paginateMovies function. All it does is that depending on which number you give it, it gives you the starting pagination page, and ending pagination page. What we need is the pageSize in order to calculate this:
movies.jsx
1 2 3 4 5 6 7 |
#paginateMovies = (pageNumber=1) => { const { movies, pageSize } = this.state; let max = pageNumber * pageSize; let min = max - pageSize; let showingArr = [...movies]; return showingArr.slice(min, max); } |
Then we simply return that paginated part of the movies array. Remember that movies contain all movies right now. We just paginate according to page number.
We give the paginated array to state property showingArr. This will let us show just the paginated pages.
In render, we use showingArr to display the movie data for this page:
movies.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<tbody> {showingArr.map(movie => { return ( <tr key={movie._id}> <td>{movie.title}</td> <td>{movie.genre.name}</td> <td>{movie.numberInStock}</td> <td>{movie.dailyRentalRate}</td> <td><button onClick={() => this.handleDelete(movie)} className="btn btn-danger btn-sm">delete</button></td> </tr> ); })} </tbody> |
Thus, we always use showingArr to hold the movies that we’re showing on the page.
Displaying the Pagination links
So we create a Pagination component to display the links. The idea is that we need:
1) the total list of movies
2) page size
That way, we can calculate the total # of pages:
1 |
const pagesCount = itemsCount / pageSize; |
Once we have page count, we know how many links to display.
Then, we simply fill an array from 1 to pagesCount. This is to signal the page number the user have clicked. Once the user clicks it, it gives that page number back to movies component to render the movies for this particular page. The showingArr in movies will paginate and calculate it.
1 |
const pagesArr = _.range(1, pagesCount+1); |
Then for each element in the array, we simply return a list item with a link in it:
1 2 3 4 5 6 7 8 9 10 |
{pagesArr.map(page => { let isActive = page === currentPage ? 'page-item active' : 'page-item'; return ( <li key={page} className={isActive}> <a className="page-link" onClick={() => onPageChange(page)}> {page} </a> </li> )} )} |
We also give it isActive functionality. We compare if the currentPage number is the same as ours. If it is, we give ‘active’ to its class.
Also, notice we give a callback function onPageChange. If a user clicks this link, we give the number back to the parent component movies.jsx so that it can change its showingArr.
Changing Paginated Movies UI
If you want to change the paginated movies that are shown, simply give it a different page number and it will calculate the starting and ending pagination number for you. Remember to give the array back to showingArr so that render will work. We pass function handlePageChange into the prop of Pagination component as described above. When the user clicks a link in Pagination component, that number will come back to this function as page parameter. We then change our showingArr accordingly.
movies.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
handlePageChange = page => { // causes new rendering. // whenever we render, currently, we render all movies // what we need to do is to render only certain # of pages // in accordance to our currentPage // we render according to our currentPage // if its currentPage is 1, [1,2,3,4] // if currentPage is [5, 6, 7, 8] this.setState({ currentPage: page, showingArr: this.#paginateMovies(page), }) } |
Changing Genre
We need a selectedGenre property in our state object to keep track what genre the user have selected.
movies.jsx
1 2 3 4 |
state = { ... selectedGenre: null, } |
Changing genre requires us to get all the movies associated with this genre. We use getMoviesByGenreName to get the movies by genre name.
We need to update our movies with this newly list of movies. This way, the total # of movies will appear correctly, and also paginateMovies depend on this movies array to show the paginated results. We then update selectedGenre to keep track of what genre we’re on.
Finally, all that is done, we update showingArr and paginate the new list of movies.
movies.jsx
1 2 3 4 5 6 7 8 9 10 11 12 |
handleGenreSelect = genre => { let movies = moviesData.getMoviesByGenreName(genre.name); this.setState({ selectedGenre: genre, movies, currentPage: 1 }, () => { this.setState({ showingArr: this.#paginateMovies(1) }) }) } |
Genre List
So remember in our GPMovieDataClass, we filter out all the unique genres from our movies test data. We then implemented getGenreAsArrayOfObjects
to get the list of genres as full objects. We pass this list into our ListGroup to render the genres via items.
We then map through the items and display the names and unique ids for these genres.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import React, { Component } from 'react'; const ListGroup = props => { const { items, textProperty, valueProperty, selectedItem, onItemSelect, noGenre } = props; let classes = 'list-group-item d-flex justify-content-between align-items-center'; let selectedGenreName = selectedItem && selectedItem.name; return (<div> <ul className="list-group"> {items.map(item => { return(<li key={item[valueProperty]} onClick={() => onItemSelect(item)} className={item.name === selectedGenreName ? classes + ' active' : classes}> {item[textProperty]} </li>); })} <li key='999' className={classes + (!selectedGenreName?' active':'') } onClick={()=>noGenre()}> All Genres </li> </ul> </div>); } ListGroup.defaultProps = { textProperty: 'name', valueProperty: "_id", }; export default ListGroup; |
Notice we used defaultProps for property names. This is because its cleaner for us to specify the property names we’re looking for in the genre objects.
For putting ‘active’ in the className to specify the active selected genre, we already selectedGenre in movies component state object. That way, when we select a link here, it gives the genre name back up to movies component via onItemSelect. Movies component then puts the string into selectedGenre to keep track.
Type Checking
Notice in Pagination component, we have propTypes object that can let you specify the ‘type’ of the property. For example, in itemCount, we specify that it must be a number and is required.
1 2 3 4 5 6 |
Pagination.propTypes = { itemsCount: PropTypes.number.isRequired, pageSize: PropTypes.number.isRequired, currentPage: PropTypes.number.isRequired, onPageChange: PropTypes.func.isRequired, } |
This means that in Movies Component, if you do something like this:
1 2 3 4 5 6 |
<Pagination itemsCount="abc" pageSize={pageSize} currentPage = {currentPage} onPageChange={this.handlePageChange} /> |
You’ll get an error:
This ensures that we check the type of the prop we inject.
In order to use this, make sure you install and import prop-types.
1 |
import PropTypes from 'prop-types'; |
You can also specify the default values you want your prop types to be:
1 2 3 4 |
ListGroup.defaultProps = { textProperty: 'name', valueProperty: "_id", }; |
in our case, we use textProperty and valueProperty prop passed in from Movies component to specify the property names to retrieve from each Genre object.
We give it a default to say that we should look for ‘name’ and ‘_id”. This can save space when you are putting all these props into components which makes it look messy: