QwikDev / qwik

Instant-loading web apps, without effort
https://qwik.dev
MIT License
20.71k stars 1.29k forks source link

[📖] route(s): how to make headless components #6099

Closed Nefcanto closed 1 month ago

Nefcanto commented 5 months ago

Suggestion

I want to create a login form that developers can style for each project. Based on my experience in React, I tried to separate behavior from UI and pass the behavior, which is a common practice. For example in react-dropzone, you basically pass the props as <input {...getInputProps()} />.

So, I tried to create the most basic headless component for a phone number field. I expect my developers to write this code in our projects:

const Login = ({ phoneProps, otpProps, submitProps }) => {
     return <form>
        <input {...phoneProps} class="bg-red-400" />
        <hr />
        <input {...otpProps} class="bg-blue-400" />
        <hr />
        <button {...submitProps} class="border">Send</button>
    </form>
}

export default Login

So, I tried to create a signal in the base and create a $ function, and I passed them:

    const phoneNumber = useSignal("")
    const phoneProps = {
        value: phoneNumber.value,
        onChange$: $(e => console.log(e.target.value) && phoneNumber.value = e.target.value)
    }
    <Login phoneProps={phoneProps} />

But nothing gets printed to the console.

So, can you please create a page in the advanced menu to show how should we create headless components in Qwik?

fprl commented 5 months ago

Hi @Nefcanto , as stated in the docs, you need to use onInput$.

Here's a demo working: https://stackblitz.com/edit/qwik-starter-aqqd68?file=src%2Froutes%2Findex.tsx,src%2Fcomponents%2Flogin-form.tsx

I suggest to you — if you are not there — to join the Qwik discord channel.

Regards!

Nefcanto commented 5 months ago

@fprl, to be honest, I needed onKeyDown$. But that's not the point here. I want to know how the community creates headless components. How do they separate behavior from structure in Qwik?

fprl commented 5 months ago

@Nefcanto then this is not an issue for GH, right? I recommend you to visit the Discord and ask the community or check the multiple questions that have been done in the past. There is also qwik-ui and you can learn how to handle headless component from their codebase (and discord).

Nefcanto commented 5 months ago

@fprl, with respect, I disagree. It could be a page in the advanced menu of the docs. Many teams come across this requirement and it's better to have an official way for doing so.

fprl commented 5 months ago

@Nefcanto no problem at all. I understand but sometimes it's a bit subjective topic how to build things. Maybe someone from the core team thinks this is a good idea!

gioboa commented 3 months ago

You can have a look at QwikUI repository There are a lot of headless components to check. This is a special case but we can create a cookbook to show how do this kind of things. @thejackshelton WDYT?

thejackshelton commented 3 months ago

https://qwikui.com/contributing/

Some of this is documented here. Although part of it is a bit out of date and could be updated, I will add it as a task in our upcoming sprint. Qwik UI is a good resource to look at.

We use PropsOf to type it to a particular piece of markup, and then consumers have the ability to customize further by passing signals to the root component piece with the bind:x syntax.

@Nefcanto like @fprl mentioned it largely depends on the approach.

What you're doing with the props resembles a more hooks based approach (where it's assumed the user owns the markup), if you're doing this in a component then it is not composable.

Composability provides granular access to each component parts, so you can wrap them and add your own event listeners, props, etc.

I'm not sure yet if this is the responsibility of the Qwik docs to explain. We have learned some clever patterns though (for example inline components) that need to be documented which I will document in core here soon 👍 .

gioboa commented 3 months ago

@Nefcanto I'd love to hear your thoughts on this. Did you implement your headless solution?

Nefcanto commented 3 months ago

@thejackshelton, thank you for the explanation. The the user owns the markup was a key point in what you said.

@gioboa and @thejackshelton, for us we think of the domain. Domain is the basis here. Let's talk about a mini-cart component.

These are the domain requirements:

  1. Branding and theming should be possible
  2. Leaves should be simple (instances of MiniCart component should be as simple as possible)
  3. Data is almost the same for all of the mini-cart components (an array of order items, each item containing quantity and price, discounts might apply, and a total should be shown)
  4. Behavior should be 100% centralized (removing an item, increasing/decreasing quantity, listening to "addedToCart" event to update itself, etc.)

Based on these requirements, we come up with a hook:

import { useCart } from "Sales"

Then in an instance of a mini-cart, we will write:

const MiniCart = () => {
    const {
        increaseBehavior,
        decreaseBehavior,
        removeBehavior,
        items,
        total,
        emptyCartBehavior,
    } = useCart()

    return <>
        {
            items.map(item => <div>
                 <h1>{item.title}</h1>
                 <button {...increaseBehavior>More</button>
                 <button {...decreaseBehavior}>Less</button>
            </div>)
        }
    </>
}

As @thejackshelton said this is a hook-based approach that assumes the user owns the markup. But this means that creating good markup is a responsibility for the leaf developer. For example buttons and aria-* attributes.

Thus we use atomic UI to give them basic components with these built-in features:

import { Button } from "Base"

<Button {...increaseBehavior} class="bg-red-400">More</Button>

Or

import { OrderItem } from "Base"
<OrderItem
    item={item}
    class="bg-slate-400 mx-10 my-6"
    itemClass="font-bold"
    buttonClass="bg-red-400"   
/>

This means that our leaf developers (who create instances of mini-carts on different websites) can:

  1. Control the nesting
  2. Control the tag selection (h1, h2, div, etc.)
  3. Style 100% anyhow they want
  4. Add animations as they want
  5. Control the graphic 100% (adding SVGs or images, etc. anywhere they want)
  6. Write not even one line of behavioral code (JS code)
  7. Can reuse base markups in atomic and molecular granular levels to compose

That's what we do now.

maiieul commented 2 months ago

Thanks @Nefcanto for the suggestion. I don't think any other framework has that kind of page in their docs, because it is either very specific to your application, or complex if you're a UI library because you need to support a lot of cases. Also it depends on the developer's preferences. I'm not sure we could condense all of that into one route, but maybe it's worth a try some time later once v2 is stable and the more urgent issues for the docs have been tackled.