Skip to content

Instantly share code, notes, and snippets.

@Southclaws
Created July 21, 2021 19:58
Show Gist options
  • Save Southclaws/8317d446638324c9fd33112c496cfc96 to your computer and use it in GitHub Desktop.
Save Southclaws/8317d446638324c9fd33112c496cfc96 to your computer and use it in GitHub Desktop.
InputWithChips renders what appears to be a single input field that contains small tag-like elements inside it known as "Chips". When the user types something and presses Enter, their text is turned into an element inside the interactive box which can be removed either by using backspace or by clicking a small "delete" inside the chip. It achiev…
import React, { Component, useState } from "react";
type Props = {
// The tags to show the component with when it's created
initialTags: string[];
// Callback to fire when the user adds a new tags
onAdd: (text: string) => void;
// Callback to fire when the user removes a tag either by backspace or click
onRemove: (index: number, text: string) => void;
};
type State = {
chips: string[];
input: string;
};
/**
* InputWithChips renders what appears to be a single input field that contains
* small tag-like elements inside it known as "Chips". When the user types
* something and presses Enter, their text is turned into an element inside the
* interactive box which can be removed either by using backspace or by clicking
* a small "delete" inside the chip.
*
* It achieves this by rendering a small input box after each chip. The final
* chip sits next to an input that fills the rest of the container's width. To
* the user, this appears as if it's one input with chips inside it but it's
* actually many small inputs which don't allow input but only backspace to
* delete the chip to its left.
*
* It doesn't handle arrow keys so you can't move around the whole element like
* an input but that could be added easily.
*/
class InputWithChips extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
chips: this.props.initialTags ?? [],
input: "",
};
}
addTag(text: string) {
const chips = this.state.chips.concat(text);
this.setState({
...this.state,
chips,
});
}
removeTag(idx: number) {
const newTags = [...this.state.chips];
newTags.splice(idx, 1);
this.setState({ ...this.state, chips: newTags });
this.props.onRemove?.call(this, idx, this.state.chips[idx]);
}
handleBetweenInput(idx: number, event: any) {
event.preventDefault();
if (event.code === "Backspace") {
this.removeTag(idx);
}
}
render() {
return (
<>
<div>
{this.state.chips.map((data, idx) => {
// Check if this chip is between two or at the end of the list since
// if it's not don't render the input to the right hand side because
// that input is rendered below as it's always shown regardless of a
// list of current chips the user has entered into the field already
const between = idx < this.state.chips.length - 1;
return (
<span key={idx}>
<label htmlFor={`between-${idx}`}>
{data}{" "}
<button onClick={() => this.removeTag.bind(this)(idx)}>
x
</button>
</label>
{between ? (
<input
onKeyUp={(e) => this.handleBetweenInput.bind(this)(idx, e)}
onChange={(e) => this.handleBetweenInput.bind(this)(idx, e)}
id={`between-${idx}`}
className="between"
type="text"
value=""
/>
) : null}
</span>
);
})}
<input
onKeyUp={(e) => {
if (e.code === "Backspace") {
this.removeTag(this.state.chips.length - 1);
} else if (e.code === "Enter") {
this.props.onAdd?.call(this, this.state.input);
this.setState({
chips: [...this.state.chips, this.state.input],
input: "",
});
}
}}
onChange={(e) => {
this.setState({ ...this.state, input: e.target.value });
}}
className="end"
type="text"
value={this.state.input}
/>
</div>
<style jsx>{`
div {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
border: 1px solid hsla(1, 50%, 50%, 20%);
padding: 0.2em;
--gap-size: 0.1em;
}
span {
display: flex;
margin: 0.1em;
align-content: center;
justify-content: space-around;
}
button {
padding: 0.1em;
cursor: pointer;
line-height: 1;
}
label {
border: 1px solid hsla(180, 50%, 60%, 30%);
background-color: hsla(180, 50%, 60%, 30%);
border-radius: 0.2em;
padding: 0.1em var(--gap-size);
}
input {
border: none;
background-color: rgba(0, 0, 0, 0);
height: 2em;
padding-left: 0.2em;
width: calc(var(--gap-size) * 10);
margin-left: 0;
margin-right: calc(var(--gap-size) * -4);
}
input:focus {
outline: none;
}
.end {
flex-grow: 1;
}
`}</style>
</>
);
}
}
const Page = () => {
const [chips, setTags] = useState(["xyz", "samp", "michael"]);
return (
<div>
<InputWithChips
initialTags={chips}
onAdd={(text: string) => {
console.log("Added a tag:", text);
setTags([...chips, text]);
}}
onRemove={(idx: number, text: string) => {
console.log("Removed a tag:", idx, text);
const newTags = [...chips];
newTags.splice(idx, 1);
setTags(newTags);
}}
/>
<p>{JSON.stringify(chips)}</p>
<style jsx>{`
div {
padding: 1em;
border: 1px solid black;
}
`}</style>
</div>
);
};
export default Page;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment