Skip to content

Instantly share code, notes, and snippets.

@FezVrasta
Last active November 7, 2024 16:18
Show Gist options
  • Save FezVrasta/1eedff6cf752088d80d74fa0745dc8ce to your computer and use it in GitHub Desktop.
Save FezVrasta/1eedff6cf752088d80d74fa0745dc8ce to your computer and use it in GitHub Desktop.
TailwindCSS utility to convert objects into strings
type Value = string | number | boolean;
interface Config extends Record<string, Value | Config> {}
/**
* Converts a Tailwind CSS config object to a list of class names.
*
* The config object properties and the values will be joined with
* a hyphen to form the class name. If the value is `false`, the class
* name will be omitted. If the value is `true`, the class name will
* be included as is.
*
* The `&` key can be used to join the parent class name with the
* child class name. This works similar to the `&` in SCSS.
*
* Examples:
*
* ```ts
* twj({ grid: { cols: 12, gap: 4 } }); // "grid-cols-12 grid-gap-4"
* twj({ grid: { "&": true, gap: 4 } }); // "grid grid-gap-4"
* twj({ group: { hover: { bg: "blue", text: "white" } } }); // "group-hover:bg-blue group-hover:text-white"
* ```
*
* @param config The Tailwind CSS config object.
* @returns A list of class names.
*/
function configToClassNamesList(config: Config): Array<Array<Value>> {
const result: Array<Array<Value>> = [];
function traverse(obj: Config, path: string[]) {
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
// If the value is an object, recurse into it
traverse(obj[key], [...path, key]);
} else {
// If the value is not an object, push the path and value to the result
result.push([...path, key, obj[key]]);
}
}
}
traverse(config, []);
return result;
}
export function twj(config: Config): string {
const classNamesList = configToClassNamesList(config);
return classNamesList
.map((parts) => {
parts = parts
.filter((part) => part !== '&')
.reduce((parts, part) => {
if (typeof part === 'string' && part.startsWith('&')) {
return [
...parts.slice(0, -1),
`${parts.at(-1) ?? ''}${part.slice(1)}`,
];
}
return [...parts, part];
}, [] as Value[]);
if (parts.at(-1) === false) {
return undefined;
}
if (parts.at(-1) === true) {
parts.pop();
}
return parts.join('-');
})
.filter((className) => className !== undefined)
.join(' ');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment