Lesson 2
Forms - apply to work for uncle snoop
Agenda:
$ cd ~/code/react-course
$ git clone https://github.com/nikfrank/react-course-workbook-2
$ cd react-course-workbook-2
$ yarn
$ npm start
Here we're going to do a bunch of super useful examples of getting data from users!
But to jazz it up, we're making an application form for a job in tha dogg pound
we learned already that everything the user sees has to be from the state.
So in React every time the user types even one character, we need that in the state - only then it can render to the user.
To take text input from a user, we're going to make an <input/> rendering a value prop from .state and an onChange eventHandler function to react to typing
This pattern is standard in React and goes by the name: controlled input pattern
./example/App.js
class App extends Component {
state = { someWord: '' }
// event.target.value is where React sends our new value
// that's what we'll save to this.state.someWord
wordChanged = (event)=> this.setState({ someWord: event.target.value })
render(){
const someWord = this.state.someWord;
return (
<input value={someWord} onChange={this.wordChanged} />
);
}
}Data comes in through the onChange eventHandler, and only renders to the input box if our eventHandler function saves the new value into the .state
React only lets us make changes to state using a Component's this.setState(...) function
Our onChange eventHandler function this.wordChanged(...) updates our .state on every text input change.
Then we render the new value into <input value={ HERE }/> the input's value prop.
Focus in!
this.setStateis a function- we can call it like
this.setState() - but of course we pass in an object we wish to merge onto
stateby doingthis.setState({ someWord: event.target.value })
We'll use this same pattern for inputs of all sorts
- dropdowns
<select value={country} onChange={this.setCountry}>...options...</select> - inputs from npm modules like
<DatePicker value={day} onChange={this.setDay}/> - custom Components we'll write (who will call
this.props.onChange(...)from inside)
ok, the actual lesson:
$ git checkout start
here we're going to put an initial state to render some values into our inputs
and we'll bind our submit button's onClick to a function that logs out the state
we'll worry about implementing onChange handlers later - our goal here is just to confirm we have the basic first step in React.
we already have an inital state, it's just empty! where we had
./src/App.js
class App extends Component {
state = {}
//...
}let's put in some initial values for the .rapName and .albumSales to render
./src/App.js
class App extends Component {
state = {
rapName: 'Mikey G',
albumSales: 420,
}
//...
}and where we have
./src/App.js
<input placeholder='Rap Name'/>let's render those .state values into our <input value={...}/>s
./src/App.js
<input placeholder='Rap Name' value={this.state.rapName}/>similarly for the .albumSales <input/>'s value={...} prop
After the form is filled, we need to respond to the "submit" button's onClick for the user to complete their work.
where we have
./src/App.js
<button>Submit</button>we can write
./src/App.js
<button onClick={this.submitForm}>Submit</button>and so we'll also need a submitForm function
./src/App.js
class App extends Component {
state = {
rapName: 'Mikey G',
albumSales: 420,
}
submitForm = ()=> {
console.log( 'send this to the API', this.state );
console.log( 'using APIs is covered in workbook 4!' );
}
render(){
return (
<div>
//...
<button onClick={this.submitForm}>Submit</button>
</div>
);
}
}we can see the state of our form logged in the console (Ctrl Shift i / command option j) when we hit submit
next up, user input!
if you're starting from this step $ git checkout step0 git cheat sheet
Snoop wants the user's input to be set in the .state so it can be rendered back to the user (which is what they expect to happen!)
just like we saw in the concepts section, we'll be adding onChange callback props to all of our inputs
where we have
./src/App.js
<input placeholder='Rap Name' value={this.state.rapName}/>we can bind our onChange handler (which doesn't exist yet)
./src/App.js
<input placeholder='Rap Name'
onChange={this.setRapName}
value={this.state.rapName}/>and so we'll need to write our this.setRapName eventHandler as an instance method
./src/App.js
class App extends Component {
//...
submitForm = ()=> {
console.log( this.state );
}
//...
}this.submitForm is an instance method, so let's put our new one next to it
./src/App.js
class App extends Component {
//...
setRapName = (event)=> this.setState({ rapName: event.target.value })
submitForm = ()=> {
console.log( this.state );
}
//...
}now when we hit submit, we should see the new value for .rapName logged as part of the state object
you can do the same for .albumSales! (make sure to name your eventHandler function well)
if you're starting from this step $ git checkout step1 git cheat sheet
Snoop is going to want to know more than a name and a number!
He also has a list of open positions to apply to, and is going to want to know where you're from (country / state)
first let's add a field to our init state
./src/App.js
//...
state = {
rapName: '',
albumSales: 0,
applyingFor: '',
}
//...and a select tag with our options
into the rendered JSX
./src/App.js
//...
<div className='form-field'>
<select>
<option value=''>Select Position</option>
<option value='driver'>Driver</option>
<option value='trafficker'>Trafficker</option>
<option value='producer'>Producer</option>
<option value='rapper'>Rapper</option>
</select>
</div>
//...now all we need is an onChange and value prop just like our last examples!
./src/App.js
//...
<select value={this.state.applyingFor} onChange={this.setApplyingFor}>
//...
</select>
//...we'll need to write the this.setApplyingFor function, but you knew that already!
Snoop wants our <select> to look better than the default style
Let's give'm what he wants
we already have
./src/App.css
/* ... */
.Apply .form-field input {
padding: 4px;
margin: 6px;
border-radius: 7px;
}
/* ... */styling our <input/>s:
.Applyis our container<div className='Apply'>...</div>- we've wrapped all of our form fields in
<div className='form-field'>...</div> inputwill select any text box<input/>inside such a div
so let's add also similar styles for the dropdowns
./src/App.css
.Apply .form-field select {
background-color: white;
padding: 4px;
margin: 6px;
border-radius: 7px;
}and to get rid of those pesky outlines
./src/App.css
.Apply .form-field input:active,
.Apply .form-field input:focus,
.Apply .form-field select:active,
.Apply .form-field select:focus {
outline: none;
}we can always add more to the styling later.
Sometimes we want to only render an element for certain states
Let's make another dropdown to select our Country
./src/App.js
//...
state = {
//...
country: '',
}
setCountry = (event)=> this.setState({ country: event.target.value })
//...
<div className='form-field'>
<select value={this.state.country} onChange={this.setCountry}>
<option value=''>Select Country</option>
<option value='USA'>USA</option>
<option value='Canada'>Canada</option>
<option value='Israel'>Israel</option>
</select>
</div>
//...Here, only when country === 'USA' are we going to want a <select/> for State
( later, we could choose the options for State based on the country, this is the simpler exercise to start with )
Using react's docs for conditional rendering we see it's pretty easy to render our State select based on our this.state.country
./src/App.js
{
(this.state.country === 'USA') ? (
<div className='form-field'>
<select value={this.state.whichState} onChange={this.setWhichState}>
<option value=''>Select State</option>
<option value='CA'>California</option>
<option value='NY'>New York</option>
<option value='MI'>Michigan</option>
</select>
</div>
) : null
}here we're using the ternary expression (inline if statement):
- when country is 'USA', the expression evaluates to a select tag (before the :)
- otherwise, the expression evaluates to
null, which in JSX means "render nothing"
to solve the naming issue with "State", I've named the field .whichState and the onChange handler this.setWhichState to avoid overwriting this.setState. Perhaps this is why Canada calls them "provinces"
of course, we'll need to initialize this.state.whichState and write the this.setWhichState onChange eventHandler
if you're starting from this step $ git checkout step2 git cheat sheet
Snoop wants us to validate our input!
Rap Names must contain "gg"
also, Snoop wants applicants to provide a valid email address.
Every time the .rapName value changes - along with setting the value - we'll also need to calculate another value for the state called isRapNameValid which we'll use to conditionally render a validation message to the user:
./src/App.js
setRapName = (event)=>
this.setState({
rapName: event.target.value,
isRapNameValid: event.target.value.includes('gg'),
})and in our render function (perhaps right after the input box)
./src/App.js
{
!this.state.isRapNameValid ? (
<div className='invalid-field-warning'>
Rap Name must contain "gg"
</div>
) : null
}Snoop wants our submit <button> to be disabled when there is invalid data in our state
./src/App.js
<button onClick={this.submitForm}
disabled={!this.state.isRapNameValid ||
!this.state.applyingFor ||
!this.state.country ||
(this.state.country ==='USA' && !this.state.whichState)}>
Submit
</button>checking that an email is valid is best done with a regex
we can use the same flow as in .isRapNameValid, but for email now (remember the submit-disabled boolean logic!)
in the solution for the step (git checkout step3) I've also added styling for the invalid-field-warning className, so it will look like a problem (red)
if you don't want to cheat by reading the solution but need some direction in how to use a regex in js to calculate a boolean based on matching it against a string... you can go ahead and google it
not too shabby! We're checking that the form is filled AND the fields with validation have passed.
if you're starting from this step $ git checkout step3 git cheat sheet
Snoop wants to know when you can start working!
To make our life easy, we're not going to build a datepicker from scratch - that'd be reinventing the wheel!
Instead, we're going to use Hacker0x01's popular react-datepicker module
$ yarn add react-datepicker
the react-datepicker docs should give you all you need to pick a date and put it in the state!
the team that wrote react-datepicker made their <DatePicker/> Component using the same controlled input pattern, which should make it very easy to use
to check a solution for step 3, git checkout step4
if you have a bug getting the date from the <DatePicker/> HINT: check the react-datepicker docs example
if you're starting from this step $ git checkout step4 && yarn git cheat sheet
Snoop wants us to randomize the colors on his portrait when we click it.
The <Snoop/> component which came with this workbook is currently using hard-coded colors:
./src/App.js
//...
<Snoop color='purple' faceColor='white' className='snoop-logo'/>
//...If we want to be able to change those values during runtime, we'll need to render them from .state of course!
./src/App.js
//...
state = {
//...
snoopHairColor: 'purple',
snoopFaceColor: 'white',
}
//...
<Snoop color={this.state.snoopHairColor} faceColor={this.state.snoopFaceColor}
className='snoop-logo'/>
//...So it's still hard-coded, but now we'll be able to write an event handler to change the colors (responding to clicking the logo):
./src/App.js
//...
const snoopColors = ['red', 'purple', 'white', 'black', 'orange', '#0c0'];
class App extends Component {
//...
changeSnoopColor = ()=> {
const colorIndex = Math.floor( Math.random() * snoopColors.length );
const newHairColor = snoopColors[ colorIndex ];
const newFaceColor = snoopColors[ (colorIndex +1) % snoopColors.length ];
this.setState({ snoopHairColor: newHairColor, snoopFaceColor: newFaceColor });
}
//...Of course, we'll need to pass this function to <Snoop/> as a prop and bind it to the svg element inside ./src/snoop.svg.js
./src/App.js
//...
<Snoop color={this.state.snoopHairColor} faceColor={this.state.snoopFaceColor}
className='snoop-logo' onClick={this.changeSnoopColor}/>
//..../src/snoop.svg.js
import React from 'react';
export default ({ color='black', faceColor='white', className='', onClick=(()=>0) })=> (
<svg x="0px" y="0px" viewBox="0 0 100 125"
className={className}
onClick={onClick}
enableBackground="new 0 0 100 100">
//...
//...What fun!
Now, when the user clicks on the SVG element (or any of its children):
- the click event will trigger our changeSnoopColor function
- the changeSnoopColor function randomly selects two colors and sets them into the
state - the state update triggers a render withe new colors (as required)
If you're wondering what
({ onClick=(()=>0) })=> //...means, let's walk through it:
- we're defining a function
()=> //... - destructuring
onClickfrom the first param (props)({ onClick })=> //... - setting a default value for
onClick:({ onClick=(...) })=> //... - the default value is a function that suddenly returns 0
({ onClick=(()=>0) })=> //...
- we're setting a default function in case the prop is undefined, we won't throw errors when the user clicks Snoop
if you're starting from this step $ git checkout step5 git cheat sheet
Snoop is really getting into this web design thing - he likes to keep his users flowing.
Snoop doesn't like the disabled submit button - he'd rather have the button always work, and if there's a validation issue a notification popup (called a "toastr" on the streets) to appear.
Let's give'm what they want!
With a bit of googling, we can find react-notifications which will do most of the work for us
$ yarn add react-notifications
//...
import { NotificationContainer, NotificationManager } from 'react-notifications';
import 'react-notifications/lib/notifications.css';
//...The NotificationManager is a singleton that we'll use to trigger notifications, NotificationContainer is the Component which will render them.
Let's render the container, and remove the submit disabling
./src/App.js
//...
<div className='Apply'>
<NotificationContainer/>
//...
<button onClick={this.submitForm}>
Submit
</button>
//...now we can trigger notifications in our submitForm function
./src/App.js
//...
submitForm = ()=>{
(!this.state.isEmailValid ||
!this.state.isRapNameValid ||
!this.state.applyingFor ||
!this.state.country ||
(this.state.country ==='USA' && !this.state.whichState)) ?
NotificationManager.warning('Missing Information', 'Application not Submitted', 3000):
console.log({
...this.state,
isRapNameValid: undefined,
isEmailValid: undefined,
startDate: this.state.startDate.valueOf(),
})
}
//...you'll notice we've just moved the boolean logic from the disabled prop to our new ternary statement!
if you're starting from this step $ git checkout step6 git cheat sheet
Snoop wants to give our applicants some fireworks when they finish the application; make them feel like a real OG
We had a creative brainstorming sesh, and the best idea we came up with was to have Tupac shoot some gold records out of an AK47 when the user submits the job application successfully.
here is an image of tupac and here are some AKs to choose from
feel free to find a gold record with a see-through background on google image search on your own... (shouldn't be too hard)
we'll need to download them, render them, style them, show them on form submission, and then animate them.
- make sure the files you find are .png with see through backgrounds
- save the files into your project directory and name them well (tupac.png, ak47.png, goldRecord.png)
let's import the images and render them somewhere
./src/App.js
//...
import tupac from './tupac.png';
import ak47 from './ak47.png';
import goldRecord from './goldRecord.png';
//...
render(){
return (
<div className='Apply'>
//...
<img className='tupac' src={tupac}/>
<img className='ak47' src={ak47}/>
<img className='goldRecord' src={goldRecord}/>
</div>
);
}
}and to get started
./src/App.css
/* ... */
.tupac {
position: fixed;
bottom: 0;
left: 0;
width: 50vw;
max-width: 400px;
z-index: 9;
}
.ak47 {
position: fixed;
bottom: 0;
left: 180px;
height: 150px;
width: auto;
z-index: 10;
transform: scale(-1,1) rotate(40deg);
}
.goldRecord {
position: fixed;
height: 40px;
width: 40px;
bottom: 50px;
left: 50px;
z-index: 12;
}... normally we won't want to position: fixed; anything. However, since we'll be animating these images, it's okay here.
./src/App.js
//...
state = {
// ...
showTupac: false,
}
//...
submitForm = ()=>{
(!this.state.isEmailValid ||
!this.state.isRapNameValid ||
!this.state.applyingFor ||
!this.state.country ||
(this.state.country ==='USA' && !this.state.whichState)) ?
NotificationManager.warning('Missing Information', 'Application not Submitted', 3000):
this.setState({ showTupac: true });
}
//...
render(){
return (
<div className='Apply'>
//...
{this.state.showTupac ? (
<div>
<img className='tupac' src={tupac}/>
<img className='ak47' src={ak47}/>
<img className='goldRecord' src={goldRecord}/>
</div>
): null}
</div>
);
}
}Here we'll use css animation property using @keyframes's from and to syntax.
How this works is we can override some of our css properties from the animation
Here, since we position: fixed; our images, we can override their bottom and left properties to animate them.
(and by them, I mean the gold record... we'll use animation-iteration-count: infinite; to give tupac an infinite AK)
./src/App.css
.goldRecord {
height: 40px;
width: 40px;
position: fixed;
bottom: 212px;
left: 470px;
z-index: 12;
animation: shoot-record infinite 0.125s linear;
}
@keyframes shoot-record {
from {
bottom: 212px;
left: 470px;
}
to {
bottom: 1012px;
left: 1270px;
}
}of course, the exact pixel numbers will vary based on the images you selected.
let's let the animation run for a few seconds, and then remove it and clear the form
we'll use setTimeout, which is the fundamental timing appliance in javascript.
where we had
./src/App.js
this.setState({ showTupac: true });let's write
./src/App.js
this.setState({ showTupac: true }, ()=> setTimeout(()=> this.setState({ showTupac: false }), 8000));and we'll break it down:
- (contextually) the submission has succeeded, let's show tupac
- the second param we send to
this.setStateis a callback function - that callback runs after the state is updated to show tupac
- in that callback, we call
setTimeoutwith a function and 8000- that means our timed-out function will run in 8000ms = 8 seconds
- our timed-out function will call
this.setStateto setthis.state.showTupactofalse
- in that callback, we call
there's other ways to do this, but this was the shortest!
also in the timed-out setState, we may want to reset the form... which will be a feature challenge.
Feature challenges!!!
- When the country is set to something other than 'USA', clear
.whichState - Render the notification text based on which information is wrong / missing
- change the direction of the weed leaf orbit when the user clicks on it
- declare any rap name used by a member of tha dogg pound to be invalid
- album sales should change 1000 per step and can't be negative (except vanilla ice)
- reset the form after successful submission




