react-querybuilder / react-querybuilder

The Query Builder component for React
https://react-querybuilder.js.org/
MIT License
1.05k stars 238 forks source link

Shadcn / Tailwind example #672

Open sowka1995 opened 2 months ago

sowka1995 commented 2 months ago

Description of the feature

Are there any plans to add a new theme based on the growing popularity of shadcn or pure Tailwind?

Maybe someone already has a demo?

jakeboone02 commented 2 months ago

I've never used shadcn before, but for tailwind you can add the utility classes to each component with the controlClassnames prop: https://react-querybuilder.js.org/docs/components/querybuilder#controlclassnames

I don't have an example specifically for tailwind but the Tremor example uses tailwind internally: https://github.com/react-querybuilder/react-querybuilder/tree/main/examples/tremor

jakeboone02 commented 2 months ago

I was going to close this but I think I'll keep it open as a request for a pure Tailwind example.

jide commented 1 month ago

I'm trying to implement this, still very rough but I'll share progress and code.

Implementing a real theme might not work since shadcn/ui is based on copying/pasting components, so there's no way to import those as you would in a classic UI library.

Capture d’écran 2024-03-22 à 01 18 14
jakeboone02 commented 1 month ago

shadcn/ui is based on copying/pasting components

Yeah, when I was looking into it I couldn't figure out how that was a benefit. That said, you could theoretically replace all the components with custom components. A lot of the default logic is in hooks so it might not actually be a huge amount of work.

jide commented 1 month ago

Well that's the selling point of shadcn/ui, you own the ui library, so customizing it is as easy as editing files directly.

It has its upsides and downsides, but for example if I wanted to customize @react-querybuilder/chakra, I would have to fork it or override classes, which may not cover all my needs.

On the other hand, with creating a react-querybuilder-shadcn-ui component, since I would import ui components from my own codebase, it will use my very own ui components that are already custom.

Not everyone likes it, but it's a cool approach.

That said, making a react-querybuilder-shadcn-ui library would imply making copy-pastable code instead of a npm module, which is what I'm planning to do and share.

Another approach would be using dependency injection to inject custom components, but it would be overwhelming.

sowka1995 commented 1 month ago

I'm trying to implement this, still very rough but I'll share progress and code.

Implementing a real theme might not work since shadcn/ui is based on copying/pasting components, so there's no way to import those as you would in a classic UI library.

Capture d’écran 2024-03-22 à 01 18 14

It looks really awesome even at this point! :heart_eyes:

jakeboone02 commented 1 month ago

you own the ui library, so customizing it is as easy as editing files directly.

For what it's worth, I'm not knocking this approach. I guess I don't understand the difference between copy/pasting code and forking a library. You lose the upgrade path either way, and while I can see the benefits of "owning" your components, you're also taking on responsibility for compatibility, testing, etc.

if I wanted to customize @react-querybuilder/chakra, I would have to fork it or override classes

This is not correct. You can copy/paste the components from the @react-querybuilder/chakra source and use them independently of the package itself. You can also implement the package as recommended but override certain components with modified versions using the controlElements prop. (I'm not exactly sure what you mean by "override classes," but if I understand correctly that would not be necessary.)

making a react-querybuilder-shadcn-ui library would imply making copy-pastable code instead of a npm module

I would be happy to add something like this to the examples folder, foregoing an npm package. Feel free to submit a PR to that effect!

Another approach would be using dependency injection to inject custom components, but it would be overwhelming.

Again, controlElements allows you to replace any component with your own implementation--no fancy DI or anything like that. We even have documentation on falling back to the default components if your custom implementation only has to cover certain cases. And if you only need to modify styles, controlClassnames can add custom CSS classes to each component (or you can apply styles to the standard classes).

jide commented 1 month ago

For what it's worth, I'm not knocking this approach. I guess I don't understand the difference between copy/pasting code and forking a library. You lose the upgrade path either way, and while I can see the benefits of "owning" your components, you're also taking on responsibility for compatibility, testing, etc.

Honestly this is a whole debate by itself, and I can totally understand it's not obvious, but after years using Chakra and fighting it to customize components (in particular the theming API is complex with components made of multiple parts) VS editing shadcn/ui components by hand in my components folder I'm sold, but I was very skeptical at first.

And forking Chakra is a lot more effort than copy pasting some JSX.

This is not correct. You can copy/paste the components from the @react-querybuilder/chakra source and use them independently of the package itself. You can also implement the package as recommended but override certain components with modified versions using the controlElements prop. (I'm not exactly sure what you mean by "override classes," but if I understand correctly that would not be necessary.)

Yes of course, but what I meant is that in the end if you want to customize freely and use your very own components you end up copy pasting basically :) Which is kind of the point of the copy-pasting approach of shadcn/ui.

I would be happy to add something like this to the examples folder, foregoing an npm package. Feel free to submit a PR to that effect!

I will !

Again, controlElements allows you to replace any component with your own implementation--no fancy DI or anything like that. We even have documentation on falling back to the default components if your custom implementation only has to cover certain cases. And if you only need to modify styles, controlClassnames can add custom CSS classes to each component (or you can apply styles to the standard classes).

What I meant is that to publish a module that would allow using your own shadcn/ui components, since the paths are local to your project, it's impossible to reference them from the module code.

jakeboone02 commented 1 month ago

after years using Chakra and fighting it to customize components

I feel your pain with Chakra. I nearly gave up on building that compatibility package several times. Chakra and Ant Design have been the most painful so far, although MUI also took a long time to get right because of the way they handle the context provider.

What I meant is that to publish a module that would allow using your own shadcn/ui components, since the paths are local to your project, it's impossible to reference them from the module code.

Ok I think I understand now. You're saying that it doesn't make sense to publish a compatibility package for shadcn à la @react-querybuilder/chakra or @react-querybuilder/antd because it wouldn't be able to reference anything specific to shadcn, all of the shadcn-based code being local to the consuming project. At first I thought you were saying you'd have to fork RQB in order to use (copy/pasted) shadcn components.

For what it's worth—and for future reference, I guess—here's an example using the shadcn Button component for all the buttons in RQB. This is based on the Button example in the shadcn docs and the actual ActionElement code from RQB:

import { Button } from "@/components/ui/button";

const MyActionElement = (props: ActionPropsWithRulesAndAdders) => (
  <Button
    type="button"
    className={props.className}
    title={props.disabledTranslation && disabled ? props.disabledTranslation.title : props.title}
    disabled={props.disabled && !props.disabledTranslation}
    onClick={e => props.handleOnClick(e)}
  >
    {props.disabledTranslation && props.disabled ? props.disabledTranslation.label : props.label}
  </Button>
);

export const App = () => {
  return <QueryBuilder controlElements={{ actionElement: MyActionElement }} />
}

(controlElements#actionElement is new in RQB v7.)

jide commented 1 month ago

Ok I think I understand now. You're saying that it doesn't make sense to publish a compatibility package for shadcn à la @react-querybuilder/chakra or @react-querybuilder/antd because it wouldn't be able to reference anything specific to shadcn, all of the shadcn-based code being local to the consuming project. At first I thought you were saying you'd have to fork RQB in order to use (copy/pasted) shadcn components.

Absolutely !

For what it's worth—and for future reference, I guess—here's an example using the shadcn Button component for all the buttons in RQB. This is based on the Button example in the shadcn docs and the actual ActionElement code from RQB:

That's pretty much what I did, although I used different variations for addRule, addGroup etc.

sowka1995 commented 1 month ago

Do you already have a working example you could share? I need it and I don't know whether to start implementing Shadcn integration from scratch.

jakeboone02 commented 1 month ago

Do you already have a working example you could share? I need it and I don't know whether to start implementing Shadcn integration from scratch.

Unless @jide has created something, my example in https://github.com/react-querybuilder/react-querybuilder/issues/672#issuecomment-2019118003 is all we have for now.

The problem with Shadcn is there is you can't really do "integration." Since components are local to each project and not common to all Shadcn implementations, you'd have to reimplement RQB components using your own Shadcn components.

You shouldn't have to reimplement QueryBuilder, RuleGroup, or Rule, but you would need to reimplement ActionElement, ValueSelector, and ValueEditor (and, depending on your configuration, DragHandle, NotToggle, and ShiftActions). The most complex of those is ValueEditor; the others are relatively simple.

jide commented 3 weeks ago

Hey,

I took the time to extract the ui into a clean project. Here it is : https://github.com/jide/react-querybuilder-shadcn-ui/tree/main/src/components/react-querybuilder-shadcn-ui

It's still very rough, but I thought it may be useful at this stage.

jide commented 3 weeks ago

Added more UI. Starts to look decent IMO !

Capture d’écran 2024-04-24 à 01 18 58
sowka1995 commented 3 weeks ago

Added more UI. Starts to look decent IMO !

Capture d’écran 2024-04-24 à 01 18 58

It looks Impressive! I will try to use it in my project when I return from my vacation.

jide commented 3 weeks ago

Forgot to mention I also added dark mode support. Still some work to do on drag n drop and shift features, but its functional.

Capture d’écran 2024-04-24 à 01 37 20
jide commented 3 weeks ago

In the end, I think my "component injection" idea may not be that bad... It's not fun to copy and paste all the files manually without a CLI tool like shadcn/ui has. Yes, I know 🙃

This way, it will be distributable as a standalone npm module while still allowing using shadcn/ui components from your project.

This is how the implementation looks like :

import { getQueryBuilderShadcnUi } from "@react-querybuilder/shadcn-ui";

// Import needed shadcn/ui components.
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import {
  Select,
  SelectContent,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

// Pass them to getQueryBuilderShadcnUi()
const components = {
  Button,
  Switch,
  Checkbox,
  Input,
  Label,
  RadioGroup,
  RadioGroupItem,
  Textarea,
  Select,
  SelectContent,
  SelectTrigger,
  SelectValue,
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
};

const QueryBuilderShadcnUi = getQueryBuilderShadcnUi(components);

@jakeboone02 What do you think of this approach ?

jakeboone02 commented 3 weeks ago

@jide this is looking really great. Kudos!

After looking at the code in full, I'm going to politely rescind my offer to host the example code in the /examples folder of this repo. The reason is purely to avoid the maintenance burden. Naturally I would feel obligated to keep the example working and up to date with the latest react-querybuilder version, but since so much of its code would be copy-pasted straight from the original source, it would effectively double the effort required for any component code changes.

However, I would be more than happy to link to this example from somewhere in the repo or the website. It's coming together as a great showcase of the modularity of RQB. My only condition would be that the example repo makes clear what RQB version the component code was copied from, in addition to a pinned react-querybuilder patch version in package.json (no leading caret/tilde/etc).


...I was just about to submit that when your last comment came through. The method you've proposed is similar to what we do with MUI components in @react-querybuilder/material. The documentation here might give you some more ideas. It would be good to explore that further.

jide commented 3 weeks ago

Damn, how did not I see this from the mui package.

Indeed that's the same approach. I'm willing to submit a PR, if you feel comfortable with the maintenance overhead. Otherwise I'll make a separate contrib npm package which is fine too.

Let me know.

jakeboone02 commented 3 weeks ago

Do a PR and we'll see where it goes.