npm package: @micheldever/qwik-compose
Description: A styled-components-like API for creating stylish Qwik primitives
Latest version: 1.2.0

<div align="center">
  <h1>
    <br />
    <div>@micheldever/qwik-compose</div>
    <br />
  </h1>
  <div>A styled-components-like API for creating stylish Qwik primitives</div>
  <br /><br />
  <pre>git clone https://mtsweb@dev.azure.com/mtsweb/oss/_git/qwik-compose</pre>
</div>

## Installation

This package is distributed via npm. You can install it as a dependency in your project by running:

```bash
yarn add @micheldever/qwik-compose
```

## Usage

To create a composed component, access one of the element properties from the `compose` function and pass an array of class names to apply:

```jsx
import { compose } from '@micheldever/qwik-compose';

// Create a <Wrapper> Qwik component that implements the following
// Tailwind CSS classes and renders as a <section> html element
const Wrapper = compose.section([
  'bg-orange-100',
  'p-16',
]);

// Create a <Title> Qwik component that implements the following
// Tailwind CSS classes and renders as a <h1> html element
const Title = compose.h1([
  'text-center',
  'text-2xl',
  'text-rose-400',
  'font-serif',
  'font-bold',
  'my-4',
]);

// Use them like regular Qwik components – except they're styled!
function Application() {
  return (
    <Wrapper>
      <Title>Hello World, this is my first composed component!</Title>
    </Wrapper>
  );
}
```

### Transient Props

Since `@micheldever/qwik-compose` does not execute any runtime code, applying conditional styling can be tricky. As such, it is recommended that you lean on `data` attributes to use as hooks for your conditional styles.

To make this pattern quick and easy to manage, you can prefix your conditional prop names with a dollar sign (`$`) to turn them into transient props. This will then convert your prop into a data attribute pre-runtime. For example, a prop named `$isLarge` is not rendered to the final DOM element, but instead is transformed to be `data-is-large` which can then be used as a styling hook.

This functionality is entirely optional and you can still directly pass `data` attributes if that makes more sense to you.

```jsx
const Button = compose.button([
  'bg-red-500',
  'text-red-50',
  'data-[is-large="true"]:text-xl',
]);

// outputs <button class="bg-red-500 text-red-50 data-[is-large="true"]:text-xl" data-is-large="true">
<Button $isLarge={true}>
```

### Extending Components

As you start to build out your component library, you may wish to use a component but slightly change or build upon its styling. While you could use the transient props pattern above, depending on how many alterations you make, this could quickly become unmaintainable.

To simplify the process, you can extend an existing composed component and supply it with a list of additional classes you wish to attach, resulting in a new component that is the best of both worlds.

```jsx
// outputs <button class="bg-red-500 text-red-50">
const Button = compose.button([
  'bg-red-500',
  'text-red-50',
]);

// outputs <button class="bg-red-500 text-red-50 border border-red-700">
const BorderedButton = compose(Button, [
  'border',
  'border-red-700'
]);
```

This also works for custom components, as long as they pass the `class` prop to a DOM element.

```jsx
function CustomButton({ children, class }) {
  return <button class={class}>{children}</button>;
}

const ComposedCustomButton = composed(CustomButton, [
  'bg-red-500',
  'text-red-50',
]);

// outputs <button class="bg-red-500 text-red-50">
<ComposedCustomButton />
```

You can also pass extra classes to individual component instances via the `class` prop.

```jsx
// outputs <button class="bg-red-500 text-red-50 border border-red-700">
<Button class='border border-red-700' />
```

### Using `as`

All composed components are polymorphic, meaning you are able to alter the way they render after they have been created by using the `as` prop. This keeps all the styling that has been applied to a component but just switches out what is ultimately being rendered (be it a different HTML element or a different custom component).

```jsx
const Button = compose.button([ ... ]);

// This component will render as a `div` element instead of a `button`
<Button as='div' />
```

### Using `attrs`

Occasionally, you may know ahead of time if your component will always use the same static prop values, such as an input element having a set type property. By using the `attrs` method, you can implicitly set any static prop values that should be passed down to every instance of your component.

Furthermore, you can also use the attrs method to attach default values for your dynamic transient props.

```jsx
const TextField = compose.input([ ... ]).attrs({ $hasIcon: false, type: 'text' });

// This will render with the `type` attribute implicitly set
// from the original declaration
<TextField />

// You can also locally override any attributes that are defined above
<TextField $hasIcon={true} type='email' />
```

```jsx
// For extended components, you can define attributes in the same way
const EmailField = styled(TextField, [ ... ]).attrs({ type: 'email' });
```

### Using `toClass`

If you just need the output class string and not the created Qwik component, you can call the `toClass` method on any composed component to receive the concatenated class.

```jsx
// outputs <button class="bg-red-500 text-red-50">
const Button = compose.button([
  'bg-red-500',
  'text-red-50',
]);

// outputs bg-red-500 text-red-50
const className = Button.toClass();
```

### Execution Hooks

`@micheldever/qwik-compose` allows you to perform additional custom logic to the concatenated class string by defining a global event handler for the `onDone` event. This can be particularly useful if you want to combine `@micheldever/qwik-compose` with a utility function such as [`tailwind-merge`](https://github.com/dcastil/tailwind-merge).

#### Example with `tailwind-merge`

```jsx
// qwik-compose.config.ts

import { defineConfig } from '@micheldever/qwik-compose';
import { twMerge } from 'tailwind-merge';
import type { DefineConfigOptions } from '@micheldever/qwik-compose';


const config: DefineConfigOptions = {
  hooks: {
    onDone: (className) => twMerge(className),
  },
};

export const { compose } = defineConfig(config);
```

```jsx
// components/button.ts

import { compose } from '../qwik-compose.config';

const Button = compose.button([
  'bg-orange-100',
  'bg-red-500',
]);

// outputs <button class="bg-red-500">
<Button />
```