Adding Swatches

The user needs a color picker in order to configure and add new colors to the palette. Since the picker has some complexity, you decide to encapsulate that complexity in a component. You haven't written it yet, but you add it to App:

import {Picker} from './Picker';

// ...

function App() {
return (
<div className="App">
<Picker />
{/* ... */}
</div>
);
}

Then you create your Picker component in Picker.js. It looks mostly like Swatch, but all the child elements have been removed and an ID has been added:

export function Picker(props) {
const style = {
backgroundColor: 'black',
};
return (
<div id="picker" className="swatch" style={style}>
</div>
);
}

The picker needs three sliders for the red, green, and blue intensities. These will range from 0 to 255. It also needs a text input for the name of the color and a button for adding the configured color to the palette.

export function Picker(props) {
// ...
return (
<div id="picker" className="swatch" style={style}>
<input type="range" min="0" max="255" />
<input type="range" min="0" max="255" />
<input type="range" min="0" max="255" />
<input id="name-input" type="text" />
<button>Add</button>
</div>
);
}

The form elements are not neatly aligned. You turn the picker into a vertical flex parent by adding these styles to App.css:

#picker {
display: inline-flex;
flex-direction: column;
justify-content: center;
padding: 20px;
}

#name-input {
margin: 20px 0;
}

Try dragging the sliders. What happens?

Something should happen when the user drags the sliders. Perhaps you could show the user what color has been configured. You decide to take the numbers from the sliders and compose a string for the picker's background color. Instead of letting the sliders themselves store the numbers, you create three pieces of state for the red, green, and blue intensities:

import {useState} from 'react';

function Picker(props) {
const [red, setRed] = useState(0);
const [green, setGreen] = useState(0);
const [blue, setBlue] = useState(0);

// ...
}

All three intensities are initially 0, which combine to make black. You must assemble the three intensities into a hexadecimal string. An sprintf function would be really handy for this, but JavaScript doesn't have one. Turning a number in [0, 255] into a two-digit hexadecimal string therefore requires some labor. The toString and padStart methods may be of assistance. You use these methods to form the string and set the background color:

function Picker(props) {
// ...

const hex = '#'
+ red.toString(16).padStart(2, '0')
+ green.toString(16).padStart(2, '0')
+ blue.toString(16).padStart(2, '0');

const style = {
backgroundColor: hex,
};

// ...
}

The red, green, and blue intensities are always 0 currently. They never change. Try setting their default values to something other than 0. Does the background color change?

The three state variables serve as the model behind the sliders. You set their value props to these variables:

export function Picker(props) {
// ...

return (
<div id="picker" className="swatch" style={style}>
<input type="range" min="0" max="255" value={red} />
<input type="range" min="0" max="255" value={green} />
<input type="range" min="0" max="255" value={blue} />
{/* ... */}
</div>
);
}

When the page reloads, are the sliders positioned according to the initial state? When you change the sliders, does the color update?

To make the sliders update the state variables, you must add a callback for the sliders' change events. These callbacks will trigger setRed and its siblings:

export function Picker(props) {
// ...

return (
<div id="picker" className="swatch" style={style}>
<input
type="range"
min="0"
max="255"
value={red}
onChange={event => setRed(???)}
/>
<input
type="range"
min="0"
max="255"
value={green}
onChange={event => setGreen(???)}
/>
<input
type="range"
min="0"
max="255"
value={blue}
onChange={event => setBlue(???)}
/>
{/* ... */}
</div>
);
}

You aren't sure what to pass as for parameters to the state setters. In the past, you've had a reference to the input element, and you've pulled out its contents with element.value. In React, you don't have a reference. However, you do have the event object, and it has a target parameter that is a reference to the element that triggered the event.

You grab the input's current value with event.target.value, convert it from a string to an integer, and set the state:

export function Picker(props) {
// ...

return (
<div id="picker" className="swatch" style={style}>
<input
type="range"
min="0"
max="255"
value={red}
onChange={event => setRed(parseInt(event.target.value))}
/>
<input
type="range"
min="0"
max="255"
value={green}
onChange={event => setGreen(parseInt(event.target.value))}
/>
<input
type="range"
min="0"
max="255"
value={blue}
onChange={event => setBlue(parseInt(event.target.value))}
/>
{/* ... */}
</div>
);
}

Try changing the sliders now. Does the background color update automatically?

Try applying this same strategy for the color name. You need state, and you need to tie that state to the text input.

You end up with a very similar structure:

export function Picker(props) {
const [name, setName] = useState('');

// ...

return (
<div id="picker" className="swatch" style={style}>
{/* ... */}
<input
id="name-input"
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
{/* ... */}
</div>
);
}

After configuring the color, the user will click the button to add it to the palette. This requires some cross-component cooperation, just like deleting swatches did. The App component will have to send down a function for adding a new color. You unbundle it and call it on a click event:

export function Picker(props) {
const {add} = props;

// ...

return (
<div id="picker" className="swatch" style={style}>
{/* ... */}
<button onClick={() => add({hex, name})}>Add</button>
</div>
);
}

Note that the name and hexadecimal string and bundled together in an object literal.

Try configuring a color and clicking the button. The app breaks because the parent isn't providing the add function yet. You head to App and define addColor to prepend the new color object to the colors array:

const addColor = newColor => {
setColors([newColor, ...colors]);
};

You then pass this function down to Picker:

function App() {
// ...

return (
<div className="App">
<Picker add={addColor} />
{/* ... */}
</div>
);
}

Try adding a color. Does a new swatch appear showing the color name and hexadecimal string?

Try adding a color and then adding it again. Does a second instance appear? It does, but it probably shouldn't. You decide to remove any old color of the same name with a call to filter:

const addColor = newColor => {
setColors([
newColor,
...colors.filter(color => color.name !== newColor.name)
]);
};

That rounds out Pal. There are many other features you could add, like checking for a blank color name and storing the palette between sessions. But you are tired, and that's good enough for now.