Created
July 21, 2021 19:58
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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