Use ListBox
from @headlessui/react
with Mobx?
#2729
-
I'm trying to make https://github.com/tailwindlabs/headlessui/tree/develop/packages/%40headlessui-react#listbox-select work with MobX. The demo of the below example is similar to the 1st one (Custom with Avatar) on https://tailwindui.com/components/application-ui/forms/select-menus Before using ListBox:store/index.tsimport { 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.tsximport { 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.tsximport * 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.tsxconst 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 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.tsxinterface 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 |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 2 replies
-
Okay so I got close with the answer. However, the checkmark is not working beside the selected App.tsx<Select
label="Shape"
options={shapes}
value={shapes.find(
(shape) => shape.name.toLowerCase() === frameItStore.trafficSignal.shape
)}
onChange={(value) => {
const shape = value as TrafficSignalShape
frameItStore.updateTrafficSignal({ shape })
}}
/> Select.tsximport * 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: any
onChange: (value: any) => void
options: Array<Option>
}
export const Select = ({ label, options, value, onChange }: IProps) => {
const [selectedOption, setSelectedOption] = React.useState<Option>(value)
return (
<Listbox
value={selectedOption}
onChange={(value: any) => {
setSelectedOption(value)
onChange(value)
}}
>
{({ 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>
)
} I'm not sure how the checkmark is not working? Would love to know what I'm doing wrong? |
Beta Was this translation helpful? Give feedback.
-
If there is still some question in there, please focus on providing a minimal example of the problem. Nobody is going to dive into your deeply nested code, sorry. |
Beta Was this translation helpful? Give feedback.
-
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.tsximport * 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>
)
} |
Beta Was this translation helpful? Give feedback.
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.tsx