More Interactivity with React

Dr. Greg Bernstein

February 24th, 2020

More Interactivity with React

Readings

Form to Add Question

  • Will create a new component to add/edit questions to Quiz-O-Matic

  • Need to take in text for the question, each of the choices and indicate the correct choice.

Initial Mockup

Example branch classComp4

Lot’s of Pieces

Form component quickly become complicated!

  • Text area <textarea> for question
  • Inputs <input> for each choice
  • Buttons for deleting choices, adding choice, adding question
  • Select <select> for choosing the answer

Design 1

Keep all the form info in the component state:

class AddQuestion extends React.Component {
  constructor(props) {
    super(props); // Must call
    this.state = {
      question: "When can you store plaintext passwords on the server?",
        choices: ["If I feel like it", "sometimes", "never"], answer: 2};
  }

  render() {...}
}

Design 2

No “functionality” yet since no event handling:

render() { //familiar pattern below!
    let choiceItems = this.state.choices.map(function(choice, i) {
        return <li  key={"choice"+i}>
            <button>Delete</button><span>   </span>
            <input value={choice}/>
        </li>
    })
    const letters = "abcdefghi";
    let options = this.state.choices.map(function(choice, i){
        return <option key={"opt"+i} value={i}>{letters[i]+". "}</option>;
    }) //familiar pattern here too!

  return <div className="addQComp">
      <p>Question</p>
      <textarea value={this.state.question}></textarea>
      <p>Choices</p>
      <ol style={{listStyleType: "lower-alpha", paddingLeft: "1em"}}>
      {choiceItems}</ol>
        <p><span>Answer:</span>
        <select value={this.state.answer}>{options}</select></p>
      <p><button>Add Choice</button><button>Add Question</button></p>
  </div>;
}

Updating Text Area

Branch classComp5

  • Add a handler for changes to the text area:
textAreachange(event){
  this.setState({question: event.currentTarget.value});
}
  • Add an onChange attribute to the text area:
<textarea value={this.state.question} onChange={this.textAreachange.bind(this)}/>

this, Arrays, and bind()

Updating Choices 1

Issues:

  • Variable number of possible choices
  • Need to update state within an array

Updating Choices 2

Event Handler for choice <input> change:

choiceChange(i, event) {
  // Creates a new modified array of choices
  let newChoices = this.state.choices.map(function(choice, index){
    if (index === i) { // Only changes a particular choice
      return event.currentTarget.value; // from the <input> element
    } else
      return choice;
  })
  this.setState({choices: newChoices}); // update state
}

Updating Choices 3

The following from render doesn’t work for multiple reasons!

let choiceItems = this.state.choices.map(function(choice, i) {
    return <li  key={"choice"+i}>
        <input onChange={this.choiceChange(i).bind(this)} value={choice}/>
    </li>
});

Updating Choices 4

Why?

In a different function context!

Closure Based Fix

Put this in a variable with a different name for safekeeping. Also see proper use of bind with additional variable i.

let that = this;  // Saves this into another variable for use below
let choiceItems = this.state.choices.map(function(choice, i) {
    return <li  key={"choice"+i}>
        <button onClick={that.delChoice.bind(that, i)}>Delete</button><span>   </span>
        <input onChange={that.choiceChange.bind(that, i)} value={choice}/>
    </li>
});

Modifying a state Array

Adding a “choice” 1

JSX for add choice button, and add question button:

<p><button onClick={this.addChoice.bind(this)}>Add Choice</button>
  <button onClick={this.addQuestion.bind(this)}>Add Question</button></p>

Adding a “choice” 2

Event handling code, note use of array method:

addChoice() {
  // concat method create a new array with added element
  this.setState({choices: this.state.choices.concat("")});
}

Deleting a Choice 1

JSX for delete choice buttons, note bind with i:

let choiceItems = this.state.choices.map(function(choice, i) {
    return <li  key={"choice"+i}>
        <button onClick={that.delChoice.bind(that, i)}>Delete</button><span>   </span>
        <input onChange={that.choiceChange.bind(that, i)} value={choice}/>
    </li>
});

Deleting a Choice 2

Event handling code, note use of array filter method:

delChoice(i) {
  // filter produces a new array
  let upChoices = this.state.choices.filter(function(choice, index){
    if(index === i)
      return false;
    else
      return true;
  })
  this.setState({choices: upChoices});
}

Function Call Up!

Quiz-o-Matic Questions Update

  • Want the AddQuestion component to be able to add a question to the app.
  • The questions array is part of its parents state so it can’t modify it
  • What can we do? Pass a method down to update questions.

addQuestion() on Quiz-o-Matic

class QuizOMatic extends React.Component {
  constructor(props) {
    super(props); // Must call
    // a member variable called "state" to hold the state as a JS object
    this.state = {
      show: "addQ", questions: questions,
      user: "CS351", score: "0/0", minutes: 12
    };
  }

  addQuestion(q) { // Take a multiple choice question as input
    this.setState({questions: this.state.questions.concat(q)});
    // Show the quiz so we can see it
    this.setState({show: "quiz"});
  }
  // other stuff
  render(){...}
}

Pass addQuestion() down

Part of render():

switch (this.state.show) {
  case "intro":
    contents = <Intro user={this.state.user} takeQuiz={this.quizHandler.bind(this)}/>;
    break;
  case "quiz":
    contents = <Quiz questions={this.state.questions} gradeIt={this.resultHandler.bind(this)}/>;
    break;
  case "summary":
    contents = <Summary user={this.state.user} score={this.state.score}
                        minutes={this.state.minutes} again={this.quizHandler.bind(this)}/>;
    break;
  case "addQ": // Passing addQuestion method down here.
    contents = <AddQuestion addQuestion={this.addQuestion.bind(this)}/>;
    break;
  default:
    contents = <h2>Warning something went wrong!!!</h2>;
}

Using the passed method 1

AddQuestion component event handler:

addQuestion() { //Too many functions with the same name?
  // Put local state into nicely formatted object
  let q = {question: this.state.question, choices: this.state.choices,
    answer: this.state.answer};
  // Call passed down function with new question object
  this.props.addQuestion(q);
}

Using the passed method 2

Portion of AddQuestion render:

<p><button onClick={this.addChoice.bind(this)}>Add Choice</button>
  <button onClick={this.addQuestion.bind(this)}>Add Question</button></p>