@micheldever/qwik-compose
v1.1.0
Published
A styled-components-like API for creating stylish Qwik primitives
Downloads
68
Readme
Installation
This package is distributed via npm. You can install it as a dependency in your project by running:
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:
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.
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.
// 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.
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.
// 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).
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.
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' />
// 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.
// 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
.
Example with tailwind-merge
// 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);
// 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 />