- 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.
- Be prepared to discuss at the end of class: What design pattern is responsible for the magic?
- Remove the
Square
and Board
classes.
- In the
Game
class, remove the < Square>
and <Board>
elements. Just render the div
elements.
- 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 }] }
}
- 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>
);
}
}
- 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 ...(..., ...)}/>
- That square is rather puny. In
index.css
, change the width and height of the square
class to 4em
- 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
- 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) } />
- 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
?
- 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>
)
}
}
- 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
- Now hook up text editors to the second column of the property sheet:
<TextEditor dataProps={this.props.dataProps} changeListener={this.props.changeListener} />
- 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?
-
Now what happens when you edit the color or text property? Why?
- 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.
- 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>
)
}
}
- In the
Row
renderer, make a ColorEditor
if the property name is color
and a TextEditor
otherwise. How can you change the colors?
- 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 }
- 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>
}
- 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.
- What design pattern is responsible for the magic?