After cloning this directory, run npm start
from inside of the reactify-card
directory. Now when you visit http://localhost:3000
, you will see three different Cards, the source code for which can be found in ./reactify-card/src/StaticCards.js
.
For the most part, these cards have been marked up in standard HTML, but with some minimal changes to make them play nicely within a React project.
- Each card has been put into its own stateless functional component, which are
export
ed out ofStaticCards.js
andimport
ed intoApp.js
. - Image assets are imported and given variable names at the top of the file, and then referenced like so:
<img src={imgCourseCard} />
(In React, curly braces are used when dealing with variables). - React uses
className
in its markup rather thanclass
(Many of the class names in use are provided viatachyons-egghead
, a compilation of customized Tachyons styles)
To start to DRY (Don't Repeat Yourself) out our markup, we'll look for className
s in our markup that are being reused multiple times, and replace them with variable assignments. This is good to do, because if we decide to change something later on, we only have to change it in one place.
To start, notice that all of the cards begin with <div className='relative card ...'>
. We can create a constant commonCardClasses
for these two classes at the top of our file:
const commonCardClasses = 'relative card'
After pulling out the common classes, I like to use ES6 Template Literals to interpolate the other required classes into a single variable. So, looking at our three card examples, I come up with the following:
const courseCardClasses = `${commonCardClasses} card-stacked-shadow card-course`
const lessonCardClasses = `${commonCardClasses} card-lesson`
const playlistCardClasses = `${commonCardClasses} card-stacked-shadow sans-serif card-playlist`
Now that we've declared variables for each, we can replace the repeated strings. Remember to use curly braces instead of quotes since we're using variables. After you've replaced these classes, the cards should look exactly the same.
We can give the same treatment to the common "inner" card classes. All three of our example cards have the following "inner" card classes:
flex flex-column items-center br2 bg-white navy relative z-1 card-course-inner
Our Course and Lesson cards also feature these additional inner classes:
overflow-hidden pa4 pointer
Following the pattern of above, we will create variables for our common inner classes.
const commonInnerClasses = 'flex flex-column items-center br2 bg-white navy relative z-1 card-course-inner'
const enhancedInnerClasses = `${commonInnerClasses} overflow-hidden pa4 pointer`
The Course and Lesson cards have additional classes that enhance the common inner classes, hence the name. Sometimes naming things is hard.
There are many more areas where classNames
are repeated: The button that shows when hovering over a card, the title and author typography, the card footers... Following the pattern of replacing instances of duplication with a resuable variable goes a long way in cleaning up presentational markup!
We've cleaned up our code quite a bit by extracting repeatedly used classes into varaibles, but now the markup for our play button variations look like this:
<div className={playBtnClasses}></div>
<div className={hoverPlayBtnClasses}></div>
We know from our repeated className
s that we have a base button with a variation applied to it. With that in mind, we will create a <PlayButton />
component with a hover
variant that will be activated via a prop passed into the component.
In a new file called PlayButton.js
, we will migrate over the appropriate lines of code from StateCards.js
. Since our component will make use of props, we need to add PropTypes
as a destructured import from React:
import React, { PropTypes } from 'react'
const commonPlayBtnClasses = 'fa fa-play w3 h3 f3 absolute z-1 gray items-center justify-center br-pill pointer card-play-btn'
const hoverPlayBtnClasses = `${commonPlayBtnClasses} bg-white-70 o-0`
const playBtnClasses = `${commonPlayBtnClasses} hover-turquoise bg-white`
With our button prerequisites in place, we can write the stateless functional component for the PlayButton
. The hover
prop is a boolean that we will set to be false by default, meaning that we will have to specify if we want to use that variation.
Recall that our button markup was in a single <div>
, so all we need to return is a self-closing <div />
with the appropriate className
s applied.
Since we have only two variations of the PlayButton
we will use a ternary statement to decide which of our className
variables to use. Our ternary statment will say "if this is the hover
variation, use the hoverPlayBtnClasses
, otherwise use the regular playBtnClasses
".
We can write our ternary statement directly into the className
declaration inside of the <div />
we are returning.
const PlayButton = ({ hover=false }) => {
return <div className={hover ? hoverPlayBtnClasses : playBtnClasses} />
}
PlayButton.propTypes = {
hover: PropTypes.bool
}
Now that our PlayButton
component is complete, we will make it the default export for our file:
export default PlayButton
With this file complete, we can now go back to our StaticCards.js
file and import our newly created PlayButton
:
// inside StaticCards.js
import PlayButton from './PlayButton'
We can also remove the class name variables related to the PlayButton
.
In order to use our new component, we just replace our previous markup like so:
// Old:
<div className={playBtnClasses}></div>
// New:
<PlayButton />
Since the hover
prop on our PlayButton
is a boolean, if we want to use that button variation, we can simply add the word to use that variation:
<PlayButton hover />
One component extracted, several more to go!
In order to form our plan of attack, let's do the component hierarchy excercise featured in Facebook's "Thinking in React" article. This exercise is useful not only to help you visualize how your components fit together, but also to help you come up with names for the subcomponents.
This can be done with any software that allows you to annotate an image (I'm using Preview, that ships by default in Mac OS).
The first step is to break each card into common parts, and then drill down from there. Each Card has the same basic attributes (e.g. white background, rounded corners), and will have its sections annotated in different colors. In this case, I'm using neon pink for the card's header, neon green for the body, and neon blue for the footer.
In our Lesson Card, the header is empty, so no subcomponent will be needed. Our Course Card has an image, so we know we will need a component for that. I've highlighted it in orange.
Our Playlist Card has a lot going on, and will need to host an entire set of subcomponents. Let's take a closer look.
I've drawn a purple square around the area inside the header that the playlist will take up. Examining the inside of the playlist, I can see several line items, each representing a video in the list. I know that we will need subcomponents for each of these, so I'll separate them with a dashed purple line. Now that we can see them sliced horizontally, we have different vertical lines to draw as well, separating the playlist status icon, a language type icon, the video's name, and the video length.
Below the Playlist Entries, we have the PlayButton
component outlined in dark green, even though we've already created it.
Finally, underneath the PlayButton
we have a summary of the remaining time in the playlist.
It can be helpful to look at the hierarchy as a tree. We'll revisit this as we build out the subcomponents.
CardHeader
HeaderImage
Playlist
PlaylistItem
PlayedStatus
(conditional classes on the<li>
)CategoryIcon
VideoTitle
VideoLength
PlayButton
PlaylistSummary
The body of the card is simple, and contains two subcomponents: a title, and the author.
CardBody
CardTitle
CardAuthor
The footer is split between an indicator of the type of material the card represents, and statistics or metadata about the material-- such as number of videos and length.
Outlined in maroon, the MaterialType
features a pill with different styling applied depending on if the material is a course, lesson, or a playlist.
The metadata for the material has been sliced into subcomponents by yellow lines, and again, presentation depends upon the type of material.
CardFooter
MaterialType
MaterialMeta
LessonCount
CompletedLessonCount
ProgressBar
LessonLength
LessonTypeIcon
In the last section, we created a component hierarchy for our cards, and now we will start creating a reusable Card
component that will eventually replace our StaticCard
examples.
First, we'll create a new file called Card.js
that will contain our code. Then we'll copy and paste our React and index.css imports out of StaticCards.js
into our new file. We can also bring over our commonCardClasses
and commonInnerClasses
variables, since we'll be using them from the start.
import React from 'react'
import './assets/index.css'
const commonCardClasses = 'relative card'
const commonInnerClasses = 'flex flex-column items-center br2 bg-white navy relative z-1 card-course-inner'
With our minimum imports and variables in place, we can scaffold our card component. Like our examples in StaticCards.js
, our new Card
component will be a stateless functional component that returns an outer div
with our commonCardClasses
class names, and inside of it an inner div
with our commonInnerClasses
class names. We will export Card
so we can use it in other files.
export const Card = () => {
return (
<div className={commonCardClasses}>
<div className={commonInnerClasses}>
</div>
</div>
)
}
We need to update index.js
to import our new Card
component so we can preview it as it changes. We'll start by doing a destructured import of Card
from the file we just created:
import { Card } from './Card'
Now let's add a div
above our StaticCourseCard
component so we can preview our Card
as we build it. I'm going to add the class mt5
to the div
containing StaticCourseCard
so we can keep our spacing consistent.
// inside ReactDOM.render
...
<div>
<Card />
</div>
<div className='mt5'>
<StaticCourseCard />
</div>
...
Now that we have our pieces in place to have a live preview of our Card
component as we develop it, we can refresh our page at http://localhost:3000/
, and we see... nothing.
That's because we haven't actually put anything in our new Card
yet.
Inside of each of our example cards inside of StaticCards.js
, we have the same title h3
and author name div
in each of our cards. Let's copy and paste these two lines out of StaticCourseCard
and into our Card
component inside of the inner div
of Card.js
. We will also need to bring over our titleHeadingClasses
and authorNameClasses
variables.
// inside our Card component
...
<div className={commonInnerClasses}>
<h3 className={titleHeadingClasses}>Introduction to RxJS Marble Testing Two lines headline</h3>
<div className={authorNameClasses}>Joe Maddalone</div>
</div>
...
After saving the file, our preview app will reload, and we'll see that we have our title and author lines being displayed in a really wide Card (don't worry, we'll tackle the styling later).
Our first pass at the CardBody
component will contain the same hardcoded title and author as our examples. We'll start by declaring another stateless functional component called CardBody
that doesn't take any parameters (yet!) and returns a div
containing our title h3
and author div
lines.
A couple things of note: we're not using the export
keyword for our CardBody
because we aren't going to use it in any other files. Also, we need to surround our title and author lines inside of a div
because "adjacent JSX elements must be wrapped in an enclosing tag" (I can't say it better than the error message!)
const CardBody = () => {
return (
<div>
<h3 className={titleHeadingClasses}>Introduction to RxJS Marble Testing Two lines headline</h3>
<div className={authorNameClasses}>Joe Maddalone</div>
</div>
)
}
Now that we've created our CardBody
component, we can replace these two lines in our Card
component:
...
<div className={commonCardClasses}>
<div className={commonInnerClasses}>
<CardBody />
</div>
</div>
...
As expected, our new Card
preview looks the same, which means it worked! Now let's make the title and author display what is passed to them via props.
To alter our CardBody
component to accept props for title and author, we'll first need to add PropTypes
as a destructured import from React at the top of our file:
import React, { PropTypes } from 'react'
Now below our CardBody
component, we add a declaration of the prop names & types that the component should expect. Title & author will both be strings, and we'll set both of them to be required.
CardBody.propTypes = {
title: PropTypes.string.isRequired,
author: PropTypes.string.isRequired
}
The next step is to update the CardBody
component to actually accept these props. We'll start by destructuring title
and author
from the parameters passed into the functional component. Then we replace our hardcoded strings with our variables surrounded by curly braces.
const CardBody = ({title, author}) => {
return (
<div>
<h3 className={titleHeadingClasses}>{title}</h3>
<div className={authorNameClasses}>{author}</div>
</div>
)
}
With that change in place, we now need to update our <CardBody />
inside of the Card
component to pass title
and author
props:
...
<CardBody title='Test Title' author='Test Author' />
...
After saving the file, the preview of our new Card
component should now display "Test Title" and "Test Author".
Now that we know our CardBody
component is working, let's refactor our code to allow us to pass the title
and author
through our Card
component. We'll do this by following pretty much the same process that we just did: Add propTypes
to our Card
component, have it destructure title
and author
from its params.
Once that's done, we'll remove the hardcoded 'Test Title'
and 'Test Author'
strings and replace them with our new prop variables.
export const Card = ({title, author}) => {
return (
<div className={commonCardClasses}>
<div className={commonInnerClasses}>
<CardBody title={title} author={author} />
</div>
</div>
)
}
Card.propTypes = {
title: PropTypes.string.isRequired,
author: PropTypes.string.isRequired
}
Saving the file at this point, our Card
preview again updates to be invisible. This is because we need to pass title
and author
props in when we instantiate our Card
.
In our index.js
file where we are rendering our exmaple cards, I'm going to create an object variable above our call to ReactDOM.render
that will hold our sample data. For illustrative purposes, I'm going to replicate our example cards exactly, so I'll just copy and paste the title and author strings we've already been using.
const testData = {
title: 'Introduction to RxJS Marble Testing Two lines headline',
author: 'Joe Maddalone'
}
With our test data in place, we can now update our Card
component to make use of the title
and author
props:
<Card title={testData.title} author={testData.author} />
After we save the file, our card is back to showing us the wide preview of our Card
, but this time without the values having been hardcoded.
We've made progress on a generic Card
component with a CardBody
subcomponent, but now we will turn our attention toward creating our CourseCard
, LessonCard
, and PlaylistCard
components.
In the first couple stages of this project, we went through our markup and extracted common and unique className
s for each of our cards. Each of our Card types has its own outer, inner, and footer styles.
In order to keep ourselves organized, we'll set up a master cardTypes
object to hold the settings for each of our Card
variations.
One of the tools we'll use to help us is keys
from the lodash
library. We need to add it to our project...
npm install --save lodash
...and then import it at the top of Cards.js
:
import { keys } from 'lodash'
Putting together our cardTypes
object should be pretty straight forward-- we will have a key for each type (course
, lesson
, playlist
). For now, each entry will track which cardClasses
and innerClasses
each type of Card
will use. We will add to this master object as our work continues.
We'll build our cardTypes
object in our Card.js
file, and fill it out using the additional class names from each card in StaticCards.js
.
Since each of our Card types use their own classes in addition to the commonCardClasses
, we'll just migrate each of these away from their separate variables in our StaticCards.js
example file into their respective cardClasses
entry in our cardTypes
object. As far as innerClasses
go, we'll set the value to be a string template for the appropriate variable.
const types = {
'course': {
'cardClasses': `${commonCardClasses} card-stacked-shadow card-course`,
'innerClasses': `${enhancedInnerClasses}`
},
'lesson': {
'cardClasses': `${commonCardClasses} card-lesson`,
'innerClasses': `${enhancedInnerClasses}`
},
'playlist': {
'cardClasses': `${commonCardClasses} card-stacked-shadow sans-serif card-playlist`,
'innerClasses': `${commonInnerClasses}`
}
With our types
object set up, we can move on to updating our Card
component.
In our Card.propTypes
declaration, we'll add a type
key below our title
and author
propTypes. Our new type
prop will be one of the keys present in our types
object. To translate this into the form that React understands, we'll write it as:
type: PropTypes.oneOf(keys(types))
We'll start by adding type
as one of the destructured parameters where we create the Card
component. With this in place, we can refactor our className
s to perform a lookup in our cardTypes
object using the type
we've passed in to Card
to choose the correct classes.
Our Card
component should now look like this:
export const Card = ({title, author, type}) => {
return (
<div className={cardTypes[type]['cardClasses']}>
<div className={cardTypes[type]['innerClasses']}>
<CardBody title={title} author={author} />
</div>
</div>
)
}
Card.propTypes = {
title: PropTypes.string.isRequired,
author: PropTypes.string.isRequired,
type: PropTypes.oneOf(keys(cardTypes))
}
In order to see the changes we've made, we need to update index.js
to pass a type
into our Card
component:
// Inside index.js
<Card title={testData.title} author={testData.author} type='course' />
Our Course card is starting to take shape! However, since we only have a few different types of Card, it would be nice to not have to pass a type
every time and just instantiate the type of card we want right off the bat.
At the bottom of our Card.js
file, we'll export stateless functional components for each of our Card types. They'll all follow roughly the same layout, with the name and type
adjusted appropriately:
export const CourseCard = ({title, author}) => {
return (
<Card title={title} author={author} type='course' />
)
}
Jumping back to our App's index.js
file, we'll adjust our import to bring in our separate Cards, and remove the generic Card
:
import { CourseCard, LessonCard, PlaylistCard } from './Card'
We also will adjust our our rendered preview to display all three of our cards:
// inside the ReactDOM.render return
<div>
<CourseCard title={testData.title} author={testData.author} />
</div>
<div className='mt5'>
<LessonCard title={testData.title} author={testData.author} />
</div>
<div className='mt5'>
<PlaylistCard title={testData.title} author={testData.author} />
</div>
Now when we look at our example app, we can see our three dynamic Card examples are on their way to looking like their static counterparts!
The first step in creating our footer is to copy the footerClasses
and all pill
-related class name variables from our StaticCards.js
file into our Card.js
file.
Now that we have these variables in place, we will create a stateless functional component for our Footer
that itself will contain stateless functional components for the MaterialMeta
and MaterialType
components.
Since our MaterialType
component will be easier to implement, we'll create it first.
Before we create the Footer
component, we need to create something to put in it. The MaterialType
component will be the colored pill that displays the card's type.
Start by adding the appropriate pillClasses
to our master cardTypes
object: orange for course, blue for lesson, and green for playlist.
'course': {
'cardClasses': `${commonCardClasses} card-stacked-shadow card-course`,
'innerClasses': `${enhancedInnerClasses}`,
'pillClasses': `${orangePillClasses}`
}
...
With the class names in place, we'll set up our MaterialType
component to take a single prop for type
, and return a <div>
with the appropriate className
applied. Remember to include type
in the propTypes
declaration!
const MaterialType = ({type}) => {
return (
<div className={cardTypes[type]['pillClasses']}>{type}</div>
)
}
MaterialType.propTypes = {
type: PropTypes.string.isRequired
}
Our CardFooter
Component follows a similar pattern-- it will take in meta
and type
props, and return a <div>
with the common footerClasses
applied. Inside of the div
, we will pass each of the props to their respective subcomponent (again, just the MaterialType
component for now).
const CardFooter = ({meta, type}) => {
return (
<div className={footerClasses}>
<MaterialType type={type} />
</div>
)
}
CardFooter.propTypes = {
meta: PropTypes.string,
type: PropTypes.string.isRequired
}
With the footer created, we need to update the Card
component to accept meta
as a prop type & argument, and include our new CardFooter
subcomponent, passing it meta
and type
props.
export const Card = ({title, author, type, meta}) => {
return (
<div className={cardTypes[type]['cardClasses']}>
<div className={cardTypes[type]['innerClasses']}>
<CardBody title={title} author={author} />
<CardFooter type={type} meta={meta} />
</div>
</div>
)
}
Card.propTypes = {
title: PropTypes.string.isRequired,
author: PropTypes.string.isRequired,
type: PropTypes.oneOf(keys(cardTypes)),
meta: PropTypes.string
}
Now when we save our work, our App will reload and show us that we have a footer with our card indicator.
In order to display meta data on our cards, we should start by creating some.
For the purposes of this demo, we'll just add some to our testData
in our index.js
file, using the stuff we can see in our StaticCard
examples as a guide. We'll also import our image assets for passing to the cards.
import imgCourseCard from './assets/img-course-card.png'
import imgJs from './assets/js.svg'
import imgRx from './assets/rx.svg'
const testData = {
title: 'Introduction to RxJS Marble Testing Two lines headline',
author: 'Joe Maddalone',
meta: {
courseImg: imgCourseCard,
langImg: imgJs,
lessonCount: 12,
currentLesson: 7,
lessonsLeft: 5,
timeRemaining: '14:34',
videoLength: '22:22',
playlist: [
{
watched: true,
current: false,
icon: imgRx,
title: 'First Video',
length: '01:11'
},
...
With our metadata in place, we can pass it in as the meta
prop on each of our Card
variations, like so:
// inside index.js
<CourseCard title={testData.title} author={testData.author} meta={testData.meta} />
Now we need to update each of our individual Card
components to use the meta
prop. For example:
export const CourseCard = ({title, author, type, meta}) => {
return (
<Card title={title} author={author} type='course' meta={meta} />
)
}
Since each Card type has a different set of metadata that it displays, we are going to create a new stateless functional component for each. When they've been created, we'll add each of them to the appropriate type in our cardTypes
object so we can call them up later.
Our CourseCard
is simply a count of the number of lessons. Going off of the markup in StaticCard.js
's example, we can see we only need a single <div>
with some class names. Inside, we'll use curly braces to display meta.lessonCount
, and then the word "lessons". However, in case there's only one lesson, we'll use a ternary statement to determine if we are going to pluralize or not. Remember to add meta
as a required PropType.
const CourseMeta = ({meta}) => {
return (
<div className='f6 dark-gray o-50'>
{meta.lessonCount} {meta.lessonCount === 1 ? 'lesson' : 'lessons'}
</div>
)
}
CourseMeta.propTypes = {
meta: PropTypes.object.isRequired
}
In order to make our new CourseMeta
component work when looked up in our cardTypes
object, we'll create a key for metaComponent
nested inside of our course
key's object, and have the value for metaComponent
be an arrow function that takes meta
as a parameter and returns our CourseMeta
component with meta
as the prop value.
The reason we do this is because our CourseMeta
component is dynamic based on its props, and we have to be able to pass in our meta
object.
const cardTypes = {
'course': {
'cardClasses': `${commonCardClasses} card-stacked-shadow card-course`,
'innerClasses': `${enhancedInnerClasses}`,
'pillClasses': `${orangePillClasses}`,
'metaComponent': (meta) => <CourseMeta meta={meta} />
},
Creating these components will be much the same as the process we just followed, with some differences of note with our PlaylistMeta
component.
Our static mockup of the PlaylistMeta
card has different classes for the footer than our other cards, so we need to modify cardTypes
object to include a footerClasses
key for our playlist
Card, and then update our CardFooter
subcomponent to look for these additional classes.
const CardFooter = ({meta, type}) => {
const metaComponent = cardTypes[type].metaComponent ? cardTypes[type].metaComponent(meta) : null
return (
<div className={`${footerClasses} ${cardTypes[type]['footerClasses']}`}>
{metaComponent}
<MaterialType type={type} />
</div>
)
}
Let's start with a CardHeader
stateless functional component modeled after our CardFooter
. It will be a stateless functional component that will look up the type
in our cardTypes
object, and return the appropriate headerComponent
, which we will create and add to the cardTypes
entry.
const CardHeader = ({meta, type}) => {
const headerComponent = cardTypes[type].headerComponent ? cardTypes[type].headerComponent(meta) : null
return (
<div>
{headerComponent}
</div>
)
}
Now we can add it to our Card
component:
// inside the `Card` component
<div className={cardTypes[type]['innerClasses']}>
<CardHeader type={type} meta={meta} />
<CardBody title={title} author={author} />
<CardFooter type={type} meta={meta} />
</div>
Our CourseHeader
component will contain an image that we'll get from meta
, along with our PlayButton
component that we created earlier. We can just copy the structure and styles from the example in StaticCards.js
. After we create the component, we'll add it to the course
section in our cardTypes
object.
const CourseHeader = ({meta}) => {
return (
<div>
<PlayButton hover />
<div className='mw5 mt3 center ph3'>
<img alt='' src={meta.courseImg} />
</div>
</div>
)
}
CourseHeader.propTypes = {
meta: PropTypes.object.isRequired
}
const cardTypes = {
'course': {
'cardClasses': `${commonCardClasses} card-stacked-shadow card-course`,
'innerClasses': `${enhancedInnerClasses}`,
'pillClasses': `${orangePillClasses}`,
'metaComponent': (meta) => <CourseMeta meta={meta} />,
'headerComponent': (meta) => <CourseHeader meta={meta} />
},
...
Since we've already laid the groundwork for our CardHeader
, as soon as we save the file our App should update to show us the header image.
Our LessonHeader
component is much the same as our CourseHeader
, except it will only return the PlayButton
component.
There's a lot more going on in the header of our PlaylistCard
than any of the others (i.e. there's a whole playlist there!)... but in the meantime, we can add the PlayButton
and remaining time subcomponents.
Like the others, we'll start with a stateless functional component with a destructured meta
parameter. Inside of our function, we'll destructure variables for timeRemaining
and lessonsLeft
from meta
in order to render our playlist time left info. Looking at our mockup in StaticCards.js
, we need to transfer over the class names and inline style from the <div>
surrounding our PlaylistButton
and the playlist entries.
Inside of this new <div>
and below our <PlayButton />
, we'll add a new PlaylistSummary
component.
This component is yet another stateless functional component, and will take the timeRemaining
and lessonsLeft
from meta
, and we will again be making use of a ternary statement to determine if we will be pluralizing the word "lesson" or not.
const PlaylistSummary = ({timeRemaining, lessonsLeft}) => {
return (
<div className='ph4 pt5'>
<div className='tc f6 lh-title light-gray'>
{`${timeRemaining} to go (${lessonsLeft} more ${lessonsLeft === 1 ? 'lesson' : 'lessons'})`}
</div>
</div>
)
}
With PlaylistSummary
created, we can add it to the PlaylistHeader
component:
const PlaylistHeader = ({meta}) => {
const { timeRemaining, lessonsLeft } = meta
return (
<div>
<div className='relative w-100' style={{
height: '290px'
}}>
<PlayButton />
</div>
<PlaylistSummary timeRemaining={timeRemaining} lessonsLeft={lessonsLeft} />
</div>
)
}
Again, remember to declare propTypes for both of our new components.
Saving the file, you should see our PlaylistHeader
is starting to look like the mockup.
We'll start by creating a new Playlist.js
file along with an import for React and PropTypes
.
Recall our Thinking in React exercise where we planned our components. Our planned Playlist
hierarchy looked like this:
Playlist
PlaylistItem
CategoryIcon
VideoTitle
VideoLength
PlayButton
PlaylistSummary
We've already created the PlaylistSummary
and the PlayButton
, so let's do the PlaylistItem
and its subcomponents (each of which being, you guessed it, stateless functional components).
The meta
object we've been passing around contains an array called playlist
that contains objects with the data we need to create our PlaylistItem
components.
With this in mind, we know that our new Playlist
component will take the playlist
array as a prop, and in turn each item
object in the array will be passed to our new PlaylistItem
component.
Like before, we will pull our classNames from the Playlst Card example in StaticCards.js
. Looking at the example, we can tell that our Playlist
component will contain the <div>
and <ul>
, then inside of the <ul>
we will create <li>
s for each of our PlaylistItem
s.
Since we don't know what or how many PlaylistItem
s we will need, we will call the .map()
method on our playlist
prop. The map()
method takes an arrow function with two paramaters: the first is the item i
in the array, and the second paramater k
is the index of the item. We'll use the index for the key
prop that React uses to help it determine if an item needs updated.
Our arrow function will return a PlaylistItem
component, passing i
as the item
prop, and k
as the key
prop. Remember that since our arrow function doesn't have curly braces, we don't need to use the return
keyword.
const Playlist = ({playlist}) => {
return (
<div className='pr3 pt3 bg-tag-gray self-stretch h-100 br2 overflow-y-scroll'>
<ul className='list pa0 ma0 overflow-hidden card-progress-list'>
{playlist.map((i, k) => <PlaylistItem item={i} key={k} />)}
</ul>
</div>
)
}
We already know that our PlaylistItem
has an item
prop, and will return an <li>
. There will be different classes applied to each item based on its "played" status. Let's start by setting that up.
Looking at our example mockup in StaticCards.js
, we can see the same set of classes used for every item, with additional classes added for already played items or the item that will be started. For easy access, we'll destructure the watched
and current
keys from the item
, and then use ternary statements for each inside of a string template to fill out our <li>
's classNames
:
const PlaylistItem = ({item}) => {
const { watched, current } = item
const liClasses = 'flex items-start relative f6 lh-solid pointer pv3 pl4 pr3 gray hover-bg-white card-progress-list-item'
const watchedClasses = 'viewed'
const currentClasses = 'next'
return (
<li className={`${liClasses} ${watched ? watchedClasses : null} ${current ? currentClasses : null}`}>
</li>
)
}
The first child inside of our <li>
is a CategoryIcon
. This component will take in the image as a prop (that will be passed after being destructured inside of our PlaylistItem
), and return a simple <img />
tag with some class names applied:
const CategoryIcon = ({icon}) => {
return <img src={icon} className='ml2 mt1' alt='' />
}
The last subcomponents are the VideoTitle
and VideoLength
displays, both of which are straight forward:
const VideoLength = ({length}) => {
return <div className='w3 ml3 tr o-60'>{length}</div>
}
const VideoTitle = ({title}) => {
return (
<div className='truncate'>
{title}
</div>
)
}
And adding our classes, we end up with our finished component:
const PlaylistItem = ({item}) => {
const { watched, current, icon, title, length } = item
const liClasses = 'flex items-start relative f6 lh-solid pointer pv3 pl4 pr3 gray hover-bg-white card-progress-list-item'
const textClasses = 'ml2 flex justify-between flex-grow-1 lh-copy overflow-hidden lesson-title'
const watchedClasses = 'viewed o-60'
const watchedTitleClasses = 'o-60'
const currentClasses = 'next'
return (
<li className={`${liClasses} ${watched ? watchedClasses : null} ${current ? currentClasses : null}`}>
<CategoryIcon icon={icon} />
<div className={`${textClasses} ${watched ? watchedTitleClasses : null}`}>
<VideoTitle title={title} />
<VideoLength length={length} />
</div>
</li>
)
}
Now we've completed all of our Playlist subcomponents, but for the time being we have half of our Playlist-related code in Cards.js
, and half in Playlist.js
. Let's do a little cleanup and refactoring.
Start by moving PlaylistCard
, PlaylistMeta
, PlaylistSummary
, and PlaylistHeader
over into Playlist.js
.
We'll now need to add the export
keyword to PlaylistMeta
and PlaylistHeader
so we can import them into Card.js
:
import { PlaylistCard, PlaylistMeta, PlaylistHeader } from './Playlist'
We also need to move our PlaylistCard
import in our index.js
file to be from ./Playlist
instead of ./Card
.
With all of our playlist-related code all in the same file, all that's left for now is to add our Playlist
component into its place on the line below <PlayButton />
in our PlaylistHeader
component.
export const PlaylistHeader = ({meta}) => {
const { timeRemaining, lessonsLeft } = meta
return (
<div>
<div className='relative w-100' style={{
height: '290px'
}}>
<PlayButton />
<Playlist playlist={meta.playlist} />
</div>
<PlaylistSummary timeRemaining={timeRemaining} lessonsLeft={lessonsLeft} />
</div>
)
}
Splitting the code for our other specific cards will follow a process much the same to what we just did with our Playlist
file.
We'll start by creating a new file Course.js
in our src
directory. At the top of our file, we'll need to import React, our PlayButton
component, and Card
.
import React, { PropTypes } from 'react'
import PlayButton from './PlayButton'
import { Card } from './Card'
With our imports in place, we can copy and paste the code for CourseMeta
, CourseHeader
, and CourseCard
from Card.js
into our new file. We also will add the export
keyword to all three.
Now that we've exported our components, we need to adjust the places they are imported.
Inside of Card.js
, we need to do a destructured import of CourseMeta
and CourseHeader
, and inside of index.js
, we need to import CourseCard
:
// Card.js
import { CourseMeta, CourseHeader } from './Course'
// index.js
import { CourseCard } from './Course'
These steps are exactly the same as above, but replace the word "Course" with "Lesson". For the sake of lowering cognitive overhead, I'll go ahead and do that below:
We'll start by creating a new file Lesson.js
in our src
directory. At the top of our file, we'll need to import React, our PlayButton
component, and Card
.
import React, { PropTypes } from 'react'
import PlayButton from './PlayButton'
import { Card } from './Card'
With our imports in place, we can copy and paste the code for LessonMeta
, LessonHeader
, and LessonCard
from Card.js
into our new file. We also will add the export
keyword to all three.
Now that we've exported our components, we need to adjust the places they are imported.
Inside of Card.js
, we need to do a destructured import of LessonMeta
and LessonHeader
, and inside of index.js
, we need to import LessonCard
:
// Card.js
import { LessonMeta, LessonHeader } from './Lesson'
// index.js
import { LessonCard } from './Lesson'
Now that we have added all three of our dynamic card components into our App's index.js
file, we can delete the StaticCard
examples and the div
s that contain them. Upon saving the file, our App will reload, and we are left with cards that look strikingly similar to our source mockups!