This is the component that must be used.
Here is its source code:
import { Fragment, useState } from 'react'
import { Combobox, Transition } from '#headlessui/react'
import { CheckIcon, SelectorIcon } from '#heroicons/react/solid'
const people = [
{ id: 1, name: 'Wade Cooper' },
{ id: 2, name: 'Arlene Mccoy' },
{ id: 3, name: 'Devon Webb' },
{ id: 4, name: 'Tom Cook' },
{ id: 5, name: 'Tanya Fox' },
{ id: 6, name: 'Hellen Schmidt' },
]
export default function Example() {
const [selected, setSelected] = useState(people[0])
const [query, setQuery] = useState('')
const filteredPeople =
query === ''
? people
: people.filter((person) =>
person.name
.toLowerCase()
.replace(/\s+/g, '')
.includes(query.toLowerCase().replace(/\s+/g, ''))
)
return (
<div className="fixed top-16 w-72">
<Combobox value={selected} onChange={setSelected}>
<div className="relative mt-1">
<div className="relative w-full cursor-default overflow-hidden rounded-lg bg-white text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm">
<Combobox.Input
className="w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-gray-900 focus:ring-0"
displayValue={(person) => person.name}
onChange={(event) => setQuery(event.target.value)}
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
<SelectorIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Combobox.Button>
</div>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => setQuery('')}
>
<Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{filteredPeople.length === 0 && query !== '' ? (
<div className="relative cursor-default select-none py-2 px-4 text-gray-700">
Nothing found.
</div>
) : (
filteredPeople.map((person) => (
<Combobox.Option
key={person.id}
className={({ active }) =>
`relative cursor-default select-none py-2 pl-10 pr-4 ${
active ? 'bg-teal-600 text-white' : 'text-gray-900'
}`
}
value={person}
>
{({ selected, active }) => (
<>
<span
className={`block truncate ${
selected ? 'font-medium' : 'font-normal'
}`}
>
{person.name}
</span>
{selected ? (
<span
className={`absolute inset-y-0 left-0 flex items-center pl-3 ${
active ? 'text-white' : 'text-teal-600'
}`}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Transition>
</div>
</Combobox>
</div>
)
}
My question is: is it possible to show some information for an element when we hover over it?
For example, if we modify the input to this one:
const people = [
{ id: 1, name: 'Wade Cooper', info: 'test1' },
{ id: 2, name: 'Arlene Mccoy', info: 'test2' },
{ id: 3, name: 'Devon Webb', info: 'test3' },
{ id: 4, name: 'Tom Cook', info: 'test4' },
{ id: 5, name: 'Tanya Fox', info: 'test5' },
{ id: 6, name: 'Hellen Schmidt', info: 'test6' },
]
When we hover over first element, to show test1, for the second element test2 and so on.
I am not a react expert(/user). But for the Combobox component is 'active' the key word for hover. You have to conditionally render your displayed text/or the whole span like in pseudocode(however you conditionally render stuff in react):
active ? person.info : person.name
Related
I have 2 select option fields using v-for loops from constants. One is for sides and another for caps. If a user selects 2-Sided in the sides selection, I want the active item under caps to show false.
<template>
...
<!-- Sides -->
<div class="mt-6">
<h3>Sides</h3>
<RadioGroup v-model="sides" class="mt-2">
<RadioGroupLabel class="sr-only"> Choose Sides </RadioGroupLabel>
<div class="grid grid-cols-3 gap-3 sm:grid-cols-4">
<RadioGroupOption as="template" v-for="option in sideOptions" :key="option.name" :value="option" :disabled="!option.active" v-slot="{ active, checked }">
<div :class="[option.active ? 'cursor-pointer focus:outline-none' : 'opacity-25 cursor-not-allowed', active ? 'ring-2 ring-offset-2 ring-indigo-500' : '', checked ? 'bg-indigo-600 border-transparent text-white hover:bg-indigo-700' : 'bg-white border-gray-200 text-gray-900 hover:bg-gray-50', 'border rounded-md py-3 px-3 flex items-center justify-center text-sm font-medium uppercase sm:flex-1']">
<RadioGroupLabel as="span">
{{ option.name }}
</RadioGroupLabel>
</div>
</RadioGroupOption>
</div>
</RadioGroup>
</div>
<!-- Caps -->
<div class="mt-6">
<h3>Caps</h3>
<RadioGroup v-model="caps" class="mt-2">
<RadioGroupLabel class="sr-only"> Choose Caps </RadioGroupLabel>
<div class="grid grid-cols-3 gap-3 sm:grid-cols-4">
<RadioGroupOption as="template" v-for="option in capOptions" :key="option.name" :value="option" :disabled="!option.active" v-slot="{ active, checked }">
<div :class="[option.active ? 'cursor-pointer focus:outline-none' : 'opacity-25 cursor-not-allowed', active ? 'ring-2 ring-offset-2 ring-indigo-500' : '', checked ? 'bg-indigo-600 border-transparent text-white hover:bg-indigo-700' : 'bg-white border-gray-200 text-gray-900 hover:bg-gray-50', 'border rounded-md py-3 px-3 flex items-center justify-center text-sm font-medium uppercase sm:flex-1']">
<RadioGroupLabel as="span">
{{ option.name }}
</RadioGroupLabel>
</div>
</RadioGroupOption>
</div>
</RadioGroup>
</div>
</template>
<script setup lang="ts">
...
const capOptions = [
{ name: 'No Cap', active: true },
{ name: '2-Sided', active: true },
{ name: '3-Sided', active: true },
{ name: '4-Sided', active: true },
]
const sideOptions = [
{ name: '2-Sided', active: true },
{ name: '3-Sided', active: true },
{ name: '4-Sided', active: true },
]
const caps = ref(capOptions[0])
const sides = ref(sideOptions[0])
</script>
You can bind update/select event to the sides drop-down. In the event handler, based on the value chosen, you can loop through the caps array and set the active to true or false. I hope the names of capOptions and sideOptions are in sync
const capOptions = [
{ name: 'No Cap', active: true },
{ name: '2-Sided', active: true },
{ name: '3-Sided', active: true },
{ name: '4-Sided', active: true },
]
const sideOptions = [
{ name: '2-Sided', active: true },
{ name: '3-Sided', active: true },
{ name: '4-Sided', active: true },
]
onSideSelect(name) {
const cap = capOptions.find(cap =>
cap.name === name);
cap.active = false;
}
I have create a refinement component that sends data to its parent and it works correctly. after data goes to parent component, it will save to an array with useState. array value is ok when data from child component is incremental, but when the value from child component is decremented, the array value is not correct.
Here's the code:
Refinement.jsx
import { Fragment, useState } from "react";
const Refinement = (props) => {
let [fileTypeSelected, setFileTypeSelected] = useState([]);
let changingState = "";
const CheckBoxHandler = (event, content) => {
if (event) {
fileTypeSelected.push(content);
changingState = "Incremental"
}
else {
fileTypeSelected = fileTypeSelected.filter((c) => c !== content);
changingState = "Decremental"
}
}
return (
<Fragment>
<div className="w-full rounded-md overflow-hidden shadow-sm border">
<div className="flex justify-between items-center px-4 py-1 bg-biscay-700 text-white border-b">
<span className="font-semibold ">{props.RefineName}</span>
</div>
{
<div className={`px-4 py-2 w-full flex flex-col gap-2 transform transition-all duration-200 ${props.RefineValue.length <= 5 ? "" : "h-36 overflow-y-scroll scrollbar"} `}>
{
props.RefineValue.map(m => {
return (
<div className="text-sm flex justify-between items-center" key={m.id}>
<span>{m.displayName}</span>
<input className="accent-sky-500 w-4 h-4 rounded-md" onChange={(event) => {
CheckBoxHandler(event.target.checked, m.displayName);
props.onDataModified({
SearchCompnent: props.RefineName,
SearchItem: fileTypeSelected,
SearchState: changingState
});
}} type="checkbox" name="" id="" />
</div>
)
})
}
</div>
}
</div>
</Fragment>
)
}
export default Refinement;
App.jsx
import { Fragment, useState } from "react";
import Refinement from "../components/Refinement";
const App = () => {
const [fileFormatRefine, setfileFormatRefine] = useState([
{ id: 0, displayName: 'PDF' },
{ id: 1, displayName: 'DOCX' },
{ id: 2, displayName: 'PPTX' },
{ id: 3, displayName: 'XLSX' },
{ id: 4, displayName: 'TXT' },
{ id: 5, displayName: 'MP3' },
{ id: 6, displayName: 'MP4' },
{ id: 7, displayName: 'CS' },
]);
const [filterFormatModifiedDate, setFilterFormatModifiedDate] = useState([]);
let FileFormatQueryTemp = [];
const FileFormatModifiedEnvent = (enterdModifiedData) => {
const modifiedData = {
...enterdModifiedData
};
FileFormatQueryTemp = [];
for (let index = 0; index < modifiedData.SearchItem.length; index++) {
FileFormatQueryTemp.push([[modifiedData.SearchCompnent, modifiedData.SearchItem[index]]]);
}
setFilterFormatModifiedDate([]);
console.log(modifiedData.SearchItem.length);
setFilterFormatModifiedDate(FileFormatQueryTemp);
}
return (
<Fragment>
<div className="w-full lg:w-5/6 mx-auto flex gap-4 mt-16 px-4">
<div className="w-1/4 col-start-1 hidden lg:flex">
<form className="flex flex-col gap-4 w-full" id="Form" onSubmit={(event) => { event.preventDefault() }}>
<Refinement onDataModified={FileFormatModifiedEnvent} MinToShow={3} RefineValue={fileFormatRefine} RefineName="File Format"></Refinement>
<button className="h-8 text-white font-semibold text-sm rounded bg-red-600" onClick={() => { console.log(filterFormatModifiedDate.length) }}>Show Array Length</button>
</form>
</div>
<div className="lg:w-3/4 w-full flex flex-col gap-2">
<div className="w-full">
<h3 className="text-xs flex items-center font-semibold uppercase mb-2">Filters
<span className="h-4 w-4 ml-2 text-xs flex items-center justify-center rounded bg-gray-200 text-black">{filterFormatModifiedDate.length}</span>
</h3>
<div className="">
{
filterFormatModifiedDate.map(f => {
return (
<p key={f[0][1]} >{f[0]}</p>
)
})
}
</div>
</div>
</div>
</div>
</Fragment>
)
}
export default App;
if you wanna change fileTypeSelected state, you should do next:
setFileTypeSelected(prevArray => [...prevArray, content])
and in else scope:
setFileTypeSelected(prevValue => prevValue.filter((c) => c !== content));
You must always change state using his function, which comes inside array as second argument
let's consider this example
const [count,setCount] = useState(0)
setCount is the only function that can change count value
The problem is that the count needs to be subtracted from the total when the item on the list has been un-selected. So far it works when the item is selected and the count goes up. Maybe some kind of toggle is needed to subtract the total from the count. And if the count is zero it needs to show and empty div with no zero or numbers. thanks in advance if anyone can help out 😀🐍🏴☠️
Sandbox here : https://codesandbox.io/s/checkbox-counter-friday-28-fgp85?file=/src/App.js:0-948
export const DropdownMenu = (props) => {
const [selectedMenu, setSelectedMenu] = useState([]);
return (
<div className="flex justify-center relative -mr-10">
<div className="absolute top-10 w-240 opacity-100 bg-white -ml-48 shadow-md rounded-lg text-left p-3 z-30">
<span className="text-center inline-block w-3">
{selectedMenu.length}
</span>
{props.options &&
props.options.map((option, _idx) => (
<div key={option.id} className={props.titleAndSubtitleGroup}>
<div className="flex flex-row">
<input
onClick={() => {
!selectedMenu.includes(option) &&
setSelectedMenu((oldValue) => [...oldValue, option]);
}}
type="checkbox"
id={option.group}
className="bg-white border-2 rounded w-4 h-4 mt-1 flex flex-shrink-0 justify-center items-center mr-2 focus-within:border-blue-0"
/>
<label
htmlFor={option.groupId}
className={"no-underline hover:underline"}
>
{option.text}{" "}
</label>
</div>
</div>
))}
</div>
</div>
);
};
export default DropdownMenu;
import React from "react";
import DropdownMenu from "./DropdownMenu";
export const App = () => {
return (
<DropdownMenu
options={[
{
id: 1,
text: "Post",
icon: "",
url: "/#",
group: "01",
groupId: "01"
},
{
id: 2,
text: "Delete",
icon: "",
url: "/#",
group: "02",
groupId: "02"
},
{
id: 3,
text: "Forward",
icon: "",
url: "/#",
group: "03",
groupId: "03"
},
{
id: 4,
text: "Share",
icon: "",
url: "/#",
group: "04",
groupId: "04"
},
{
id: 5,
text: "Copy Link",
icon: "",
url: "/#",
group: "05",
groupId: "05"
}
]}
/>
);
};
export default App;```
You aren't removing elements from the selectedMenu array when the checkbox is unchecked.
I suggest using an object instead of an array to store a map of option id to checked status. Then get an array of values from the selectedMenu state and filter by what is selected to get a derived selected count.
const [selectedMenu, setSelectedMenu] = useState({});
...
<span className="text-center inline-block w-3">
{Object.values(selectedMenu).filter(Boolean).length}
</span>
or to conditionally only display non-zero values (this OFC can be optimized to be more DRY)
<span className="text-center inline-block w-3">
{!!Object.values(selectedMenu).filter(Boolean).length &&
Object.values(selectedMenu).filter(Boolean).length}
</span>
...
<input
onChange={e => {
const { id, checked } = e.target;
setSelectedMenu(selected => ({
...selected,
[id]: checked
}))
}}
type="checkbox"
id={option.group}
className="bg-white border-2 rounded w-4 h-4 mt-1 flex flex-shrink-0 justify-center items-center mr-2 focus-within:border-blue-0"
/>
OFC, if you wanted, or needed, to keep the array, then just conditionally filter the array, or append a value.
<input
onClick={() => {
selectedMenu.includes(option.id)
? setSelectedMenu((oldValue) =>
oldValue.filter((val) => val !== option.id)
)
: setSelectedMenu((oldValue) => [...oldValue, option.id]);
}}
type="checkbox"
id={option.group}
className="bg-white border-2 rounded w-4 h-4 mt-1 flex flex-shrink-0 justify-center items-center mr-2 focus-within:border-blue-0"
/>
I'm trying to add functionality to my addToCart button located in ProductCard. I need it to be disabled once the cupcake has been added to my cart array. When removed via the removeItem/ clearCart in MiniCart I need the button to be enabled again. I've tried if else statements and tried adding all kinds of functionality that I've Google's but have yet to succeed. I'd really appreciate some help ^^
store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
cart: [],
cupcake: [{
title: 'Cream',
price: 12.99,
image: 'https://images.pexels.com/photos/1055270/pexels-photo-1055270.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260',
id: 1,
quantity: 1
},
{
title: 'Choc',
price: 10.99,
image: 'https://images.pexels.com/photos/1055272/pexels-photo-1055272.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260',
id: 2,
quantity: 1
},
{
title: 'Frosting',
price: 14.99,
image: 'https://images.pexels.com/photos/1055271/pexels-photo-1055271.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260',
id: 3,
quantity: 1
},
{
title: 'Berry',
price: 9.99,
image: 'https://images.pexels.com/photos/3081657/pexels-photo-3081657.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260',
id: 4,
quantity: 1
},
{
title: 'Deluxe',
price: 19.99,
image: 'https://images.pexels.com/photos/1998634/pexels-photo-1998634.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260',
id: 5,
quantity: 1
},
{
title: 'Oreo',
price: 19.99,
image: 'https://images.pexels.com/photos/783274/pexels-photo-783274.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260',
id: 6,
quantity: 1
},
]
},
});
ProductCard.vue
<template>
<ul
class="
px-32
py-16
grid
row-auto
gap-10
grid-cols-1
md:grid-cols-2
xl:grid-cols-3
2xl:grid-cols-4
"
>
<li
class="bg-white rounded-lg"
v-for="cupcakes in cupcake"
:key="cupcakes.id"
>
<img
class="rounded-md w-screen object-cover max-h-60"
:src="cupcakes.image"
/>
<div class="py-2 px-8 text-gray-600">
<span class="px-10 text-xl font-bold"> {{ cupcakes.title }}</span>
<span class="px-10 text-xl font-bold">${{ cupcakes.price }}</span>
<button
class="
bg-purple-200
font-bold
px-3
mt-2
text-xl
py-4
mb-4
w-full
rounded-md
transition-all
hover:bg-purple-300
"
type="button"
v-on:click="addToCart(cupcakes)"
>
Add to Cart
</button>
</div>
</li>
</ul>
</template>
<script>
export default {
computed: {
cupcake() {
return this.$store.state.cupcake;
},
},
methods: {
addToCart(cupcakes) {
this.$store.state.cart.push({
title: cupcakes.title,
price: cupcakes.price,
image: cupcakes.image,
id: cupcakes.id,
quantity: cupcakes.quantity,
});
},
},
};
</script>
Minicart.vue
<template>
<div class="bg-white border-2 border-gray-500 rounded-md absolute right-16">
<div
class="grid grid-cols-2 gap-20 m-5"
v-for="carts in cart"
:key="carts.id"
>
<img class="w-24" :src="carts.image" alt="" />
<div class="grid grid-rows-3">
<strong class="tracking-wider font-bold">{{ carts.title }}</strong>
<p class="tracking-wider font-bold">
{{ carts.quantity }} x ${{ carts.price }}
</p>
<button
class="bg-gray-500 rounded p-2 tracking-wider font-bold text-white"
v-on:click="removeItem(carts)"
>
remove
</button>
<button type="button" v-on:click="increase(carts)">Increase</button>
<button type="button" v-on:click="deccrease(carts)">Deccrease</button>
</div>
</div>
<div class="flex mx-5 my-8 justify-between">
<span
class="tracking-wider text-xl p-4 font-bold justify-center align-middle"
>Total: ${{ total }}</span
>
<button
v-on:click="clearCart"
type="button"
href=""
class="
bg-red-400
p-4
rounded
tracking-wider
text-white text-xl
font-bold
"
>
Clear Cart
</button>
</div>
</div>
</template>
<script>
export default {
computed: {
cart() {
return this.$store.state.cart;
},
total: function () {
let total = 0;
for (let carts of this.$store.state.cart) {
total += carts.price * carts.quantity;
}
return total.toFixed(2);
},
},
methods: {
removeItem: function (carts) {
this.$store.state.cart.splice(carts, 1);
},
increase: function (carts) {
carts.quantity += 1;
},
deccrease: function (carts) {
if (carts.quantity > 1) {
carts.quantity -= 1;
} else {
this.$store.state.cart.splice(carts, 1);
}
},
clearCart: function () {
let length = this.$store.state.cart.length;
this.$store.state.cart.splice(0, length);
console.log(length);
},
},
};
</script>
Just like that. Demo https://codesandbox.io/s/jolly-shirley-dgg10
Template:
<button
class="bg-purple-200 font-bold px-3 mt-2 text-xl py-4 mb-4 w-full
rounded-md transition-all hover:bg-purple-300"
type="button"
v-on:click="addToCart(cupcakes)"
:disabled="checkCart(cupcakes.id)"
>
Add to Cart
</button>
Methods:
checkCart(id) {
return this.$store.state.cart.find((item) => item.id === id);
},
If you add this disabled-method to your button, it will be disabled, if the cupcake is already in the cart, and you can still add the other cupcakes:
<button
class="
...
"
type="button"
v-on:click="addToCart(cupcakes)"
:disabled="{ cart.includes(cupcakes) }"
>
Add to Cart
</button>
Your AddToCart-function could also be rewritten to:
addToCart(cupcakes) {
this.$store.state.cart.push(cupcakes);
},
Before using ListBox:
store/index.ts
import { action, makeObservable, observable } from 'mobx'
import type { IFrameItStore, TrafficSignal } from '#/types/index'
export class FrameItStore implements IFrameItStore {
trafficSignal: TrafficSignal = {
shape: 'circle',
}
constructor() {
makeObservable(this, {
trafficSignal: observable,
updateTrafficSignal: action.bound,
})
}
updateTrafficSignal({ shape }: TrafficSignal) {
if (shape) this.trafficSignal.shape = shape
}
}
Shape.tsx
import { observer } from 'mobx-react'
import * as React from 'react'
import { useFrameItStore } from '#/store/index'
import type { TrafficSignalShape } from '#/types/index'
export const Shape = observer(() => {
const frameItStore = useFrameItStore()
return (
<>
<label htmlFor="shape" className="mb-1 text-sm font-medium text-blue-gray-500">
Shape
</label>
<select
id="shape"
className="block w-full px-3 py-2 mb-2 bg-white border border-gray-300 rounded-md shadow-sm text-blue-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
value={frameItStore.trafficSignal.shape}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const shape = e.target.value as TrafficSignalShape
frameItStore.updateTrafficSignal({ shape })
}}
>
<option value="circle">Circle</option>
<option value="square">Square</option>
</select>
</>
)
})
App.tsx
<Shape />
After using ListBox:
Select.tsx
import * as React from 'react'
import { Listbox, Transition } from '#headlessui/react'
import clsx from 'clsx'
import { Selector, Check } from '#/components/icons/index'
type Option = {
id: string
name: string
img: string
}
interface IProps {
label?: string
options: Array<Option>
}
export const Select = ({ label, options }: IProps) => {
const [selectedOption, setSelectedOption] = React.useState<Option>(options[0])
return (
<Listbox value={selectedOption} onChange={setSelectedOption}>
{({ open }) => (
<>
<Listbox.Label className="mb-1 text-sm font-medium text-blue-gray-500">
{label}
</Listbox.Label>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white border border-gray-300 rounded-md shadow-sm cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="flex items-center">
<img
src={selectedOption.img}
alt={selectedOption.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span className="block ml-3 truncate">{selectedOption.name}</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 ml-3 pointer-events-none">
<Selector />
</span>
</Listbox.Button>
<div className="absolute w-full mt-1 bg-white rounded-md shadow-lg">
<Transition
show={open}
leave="transition duration-100 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
static
className="py-1 overflow-auto text-base rounded-md max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
{options.map((option) => (
<Listbox.Option as={React.Fragment} key={option.id} value={option}>
{({ active, selected }) => (
<li
className={clsx('relative py-2 pl-3 cursor-default select-none pr-9', {
'text-white bg-indigo-600': active,
'text-gray-900': !active,
})}
>
<div className="flex items-center">
<img
src={option.img}
alt={option.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span
className={clsx('ml-3 block truncate', {
'font-semibold': selected,
'font-normal': !selected,
})}
>
{option.name}
</span>
</div>
{selected && (
<span
className={clsx('absolute inset-y-0 right-0 flex items-center pr-4', {
'text-white': active,
'text-indigo-600': !active,
})}
>
<Check />
</span>
)}
</li>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</div>
</>
)}
</Listbox>
)
}
App.tsx
const shapes = [
{
id: '1',
name: 'Circle',
img:
'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
{
id: '2',
name: 'Square',
img:
'https://images.unsplash.com/photo-1491528323818-fdd1faba62cc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80',
},
]
<Select label="Shape" options={shapes} />
How do I convert the After part to use MobX like the Before part?
I tried passing value & onChange as it is in the Before part to Select like:
App.tsx
<Select
label="Shape"
options={shapes}
value={frameItStore.trafficSignal.shape}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => {
const shape = e.target.value as TrafficSignalShape
frameItStore.updateTrafficSignal({ shape })
}}
/>
Select.tsx
interface IProps {
label?: string
value: any
onChange: (value: any) => void
options: Array<Option>
}
export const Select = ({ label, options, value, onChange }: IProps) => {
const [selectedOption, setSelectedOption] = React.useState<Option>(options[0])
return (
<Listbox value={value} onChange={onChange}>
.
.
.
</Listbox>
)
}
But it doesn't select anything & I don't know what to do of selectedOption?
Alright I solved it. Removed the local hook state & just used the MobX state. Also, had 1 minor issue. I was setting the value as uppercase in the store when the store originally had lowercase values. The uppercase values were only for display in the UI.
Here's the modified code that works:
App.tsx
<Select
label="Shape"
options={shapes}
value={shapes.filter({ name }) => name.toLowerCase() === frameItStore.trafficSignal.shape)[0]}
onChange={(value) => {
const shape = value.toLowerCase() as TrafficSignalShape
frameItStore.updateTrafficSignal({ shape })
}}
/>
Select.tsx
import * as React from 'react'
import { Listbox, Transition } from '#headlessui/react'
import clsx from 'clsx'
import { Selector, Check } from '#/components/icons/index'
type Option = {
id: string
name: string
img: string
}
interface IProps {
label?: string
value: Option
onChange: (name: string) => void
options: Array<Option>
}
export const Select = ({ label, options, value, onChange }: IProps) => {
return (
<Listbox
value={value}
onChange={(value: Option) => {
onChange(value.name)
}}
>
{({ open }) => (
<>
<Listbox.Label className="mb-1 text-sm font-medium text-blue-gray-500">
{label}
</Listbox.Label>
<div className="relative mt-1">
<Listbox.Button className="relative w-full py-2 pl-3 pr-10 text-left bg-white border border-gray-300 rounded-md shadow-sm cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="flex items-center">
<img
src={value.img}
alt={value.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span className="block ml-3 truncate">{value.name}</span>
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 ml-3 pointer-events-none">
<Selector />
</span>
</Listbox.Button>
<div className="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg">
<Transition
show={open}
leave="transition duration-100 ease-in"
leaveFrom="transform opacity-100"
leaveTo="transform opacity-0"
>
<Listbox.Options
static
className="py-1 overflow-auto text-base rounded-md max-h-56 ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
>
{options.map((option) => {
return (
<Listbox.Option as={React.Fragment} key={option.id} value={option}>
{({ active, selected }) => {
return (
<li
className={clsx(
'relative py-2 pl-3 cursor-default select-none pr-9',
{
'text-white bg-indigo-600': active,
'text-gray-900': !active,
}
)}
>
<div className="flex items-center">
<img
src={option.img}
alt={option.name}
className="flex-shrink-0 w-6 h-6 rounded-full"
/>
<span
className={clsx('ml-3 block truncate', {
'font-semibold': selected,
'font-normal': !selected,
})}
>
{option.name}
</span>
</div>
{selected && (
<span
className={clsx(
'absolute inset-y-0 right-0 flex items-center pr-4',
{
'text-white': active,
'text-indigo-600': !active,
}
)}
>
<Check />
</span>
)}
</li>
)
}}
</Listbox.Option>
)
})}
</Listbox.Options>
</Transition>
</div>
</div>
</>
)}
</Listbox>
)
}