Skip to content

Instantly share code, notes, and snippets.

@masterfermin02
Last active January 8, 2026 15:45
Show Gist options
  • Select an option

  • Save masterfermin02/2e2065969349df0ec209aef067c35559 to your computer and use it in GitHub Desktop.

Select an option

Save masterfermin02/2e2065969349df0ec209aef067c35559 to your computer and use it in GitHub Desktop.
import React from "react";
import { useForm } from "@inertiajs/react";
import MultiSelectChips from "@/Components/MultiSelectChips";
export default function Edit() {
const techOptions = [
{ value: "laravel", label: "Laravel" },
{ value: "react", label: "React" },
{ value: "dotnet", label: ".NET" },
{ value: "twilio", label: "Twilio" },
{ value: "postgres", label: "Postgres" },
];
const { data, setData, post, processing, errors } = useForm({
technologies: ["react"], // initial values
});
function submit(e) {
e.preventDefault();
post(route("profile.update"));
}
return (
<form onSubmit={submit} className="space-y-6">
<div>
<MultiSelectChips
label="Technologies"
name="technologies"
options={techOptions}
value={data.technologies}
onChange={(next) => setData("technologies", next)}
/>
{errors.technologies && (
<p className="mt-2 text-sm text-red-600">{errors.technologies}</p>
)}
</div>
<button
type="submit"
disabled={processing}
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-60"
>
Save
</button>
</form>
);
}
import React from "react";
export default function MultiSelectChips({
label,
options,
value,
onChange,
placeholder = "Select one or more…",
disabled = false,
name,
}) {
const [open, setOpen] = React.useState(false);
const rootRef = React.useRef(null);
// Close on outside click
React.useEffect(() => {
function onDocClick(e) {
if (!rootRef.current) return;
if (!rootRef.current.contains(e.target)) setOpen(false);
}
document.addEventListener("mousedown", onDocClick);
return () => document.removeEventListener("mousedown", onDocClick);
}, []);
// Close on Esc
React.useEffect(() => {
function onKey(e) {
if (e.key === "Escape") setOpen(false);
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, []);
const selected = Array.isArray(value) ? value : [];
const selectedOptions = options.filter((o) => selected.includes(o.value));
function toggleValue(v) {
if (disabled) return;
const next = selected.includes(v)
? selected.filter((x) => x !== v)
: [...selected, v];
onChange(next);
}
function removeValue(v) {
if (disabled) return;
onChange(selected.filter((x) => x !== v));
}
function clearAll() {
if (disabled) return;
onChange([]);
}
return (
<div className="w-full" ref={rootRef}>
{label && (
<label className="mb-2 block text-sm font-medium text-gray-700">
{label}
</label>
)}
{/* Hidden input(s) for classic form compatibility (optional) */}
{name &&
selected.map((v) => (
<input key={v} type="hidden" name={`${name}[]`} value={v} />
))}
{/* Control */}
<button
type="button"
disabled={disabled}
onClick={() => setOpen((s) => !s)}
className={[
"flex min-h-[44px] w-full items-center justify-between rounded-lg border bg-white px-3 py-2 text-left text-sm shadow-sm",
"focus:outline-none focus:ring-2 focus:ring-blue-200",
disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer",
open ? "border-blue-500" : "border-gray-300",
].join(" ")}
>
<div className="flex flex-wrap gap-2">
{selectedOptions.length === 0 ? (
<span className="text-gray-400">{placeholder}</span>
) : (
selectedOptions.map((o) => (
<span
key={o.value}
className="inline-flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-800"
>
{o.label}
<span
role="button"
tabIndex={0}
aria-label={`Remove ${o.label}`}
onClick={(e) => {
e.stopPropagation();
removeValue(o.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
removeValue(o.value);
}
}}
className="inline-flex h-4 w-4 items-center justify-center rounded-full text-gray-500 hover:bg-gray-200 hover:text-gray-700"
>
×
</span>
</span>
))
)}
</div>
<span className="ml-3 select-none text-gray-500">
{open ? "▲" : "▼"}
</span>
</button>
{/* Dropdown */}
{open && !disabled && (
<div className="mt-2 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg">
<ul className="max-h-56 overflow-auto py-1 text-sm">
{options.map((o) => {
const isSelected = selected.includes(o.value);
return (
<li key={o.value}>
<button
type="button"
onClick={() => toggleValue(o.value)}
className={[
"flex w-full items-center justify-between px-3 py-2 text-left",
"hover:bg-gray-50",
isSelected ? "bg-blue-50" : "",
].join(" ")}
>
<span className="text-gray-800">{o.label}</span>
<span
className={[
"ml-3 inline-flex h-5 w-5 items-center justify-center rounded border text-xs",
isSelected
? "border-blue-500 bg-blue-500 text-white"
: "border-gray-300 bg-white text-transparent",
].join(" ")}
>
</span>
</button>
</li>
);
})}
</ul>
<div className="flex items-center justify-between border-t border-gray-100 px-3 py-2">
<button
type="button"
onClick={clearAll}
className="text-xs font-medium text-gray-600 hover:text-gray-900"
>
Clear
</button>
<button
type="button"
onClick={() => setOpen(false)}
className="rounded-md bg-gray-900 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-800"
>
Done
</button>
</div>
</div>
)}
</div>
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment