I have this awesome Tailwind-based dropdown component, which is built using the Listbox component from Headless UI.
I've posted the whole thing here, because I am sure I have to change something in the HTML to define the right role or so.
import { Fragment, useState } from "react";
import { Listbox, Transition } from "#headlessui/react";
const examples = [
"collatz",
"comparison",
"conditional",
"fibonacci",
"game-of-life-4x4",
"nprime",
];
function classExamples(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}
type DropDownProps = {
onExampleValueChange?: (newType: string) => void;
};
export default function DropDown({ onExampleValueChange }: DropDownProps): JSX.Element {
const [selected, setSelected] = useState(examples[1]);
return (
<Listbox
value={selected}
onChange={(value) => {
onExampleValueChange?.(value);
setSelected(value);
}}
>
{({ open }) => (
<>
<div className="relative mt-1" role="listbox">
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
<span className="block truncate">{selected}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9"
/>
</svg>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="absolute z-10 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" role="options">
{examples.map((examples) => (
<Listbox.Option
key={examples}
className={({ active }) =>
classExamples(
active ? "text-white bg-indigo-600" : "text-gray-900",
"relative cursor-default select-none py-2 pl-3 pr-9"
)
}
value={examples}
>
{({ selected, active }) => (
<>
<span
className={classExamples(
selected ? "font-semibold" : "font-normal",
"block truncate"
)}
>
{examples}
</span>
{selected ? (
<span
className={classExamples(
active ? "text-white" : "text-indigo-600",
"absolute inset-y-0 right-0 flex items-center pr-4"
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
);
}
So now I want to test that the selected value changes when a user picks an example.
import { fireEvent, render, screen } from "#testing-library/react";
import React from "react";
import DropDown from "../src/components/DropDown";
...
/** Testing the Dropdown Component */
describe("Rendering CodingEnvironment ", () => {
it("should call onChange at the Dropdown", async () => {
render(<DropDown />)
const select = screen.getByRole('listbox')
fireEvent.change(select, { target: { value: 'comparison' } })
expect(select).toBe('comparison')
});
});
But of course, it doesn't work the way I intended. Basically, Jest tells me "The given element does not have a value setter". Which is true, but when I try
const select = screen.getByRole('listbox').querySelector('options')
then it also doesn't work. I am unsure which element in my component has a setter that's causing this problem.
Related
I want the first option to be selected by default in dropdown. I tried combobox defaultValue property but didn't work. How can i do this?
Combobox Component
import { useState } from 'react'
import { CheckIcon, ChevronUpDownIcon } from '#heroicons/react/20/solid'
import { Combobox } from '#headlessui/react'
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export default function FormCombobox({
comboboxData,
label,
questionRange,
setQuestionRange,
}) {
const [query, setQuery] = useState('')
let items = comboboxData.map((item) => ({
id: item.content_object.nanoid,
name: item.content_object.name,
multiplier: item.multiplier,
}))
const filteredItems =
query === ''
? items
: items.filter((item) => {
return item.name.toLowerCase().includes(query.toLowerCase())
})
return (
<Combobox
as="div"
value={questionRange}
onChange={setQuestionRange}
className="my-5"
>
<Combobox.Label className="block text-left font-bold text-gray-700">
{label}
</Combobox.Label>
<div className="relative mt-1">
<Combobox.Input
className="w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm"
onChange={(event) => setQuery(event.target.value)}
displayValue={(item) => item?.name}
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</Combobox.Button>
{filteredItems.length > 0 && (
<Combobox.Options className="absolute z-10 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">
{filteredItems.map((item) => (
<Combobox.Option
key={item.id}
value={item}
className={({ active }) =>
classNames(
'relative cursor-default select-none py-2 pl-3 pr-9',
active ? 'bg-indigo-600 text-white' : 'text-gray-900'
)
}
>
{({ active, selected }) => (
<>
<span
className={classNames(
'block truncate',
selected && 'font-semibold'
)}
>
{item.name}
</span>
{selected && (
<span
className={classNames(
'absolute inset-y-0 right-0 flex items-center pr-4',
active ? 'text-white' : 'text-indigo-600'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
)}
</div>
</Combobox>
)
}
initialize the state of questionRange when declaring it .
const [questionRange, setQuestionRange] = useState(items[0])
I'm building an application where a list of images is available at the left sidebar, and on the right, there will be a div where we can drop the images.
I'm using the react-dnd library. I have followed the steps as shown in the docs, but I'm getting the error dropped is not function when I drop an image on the target.
sidebar.js
import React, { useState, createContext } from "react";
import NFTCards from "./NFTCards";
import { NFTDATA } from "../utils/data";
import uuid from "react-uuid";
import SiteLogo from "../widgets/SiteLogo";
export const SelectedNFTContext = createContext({ dropped: null });
function Sidebar() {
const [nftList, setNftList] = useState([...NFTDATA]);
const dropped = (id) => {
console.log(nftList);
const selectedNFT = nftList.filter((nft, i) => nft.id === id);
selectedNFT[0].status = "selected";
setNftList(
nftList.filter((nft, i) => nft.id !== id).concat(selectedNFT[0])
);
};
const searchNFT = (e) => {};
return (
<aside className="w-96" aria-label="Sidebar">
<div className="overflow-y-auto py-4 px-3 bg-gray-50 rounded h-screen dark:bg-gray-800">
{/* Sidebar Logo */}
<a href="/" className="flex items-center text-center pl-2.5 mb-5">
<SiteLogo className="mr-3 mt-6 h-12 sm:h-7" alt="Site Logo" />
</a>
{/* Search Bar */}
<div>
<form className="my-16">
<label
htmlFor="default-search"
className="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-gray-300"
>
Search
</label>
<div className="relative">
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
<svg
aria-hidden="true"
className="w-5 h-5 text-gray-500 dark:text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
></path>
</svg>
</div>
<input
onChange={searchNFT}
type="search"
id="default-search"
className="block p-4 pl-10 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Search NFTs & Collections..."
required=""
/>
<button
type="submit"
className="text-white absolute right-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-[#14E2B2] dark:text-black dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Search
</button>
</div>
</form>
</div>
<SelectedNFTContext.Provider value={{ dropped }}>
<div className="space-y-8 ">
{nftList
.filter((nft) => nft.status === "unselect")
.map((nft) => (
<NFTCards
index={nft.id}
key={uuid()}
id={nft.id}
imgURL={nft.imgURL}
title={nft.title}
/>
))}
</div>
</SelectedNFTContext.Provider>
</div>
</aside>
);
}
export default Sidebar;
Droppable.js
import React from "react";
import { useDrop } from "react-dnd";
import AnimatedButton from "../widgets/buttons";
import { SelectedNFTContext } from "./Sidebar";
function Dropabble() {
const dropped = React.useContext(SelectedNFTContext);
console.log(dropped);
const [{ isOver }, dropRef] = useDrop({
accept: "image",
drop: (item, monitor) => dropped(item.id),
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
});
return (
<div>
<h1 className="text-white ml-32 mt-24 text-4xl">
Drop here NFTs to Mint
</h1>
{/* Drag and Drop */}
<div
ref={dropRef}
className={
isOver
? "w-[50vw] h-[50vh] my-16 ml-32 border-dashed border-8 border-green-500 border-spacing-4"
: "w-[50vw] h-[50vh] my-16 ml-32 border-dashed border-8 border-spacing-4"
}
></div>
<AnimatedButton
onClickFunc={() => alert("Minted")}
className="relative bg-background text-2xl px-6 py-2 border-2 border-[#14E2B2] hover:text-black hover:bg-[#14E2B2] hover:transition-all rounded ml-32 my-6"
buttonName="Mint"
/>
</div>
);
}
export default Dropabble;
nftcards.js
import React from "react";
import { useDrag } from "react-dnd";
// import { NFTDATA } from "../utils/data";
function NFTCards({ index, id, imgURL, title }) {
const [{ isDragging }, dragRef] = useDrag({
type: "image",
collect: (monitor) => ({
isDragging: !!monitor.isDragging(),
}),
});
return (
<ul index={index}>
<li ref={dragRef} className={isDragging ? "border-2 " : "border-0 "}>
<img
className=" text-center w-full h-80 bg-cover object-cover"
src={imgURL}
alt={title}
/>
</li>
</ul>
);
}
export default NFTCards;
Please leave a hint where I'm doing wrong or link to the best resource.
Thanks for the help
Link to GitHub repository 👇
Repo Link
You have
<SelectedNFTContext.Provider value={{ dropped }}>
which set the value of the context to the object {dropped},
and
const dropped = React.useContext(SelectedNFTContext);
which sets dropped to the value of the context (which is not a function but an object containing a function).
Either remove one pair of curly braces around dropped in the first place, or add one pair of curly braces around dropped in the second place and it should work.
My delete callback is sending the correct id but I tested out the reducer with a few console logs and it's not working correctly.So I have a list of components in a menu that I can add to the "board". I notice that if I add only notes for example, it will delete the last one every time. But if I had one note, then one todo list, and another note, if I try to delete the first note, it will delete that one correctly.
reducer
export default function reducer(state: any, action: any) {
switch (action.type) {
case "ADD_COMPONENT":
return {
...state,
components: [...state.components, action.payload],
};
case "DELETE_COMPONENT":
return {
...state,
components: state.components.filter(
// actual code
(component: any) => component.id !== action.payload),
//test code to check what ids were being passed in
//(component: any) => { console.log(component.id, action.payload), console.log(component.id !== action.payload), component.id == action.payload }),
};
default:
return state;
}
}
The Delete case has a line I was using to test to see what ids were being passed in.
The context
import React, { createContext, useReducer, useState } from "react";
import ComponentReducer from "./ComponentReducer";
const NewComponentState: NewComponentsState = {
components: [],
addComponent: () => {},
deleteComponent: () => {},
};
export const NewComponentContext =
React.createContext<NewComponentsState>(NewComponentState);
export const NewComponentProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(ComponentReducer, NewComponentState);
const addComponent = (component: any) => {
dispatch({
type: "ADD_COMPONENT",
payload: component
});
};
const deleteComponent = (id: any) => {
dispatch({
type: "DELETE_COMPONENT",
payload: id,
});
};
return (
<NewComponentContext.Provider
value={{ components: state.components, deleteComponent, addComponent }}
>
{children}
</NewComponentContext.Provider>
);
};
The Note component that I've been using to test this
import { Menu,Transition } from "#headlessui/react";
import React, { useState, Fragment, useContext } from "react";
import NoteColorChanger from "./NoteColor";
import Draggable from 'react-draggable';
import { NewComponentContext } from "../../../Context/NewComponentContext";
interface INote {
id: any
}
const Note: React.FC <INote> = ({ id }) => {
const [content, setContent] = useState<string>()
const [title, setTitle] = useState<string>()
const [color, setColor] = useState<any>()
const [position, setPosition] = useState<any>({x: 0, y: 0})
const { components, deleteComponent } = useContext(NewComponentContext)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setContent(event.target.value)
};
const handleColor = (notecolor: any) => {
setColor(notecolor)
}
const trackPosition = (pos:any) => {
setPosition({x: pos.x, y: pos.y})
}
const deleteNote = () => {
deleteComponent(id)
}
return (
<div className={`${color} h-64 w-64 bg-yellow-200 text-black rounded-lg p-2 shadow-lg`}>
<div className="flex justify-between items-center pb-6">
<h1 className="font-bold font-Inter">Note</h1>
<Menu>
<Menu.Button>
<div className={`hover:${color} p-1 rounded-lg ease-in-out duration-100`}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"
/>
</svg>
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
as="div"
className={`bg-gray-100 font-Inter w-64 shadow-lg rounded-lg absolute translate-y-24 -translate-x-2 z-50`}
>
<Menu.Item>
{({ active }) => (
<div
id="color"
className={` flex items-center py-2 px-3 rounded-lg w-full`}
>
{<NoteColorChanger handleColor={handleColor}/>}
</div>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
id="Todo"
onClick={() => console.log(id)}
className={`${
active ? "bg-blue-500 text-white" : "text-black"
} flex items-center py-2 px-3 rounded-lg w-full`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Edit Title
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<li
onClick={deleteNote}
className={`${
active ? "bg-blue-500 text-white" : " text-black"
} flex items-center py-2 px-3 cursor-pointer rounded-lg`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Delete Note
</li>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
<textarea
className={`${color} bg-yellow-200 font-Inter w-full h-48 border-none focus:border-none focus:ring-0 resize-none`}
onChange={() => {handleChange}}
/>
</div>
);
};
export default Note;
Menu where the components are created
import React, { useContext, Fragment, useState } from "react"
import { NewComponentContext } from "../../Context/NewComponentContext"
import { Menu, Transition } from '#headlessui/react'
import Note from "./Note/Note";
import TodoList from "./Todo/TodoList";
import Photo from "./Photo/Photo";
const NewComponentMenu = () => {
const { addComponent } = useContext(NewComponentContext)
const newComponent = (componentType: number) => {
addComponent({id: Math.floor(Math.random() * 100000000), componentType: componentType})
}
return (
<div className="w-6 h-6 mt-4 ml-4 shadow-md text-gray-800 font-Inter z-50">
<Menu>
<Menu.Button>
<div className="p-1 rounded-lg bg-blue-500 hover:bg-blue-600 ease-in-out duration-100">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-blue-50"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items as="div" className="w-60 shadow-lg rounded-lg bg-white">
<h1 className="text-center font-Inter text-2xl pb-2">Items</h1>
<Menu.Item>
{({ active }) => (
<button
id="Todo"
onClick={() => newComponent(1)}
className={`${
active ? "bg-blue-500 text-white" : "text-black"
} flex items-center py-2 px-3 rounded-lg w-full`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg>
Todo list
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<li
//onClick={() => setComponent(<Moodboard />) }
className={`${
active ? "bg-blue-500 text-white" : " text-black"
} flex items-center py-2 px-3 cursor-pointer rounded-lg`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Mood Board
</li>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<li
onClick={() => newComponent(2) }
className={`${
active ? "bg-blue-500 text-white" : " text-black"
} flex items-center py-2 px-3 cursor-pointer rounded-lg`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Image
</li>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<li
onClick={() => newComponent(3)}
className={`${
active ? "bg-blue-500 text-white" : " text-black"
} flex items-center py-2 px-3 cursor-pointer rounded-lg`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Note
</li>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
);
};
export default NewComponentMenu;
component where the menu items are created
import React, { useContext } from "react";
import { NewComponentContext } from "../Context/NewComponentContext";
import NewComponentMenu from "./NewComponents/NewComponentMenu";
import Note from "./NewComponents/Note/Note";
import Photo from "./NewComponents/Photo/Photo";
import TodoList from "./NewComponents/Todo/TodoList";
const Board = () => {
const { components } = useContext(NewComponentContext);
const componentList = components.map((component, i) => {
if (component.componentType == 1) {
return (
<div key={i}>
<TodoList id={component.id} />
</div>
);
}
else if (component.componentType == 2) {
return (
<div key={i}>
<Photo id={component.id} />
</div>
)
}
else {
return (
<div key={i}>
<Note id={component.id} />
</div>
)
}
});
return (
<div className="flex space-x-10 mt-8">
<NewComponentMenu />
<div className="grid grid-cols-6 gap-8">{componentList}</div>
</div>
);
};
export default Board;
It is really a good practice to use key attribute while rendering a collection, it helps react to rerender collections of components correctly. So i can assume you should try to add unique key attribute to the place, where you render Note component, e.g <Note key={'some unique id'} />
There is an issue with your delete case in reducer. You are not returning the boolean, so no items will be returned,
case "DELETE_COMPONENT":
return {
...state,
components: state.components.filter(
(component: any) => {
console.log(component.id, action.payload)
console.log(component.id !== action.payload)
return component.id !== action.payload // here is the fix
})
};
I think it's mainly an issue with my reducer function but basically I'm trying to add and delete components. Adding works fine. Deleting doesn't work correctly. I put a console log in the onClick when I create a new component and it shows unique ids but it seems like it's not getting passed correctly into the reducer or something. If anyone has any ideas, I'm all ears.
Reducer function
export default function reducer(state: any, action: any) {
switch (action.type) {
case "ADD_COMPONENT":
return {
...state,
components: [...state.components, action.payload],
};
case "DELETE_COMPONENT":
return {
...state,
components: state.components.filter(
(component: any) => component.id == action.payload
),
};
default:
return state;
}
}
Here for the Delete, it's filtering and checking for an id, with it this way (component.id == action.payload), it deletes all of the components. If I have !=== action.payload, it doesn't delete anything.
Context
import React, { createContext, useReducer, useState } from "react";
import ComponentReducer from "./ComponentReducer";
const NewComponentState: NewComponentsState = {
components: [],
addComponent: () => {},
deleteComponent: () => {},
};
export const NewComponentContext =
React.createContext<NewComponentsState>(NewComponentState);
export const NewComponentProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(ComponentReducer, NewComponentState);
const addComponent = (component: any) => {
dispatch({
type: "ADD_COMPONENT",
payload: component
});
};
const deleteComponent = (id: any) => {
dispatch({
type: "DELETE_COMPONENT",
payload: id,
});
};
return (
<NewComponentContext.Provider
value={{ components: state.components, deleteComponent, addComponent }}
>
{children}
</NewComponentContext.Provider>
);
};
Notes component. This is the component I've been testing this with
import { Menu,Transition } from "#headlessui/react";
import React, { useState, Fragment, useContext } from "react";
import NoteColorChanger from "./NoteColor";
import Draggable from 'react-draggable';
import { NewComponentContext } from "../../../Context/NewComponentContext";
interface INote {
id: any
}
const Note: React.FC <INote> = ({ id }) => {
const [content, setContent] = useState<string>()
const [title, setTitle] = useState<string>()
const [color, setColor] = useState<any>()
const [position, setPosition] = useState<any>({x: 0, y: 0})
const {deleteComponent} = useContext(NewComponentContext)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setContent(event.target.value)
};
const handleColor = (notecolor: any) => {
setColor(notecolor)
}
const trackPosition = (pos:any) => {
setPosition({x: pos.x, y: pos.y})
}
return (
<div className={`${color} h-64 w-64 bg-yellow-200 text-black rounded-lg p-2 shadow-lg`}>
<div className="flex justify-between items-center pb-6">
<h1 className="font-bold font-Inter">Note</h1>
<Menu>
<Menu.Button>
<div className={`hover:${color} p-1 rounded-lg ease-in-out duration-100`}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"
/>
</svg>
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
as="div"
className={`bg-gray-100 font-Inter w-64 shadow-lg rounded-lg absolute translate-y-24 -translate-x-2 z-50`}
>
<Menu.Item>
{({ active }) => (
<div
id="color"
className={` flex items-center py-2 px-3 rounded-lg w-full`}
>
{<NoteColorChanger handleColor={handleColor}/>}
</div>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
id="Todo"
className={`${
active ? "bg-blue-500 text-white" : "text-black"
} flex items-center py-2 px-3 rounded-lg w-full`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Edit Title
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<li
onClick={() => deleteComponent(id)}
className={`${
active ? "bg-blue-500 text-white" : " text-black"
} flex items-center py-2 px-3 cursor-pointer rounded-lg`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Delete Note
</li>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
<textarea
className={`${color} bg-yellow-200 font-Inter w-full h-48 border-none focus:border-none focus:ring-0 resize-none`}
onChange={() => {handleChange}}
/>
</div>
);
};
export default Note;
menu component
import React, { useContext, Fragment, useState } from "react"
import { NewComponentContext } from "../../Context/NewComponentContext"
import { Menu, Transition } from '#headlessui/react'
import Note from "./Note/Note";
import TodoList from "./Todo/TodoList";
import Photo from "./Photo/Photo";
const NewComponentMenu = () => {
const { addComponent } = useContext(NewComponentContext)
const [id, setId] = useState<number>(0)
const newComponent = (component: any) => {
setId(Math.floor(Math.random() * 100000000))
addComponent(component)
}
return (
<div className="w-6 h-6 mt-4 ml-4 shadow-md text-gray-800 font-Inter z-50">
<Menu>
<Menu.Button>
<div className="p-1 rounded-lg bg-blue-500 hover:bg-blue-600 ease-in-out duration-100">
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 text-blue-50"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clipRule="evenodd"
/>
</svg>
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items as="div" className="w-60 shadow-lg rounded-lg bg-white">
<h1 className="text-center font-Inter text-2xl pb-2">Items</h1>
<Menu.Item>
{({ active }) => (
<li
onClick={() => newComponent(<Note id={id}/>) }
className={`${
active ? "bg-blue-500 text-white" : " text-black"
} flex items-center py-2 px-3 cursor-pointer rounded-lg`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6 mr-3"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
Note
</li>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
);
};
export default NewComponentMenu;
Issue
The issue you have is that you are not passing objects that have an id property to the addComponent callback. You are passing a JSX literal. Storing JSX into any React state is generally an anti-pattern, you should be storing the data and mapping it to JSX when rendering.
Solution
Pass an object with id property on it. You just need a function to generate an id when updating the state, so the id state in NewComponentMenu is unnecessary.
const NewComponentMenu = () => {
const { addComponent } = useContext(NewComponentContext);
const newComponent = () => {
addComponent({
id: Math.floor(Math.random() * 100000000),
});
};
return (
...
<Menu.Item>
{({ active }) => (
<li
onClick={newComponent}
className={`${
active ? "bg-blue-500 text-white" : " text-black"
} flex items-center py-2 px-3 cursor-pointer rounded-lg`}
>
...
</li>
)}
</Menu.Item>
...
);
};
Now that you are storing data element objects with an id property, the delete case should work.
When rendering the state.components you will need to map to the Note components.
const { components } = useContext(NewComponentContext);
...
{components.map(el => <Note id={el.id}/>)}
I am using the Headless Ui Popover.Button and I am not displaying any default text so I want the Chevron to show to the right. it does this when I have an item selected but not when nothing is in the box.
<Popover.Button className="flex justify-between w-60 text-sm font-bold uppercase items-center h-14 px-4 border-t border-b border-black rounded-none">
{
types.map((type) => (type.isSelected ? type.name : ''))
}
{open ? (
<ChevronUpIcon className="h-5 w-5 text-black" />
) : (
<ChevronDownIcon className="h-5 w-5 text-black" />
)}
</Popover.Button>
if i change the justify-between to justify-end it moves it but then the selected text is moved to the right as well.
any help would be great.
Thanks
Just wrap the first part in another div.
<div>
{
types.map((type) => (type.isSelected ? type.name : ''))
}
</div>
var checkbox = document.querySelector("input");
var buttonText = document.querySelector("#buttonText");
checkbox.addEventListener('change', (event) => {
if (event.currentTarget.checked) {
buttonText.innerHTML = "Item 1"
} else {
buttonText.innerHTML = ""
}
})
<link href="https://unpkg.com//tailwindcss#2.1.1/dist/tailwind.min.css" rel="stylesheet" />
<button class="flex justify-between w-60 text-sm font-bold uppercase items-center h-14 px-4 border-t border-b border-black rounded-none">
<div id="buttonText">
</div>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<input type="checkbox" class="mt-4">Item 1</input>