CS 151 - Day 21

Cover page image

Cay S. Horstmann

Lab

lab

A Property Sheet

  1. Copy everything from lab20 to lab21:
    cd cs151
    cp -R lab20 lab21
    cd lab21
    npm start
    
    Do this even if you got very little done in the previous lab—we'll rip most of it apart anyway.
  2. Be prepared to discuss at the end of class: What design pattern is responsible for the magic?
  3. Remove the Square and Board classes.
  4. In the Game class, remove the < Square> and <Board> elements. Just render the div elements.
  5. Add this class to the top of src/index.js
    class Square {
      constructor(color, text) {
        this.color = color
        this.text = text
      }
      getProperties() { return [
        {name: 'color', value: this.color },
        {name: 'text', value: this.text }] }    
    }
    
  6. Add this component class below. Think of it as the React analog to the draw method.
    class SquareComponent extends React.Component {
      render() {
        return (
            <div
          style={{background: this.props.data.color }}
          className="square">
            { this.props.data.text }
          </div>
        );
      }
    }
    
  7. In the Game renderer (which has ceased to be a game, but we don't care), render a blue square where the game board used to be. The square should say Fred. Hint:
            <SquareComponent data={new ...(..., ...)}/>
    
  8. That square is rather puny. In index.css, change the width and height of the square class to 4em
  9. Actually, we want to edit the square properties with a property sheet. Because of the way React works, we need to add state to the parent and update it from the property sheet, which causes the SquareComponent to be redrawn. Add this plumbing to the Game class:
      constructor(props) {
        super(props)
        this.state = { data: new Square('blue', 'Fred') }
      }
    
      // https://medium.com/@ruthmpardee/passing-data-between-react-components-103ad82ebd17
      squareChangeListener(name, value) {
        // ...
        this.setState({data: this.state.data})
      }
    
    Then update the data prop of SquareComponent to this.state.data
  10. Now we want to add a property sheet that edits the properties that the Square so kindly reveals through its getProperties method. In the Game renderer, add this below the div holding the SquareComponent:
            <Sheet dataProps={this.state.data.getProperties()} changeListener={ (n, v) => this.squareChangeListener(n, v) } />
    
  11. Of course this isn't going to work. We didn't define a Sheet component yet. A sheet has a row for each property:
    class Sheet extends React.Component {
      render() {
        return (<table>{this.props.dataProps.map(p => <Row dataProps={p} changeListener={this.props.changeListener} />)}</table>)
      }
    }
    
    What is the purpose of this.props.dataProps.map?
  12. Of course this isn't going to work. We didn't define a Row component yet. A row shows the name and value of each property:
    class Row extends React.Component {
      render() {
        return (
            <tr><td>{this.props.dataProps.name}</td><td>{this.props.dataProps.value}</td></tr>
          )
      }
    }
    
  13. Ok, but we want to edit the property values, not just see them. We need an editor. Here is one:
    // https://medium.com/capital-one-tech/how-to-work-with-forms-inputs-and-events-in-react-c337171b923b
    
    class TextEditor extends React.Component {
      constructor(props) {
        super(props)
        this.state = { text: this.props.dataProps.value }  
      }
      
      handleChange(event) {
        this.setState({text: event.target.value})
        this.props.changeListener(this.props.dataProps.name, event.target.value)
      }
      
      render() {
        return <input type='text' value={this.state.text} onChange={event => this.handleChange(event) } />
      }
    }
    
    The local state is needed to keep React happy. The important part is the call to this.props.changeListener
  14. Now hook up text editors to the second column of the property sheet:
    <TextEditor dataProps={this.props.dataProps} changeListener={this.props.changeListener} />
  15. Now follow the chain of change listeners until you get to the top. That change listener needs to actually change the square. How do you do that?
  16. Now what happens when you edit the color or text property? Why?
  17. It would be much nicer if we could edit the RGB values of the colors. First, change the color from 'blue' to [0, 0, 255]. To render RGB colors from such an array, compute this rgb string:
    const color = this.props.data.color
    const rgb = `rgb(${color[0]},${color[1]},${color[2]})`
    Then put that in the background style of the square.
  18. Now we need to edit such a color array. Here is an editor:
    class ColorEditor extends React.Component {
      constructor(props) {
        super(props)
        this.state = { color: this.props.dataProps.value }  
      }
      
      handleChange(event) {
        const color = this.state.color
        if (event.target.name === 'red') color[0] = event.target.value
        else if (event.target.name === 'green') color[1] = event.target.value
        else if (event.target.name === 'blue') color[2] = event.target.value
        this.setState({color})
        this.props.changeListener(this.props.dataProps.name, color)
      }
      
      render() {
        return (
            <div>
              <div><input name='red' type='range' min='0' max='255' value={this.state.color[0]} onChange={event => this.handleChange(event) } /></div>
              <div><input name='green' type='range' min='0' max='255' value={this.state.color[1]} onChange={event => this.handleChange(event) } /></div>
              <div><input name='blue' type='range' min='0' max='255' value={this.state.color[2]} onChange={event => this.handleChange(event) } /></div>
            </div>
        )
      }  
    }
    
  19. In the Row renderer, make a ColorEditor if the property name is color and a TextEditor otherwise. How can you change the colors?
  20. That's a bit of a hack. Let's do better than that. Add an editor field to each of the property descriptors:
        {name: 'color', value: this.color, editor: ColorEditor }
    
  21. To select the correct one, use this React feature (yes, it needs to be an uppercase E):
      render() {
        // https://reactjs.org/docs/jsx-in-depth.html#choosing-the-type-at-runtime    
        const Editor = this.props.dataProps.editor
        return <tr><td>...</td><td><Editor .../></td></tr>
    }
    
  22. As you enjoy changing the properties, ponder what would be required to support a Boolean property. Go ahead and add a BooleanEditor if you have time.
  23. What design pattern is responsible for the magic?