From 40fcede436a019c57d418c2cc597f1e27a931101 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:32:33 +0200 Subject: [PATCH 1/6] add ability to override in extendTailwindMerge --- src/lib/extend-tailwind-merge.ts | 4 +- src/lib/merge-configs.ts | 79 +++++++++++++++++++------------- src/lib/types.ts | 11 +++++ 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/src/lib/extend-tailwind-merge.ts b/src/lib/extend-tailwind-merge.ts index 6f82fd12..7e577505 100644 --- a/src/lib/extend-tailwind-merge.ts +++ b/src/lib/extend-tailwind-merge.ts @@ -1,12 +1,12 @@ import { createTailwindMerge } from './create-tailwind-merge' import { getDefaultConfig } from './default-config' import { mergeConfigs } from './merge-configs' -import { Config } from './types' +import { Config, ConfigExtension } from './types' type CreateConfigSubsequent = (config: Config) => Config export function extendTailwindMerge( - configExtension: Partial | CreateConfigSubsequent, + configExtension: ConfigExtension | CreateConfigSubsequent, ...createConfig: CreateConfigSubsequent[] ) { return typeof configExtension === 'function' diff --git a/src/lib/merge-configs.ts b/src/lib/merge-configs.ts index c512e69f..eb7a2573 100644 --- a/src/lib/merge-configs.ts +++ b/src/lib/merge-configs.ts @@ -1,51 +1,66 @@ -import { Config } from './types' +import { Config, ConfigExtension } from './types' /** * @param baseConfig Config where other config will be merged into. This object will be mutated. * @param configExtension Partial config to merge into the `baseConfig`. */ -export function mergeConfigs(baseConfig: Config, configExtension: Partial) { - for (const key in configExtension) { - mergePropertyRecursively(baseConfig as any, key, configExtension[key as keyof Config]) +export function mergeConfigs( + baseConfig: Config, + { cacheSize, prefix, separator, extend = {}, ...configOverride }: ConfigExtension, +) { + overrideProperty(baseConfig, 'cacheSize', cacheSize) + overrideProperty(baseConfig, 'prefix', prefix) + overrideProperty(baseConfig, 'separator', separator) + + for (const configKey in configOverride) { + overrideConfigProperties( + baseConfig[configKey as keyof typeof configOverride], + configOverride[configKey as keyof typeof configOverride], + ) + } + + for (const key in extend) { + mergeConfigProperties( + baseConfig[key as keyof typeof extend], + extend[key as keyof typeof extend], + ) } return baseConfig } -const hasOwnProperty = Object.prototype.hasOwnProperty -const overrideTypes = new Set(['string', 'number', 'boolean']) - -function mergePropertyRecursively( - baseObject: Record, - mergeKey: string, - mergeValue: unknown, +function overrideProperty( + baseObject: T, + overrideKey: K, + overrideValue: T[K] | undefined, ) { - if ( - !hasOwnProperty.call(baseObject, mergeKey) || - overrideTypes.has(typeof mergeValue) || - mergeValue === null - ) { - baseObject[mergeKey] = mergeValue - return + if (overrideValue !== undefined) { + baseObject[overrideKey] = overrideValue } +} - if (Array.isArray(mergeValue) && Array.isArray(baseObject[mergeKey])) { - baseObject[mergeKey] = (baseObject[mergeKey] as unknown[]).concat(mergeValue) - return +function overrideConfigProperties( + baseObject: Record, + overrideObject: Record | undefined, +) { + if (overrideObject) { + for (const key in overrideObject) { + overrideProperty(baseObject, key, overrideObject[key]) + } } +} - if (typeof mergeValue === 'object' && typeof baseObject[mergeKey] === 'object') { - if (baseObject[mergeKey] === null) { - baseObject[mergeKey] = mergeValue - return - } +function mergeConfigProperties( + baseObject: Record, + mergeObject: Record | undefined, +) { + if (mergeObject) { + for (const key in mergeObject) { + const mergeValue = mergeObject[key] - for (const nextKey in mergeValue) { - mergePropertyRecursively( - baseObject[mergeKey] as Record, - nextKey, - mergeValue[nextKey as keyof object], - ) + if (mergeValue !== undefined) { + baseObject[key] = (baseObject[key] || []).concat(mergeValue) + } } } } diff --git a/src/lib/types.ts b/src/lib/types.ts index 13083dd1..d3525733 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -46,6 +46,17 @@ export interface Config { conflictingClassGroupModifiers: Record } +export type ConfigExtend = Partial< + Pick< + Config, + 'theme' | 'classGroups' | 'conflictingClassGroups' | 'conflictingClassGroupModifiers' + > +> + +export interface ConfigExtension extends Partial { + extend?: ConfigExtend +} + export type ThemeObject = Record export type ClassGroupId = string export type ClassGroup = readonly ClassDefinition[] From 5857aac6eda23b29e61c579429fd0c1af1eee281 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:32:57 +0200 Subject: [PATCH 2/6] adjust tests --- tests/extend-tailwind-merge.test.ts | 31 +++++++++++++++++++ tests/merge-configs.test.ts | 47 ++++++++++++++++++++++++----- tests/theme.test.ts | 20 +++++++----- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/tests/extend-tailwind-merge.test.ts b/tests/extend-tailwind-merge.test.ts index 9dd1b12e..045811ee 100644 --- a/tests/extend-tailwind-merge.test.ts +++ b/tests/extend-tailwind-merge.test.ts @@ -101,3 +101,34 @@ test('extendTailwindMerge works correctly with function config', () => { expect(tailwindMerge('p-10 p-20')).toBe('p-20') expect(tailwindMerge('hover:focus:p-10 focus:hover:p-20')).toBe('focus:hover:p-20') }) + +test('extendTailwindMerge overrides and extends correctly', () => { + const tailwindMerge = extendTailwindMerge({ + cacheSize: 20, + classGroups: { + shadow: ['shadow-100', 'shadow-200'], + customKey: ['custom-100'], + }, + conflictingClassGroups: { + p: ['px'], + }, + extend: { + classGroups: { + shadow: ['shadow-300'], + customKey: ['custom-200'], + 'font-size': ['text-foo'], + }, + conflictingClassGroups: { + m: ['h'], + }, + }, + }) + + expect(tailwindMerge('shadow-lg shadow-100 shadow-200')).toBe('shadow-lg shadow-200') + expect(tailwindMerge('custom-100 custom-200')).toBe('custom-200') + expect(tailwindMerge('text-lg text-foo')).toBe('text-foo') + expect(tailwindMerge('px-3 py-3 p-3')).toBe('py-3 p-3') + expect(tailwindMerge('p-3 px-3 py-3')).toBe('p-3 px-3 py-3') + expect(tailwindMerge('mx-2 my-2 h-2 m-2')).toBe('m-2') + expect(tailwindMerge('m-2 mx-2 my-2 h-2')).toBe('m-2 mx-2 my-2 h-2') +}) diff --git a/tests/merge-configs.test.ts b/tests/merge-configs.test.ts index 5430db07..b731250c 100644 --- a/tests/merge-configs.test.ts +++ b/tests/merge-configs.test.ts @@ -5,52 +5,83 @@ test('mergeConfigs has correct behavior', () => { mergeConfigs( { cacheSize: 50, + prefix: 'tw-', separator: ':', theme: { hi: ['ho'], + themeToOverride: ['to-override'], }, classGroups: { fooKey: [{ fooKey: ['one', 'two'] }], bla: [{ bli: ['blub', 'blublub'] }], + groupToOverride: ['this', 'will', 'be', 'overridden'], + groupToOverride2: ['this', 'will', 'not', 'be', 'overridden'], + }, + conflictingClassGroups: { + toOverride: ['groupToOverride'], }, - conflictingClassGroups: {}, conflictingClassGroupModifiers: { hello: ['world'], + toOverride: ['groupToOverride-2'], }, }, { + separator: '-', + prefix: undefined, + theme: { + themeToOverride: ['overridden'], + }, classGroups: { - fooKey: [{ fooKey: ['bar', 'baz'] }], - fooKey2: [{ fooKey: ['qux', 'quux'] }], - otherKey: ['nother', 'group'], + groupToOverride: ['I', 'overrode', 'you'], + groupToOverride2: undefined!, }, conflictingClassGroups: { - fooKey: ['otherKey'], - otherKey: ['fooKey', 'fooKey2'], + toOverride: ['groupOverridden'], }, conflictingClassGroupModifiers: { - hello: ['world2'], + toOverride: ['overridden-2'], + }, + extend: { + classGroups: { + fooKey: [{ fooKey: ['bar', 'baz'] }], + fooKey2: [{ fooKey: ['qux', 'quux'] }], + otherKey: ['nother', 'group'], + groupToOverride: ['extended'], + }, + conflictingClassGroups: { + fooKey: ['otherKey'], + otherKey: ['fooKey', 'fooKey2'], + }, + conflictingClassGroupModifiers: { + hello: ['world2'], + }, }, }, ), ).toEqual({ cacheSize: 50, - separator: ':', + prefix: 'tw-', + separator: '-', theme: { hi: ['ho'], + themeToOverride: ['overridden'], }, classGroups: { fooKey: [{ fooKey: ['one', 'two'] }, { fooKey: ['bar', 'baz'] }], bla: [{ bli: ['blub', 'blublub'] }], fooKey2: [{ fooKey: ['qux', 'quux'] }], otherKey: ['nother', 'group'], + groupToOverride: ['I', 'overrode', 'you', 'extended'], + groupToOverride2: ['this', 'will', 'not', 'be', 'overridden'], }, conflictingClassGroups: { + toOverride: ['groupOverridden'], fooKey: ['otherKey'], otherKey: ['fooKey', 'fooKey2'], }, conflictingClassGroupModifiers: { hello: ['world', 'world2'], + toOverride: ['overridden-2'], }, }) }) diff --git a/tests/theme.test.ts b/tests/theme.test.ts index afe49d8f..4d1c8b71 100644 --- a/tests/theme.test.ts +++ b/tests/theme.test.ts @@ -2,9 +2,11 @@ import { extendTailwindMerge, fromTheme } from '../src' test('theme scale can be extended', () => { const tailwindMerge = extendTailwindMerge({ - theme: { - spacing: ['my-space'], - margin: ['my-margin'], + extend: { + theme: { + spacing: ['my-space'], + margin: ['my-margin'], + }, }, }) @@ -14,11 +16,13 @@ test('theme scale can be extended', () => { test('theme object can be extended', () => { const tailwindMerge = extendTailwindMerge({ - theme: { - 'my-theme': ['hallo', 'hello'], - }, - classGroups: { - px: [{ px: [fromTheme('my-theme')] }], + extend: { + theme: { + 'my-theme': ['hallo', 'hello'], + }, + classGroups: { + px: [{ px: [fromTheme('my-theme')] }], + }, }, }) From a22219dd434d46547251d5a7b62dc624bdb5d510 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:46:10 +0200 Subject: [PATCH 3/6] add override key to make overriding more explicit --- src/lib/merge-configs.ts | 8 ++++---- src/lib/types.ts | 23 +++++++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/lib/merge-configs.ts b/src/lib/merge-configs.ts index eb7a2573..c3d41dd2 100644 --- a/src/lib/merge-configs.ts +++ b/src/lib/merge-configs.ts @@ -6,16 +6,16 @@ import { Config, ConfigExtension } from './types' */ export function mergeConfigs( baseConfig: Config, - { cacheSize, prefix, separator, extend = {}, ...configOverride }: ConfigExtension, + { cacheSize, prefix, separator, extend = {}, override = {} }: ConfigExtension, ) { overrideProperty(baseConfig, 'cacheSize', cacheSize) overrideProperty(baseConfig, 'prefix', prefix) overrideProperty(baseConfig, 'separator', separator) - for (const configKey in configOverride) { + for (const configKey in override) { overrideConfigProperties( - baseConfig[configKey as keyof typeof configOverride], - configOverride[configKey as keyof typeof configOverride], + baseConfig[configKey as keyof typeof override], + override[configKey as keyof typeof override], ) } diff --git a/src/lib/types.ts b/src/lib/types.ts index d3525733..3bee64a0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,6 @@ -export interface Config { +export interface Config extends ConfigStatic, ConfigGroups {} + +interface ConfigStatic { /** * Integer indicating size of LRU cache used for memoizing results. * - Cache might be up to twice as big as `cacheSize` @@ -19,6 +21,13 @@ export interface Config { * Theme scales used in classGroups. * The keys are the same as in the Tailwind config but the values are sometimes defined more broadly. */ +} + +interface ConfigGroups { + /** + * Theme scales used in classGroups. + * The keys are the same as in the Tailwind config but the values are sometimes defined more broadly. + */ theme: ThemeObject /** * Object with groups of classes. @@ -46,15 +55,9 @@ export interface Config { conflictingClassGroupModifiers: Record } -export type ConfigExtend = Partial< - Pick< - Config, - 'theme' | 'classGroups' | 'conflictingClassGroups' | 'conflictingClassGroupModifiers' - > -> - -export interface ConfigExtension extends Partial { - extend?: ConfigExtend +export interface ConfigExtension extends Partial { + override?: Partial + extend?: Partial } export type ThemeObject = Record From 8c86919884c5591bcdd24ee25cd859e172740f1e Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:46:18 +0200 Subject: [PATCH 4/6] adjust tests --- tests/extend-tailwind-merge.test.ts | 50 ++++++++++++++++------------- tests/merge-configs.test.ts | 26 ++++++++------- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/tests/extend-tailwind-merge.test.ts b/tests/extend-tailwind-merge.test.ts index 045811ee..25407900 100644 --- a/tests/extend-tailwind-merge.test.ts +++ b/tests/extend-tailwind-merge.test.ts @@ -3,14 +3,16 @@ import { extendTailwindMerge } from '../src' test('extendTailwindMerge works correctly with single config', () => { const tailwindMerge = extendTailwindMerge({ cacheSize: 20, - classGroups: { - fooKey: [{ fooKey: ['bar', 'baz'] }], - fooKey2: [{ fooKey: ['qux', 'quux'] }, 'other-2'], - otherKey: ['nother', 'group'], - }, - conflictingClassGroups: { - fooKey: ['otherKey'], - otherKey: ['fooKey', 'fooKey2'], + extend: { + classGroups: { + fooKey: [{ fooKey: ['bar', 'baz'] }], + fooKey2: [{ fooKey: ['qux', 'quux'] }, 'other-2'], + otherKey: ['nother', 'group'], + }, + conflictingClassGroups: { + fooKey: ['otherKey'], + otherKey: ['fooKey', 'fooKey2'], + }, }, }) @@ -34,14 +36,16 @@ test('extendTailwindMerge works corectly with multiple configs', () => { const tailwindMerge = extendTailwindMerge( { cacheSize: 20, - classGroups: { - fooKey: [{ fooKey: ['bar', 'baz'] }], - fooKey2: [{ fooKey: ['qux', 'quux'] }, 'other-2'], - otherKey: ['nother', 'group'], - }, - conflictingClassGroups: { - fooKey: ['otherKey'], - otherKey: ['fooKey', 'fooKey2'], + extend: { + classGroups: { + fooKey: [{ fooKey: ['bar', 'baz'] }], + fooKey2: [{ fooKey: ['qux', 'quux'] }, 'other-2'], + otherKey: ['nother', 'group'], + }, + conflictingClassGroups: { + fooKey: ['otherKey'], + otherKey: ['fooKey', 'fooKey2'], + }, }, }, (config) => ({ @@ -105,12 +109,14 @@ test('extendTailwindMerge works correctly with function config', () => { test('extendTailwindMerge overrides and extends correctly', () => { const tailwindMerge = extendTailwindMerge({ cacheSize: 20, - classGroups: { - shadow: ['shadow-100', 'shadow-200'], - customKey: ['custom-100'], - }, - conflictingClassGroups: { - p: ['px'], + override: { + classGroups: { + shadow: ['shadow-100', 'shadow-200'], + customKey: ['custom-100'], + }, + conflictingClassGroups: { + p: ['px'], + }, }, extend: { classGroups: { diff --git a/tests/merge-configs.test.ts b/tests/merge-configs.test.ts index b731250c..5e98ceb0 100644 --- a/tests/merge-configs.test.ts +++ b/tests/merge-configs.test.ts @@ -28,18 +28,20 @@ test('mergeConfigs has correct behavior', () => { { separator: '-', prefix: undefined, - theme: { - themeToOverride: ['overridden'], - }, - classGroups: { - groupToOverride: ['I', 'overrode', 'you'], - groupToOverride2: undefined!, - }, - conflictingClassGroups: { - toOverride: ['groupOverridden'], - }, - conflictingClassGroupModifiers: { - toOverride: ['overridden-2'], + override: { + theme: { + themeToOverride: ['overridden'], + }, + classGroups: { + groupToOverride: ['I', 'overrode', 'you'], + groupToOverride2: undefined!, + }, + conflictingClassGroups: { + toOverride: ['groupOverridden'], + }, + conflictingClassGroupModifiers: { + toOverride: ['overridden-2'], + }, }, extend: { classGroups: { From 1e6346b59cac7cbc0f458f4a499daf486f6e89a4 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 20 Aug 2023 11:50:02 +0200 Subject: [PATCH 5/6] adjust docs to mergeConfigs usage --- docs/api-reference.md | 16 +++++++++++----- docs/writing-plugins.md | 6 ++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index ff0d41f0..7ec6fc26 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -200,16 +200,22 @@ But don't merge configs like that. Use [`mergeConfigs`](#mergeconfigs) instead. function mergeConfigs(baseConfig: Config, configExtension: Partial): Config ``` -Helper function to merge multiple config objects. Objects are merged, arrays are concatenated, scalar values are overridden and `undefined` does nothing. The function assumes that both parameters are tailwind-merge config objects and shouldn't be used as a generic merge function. +Helper function to merge multiple tailwind-merge configs. Properties with the value `undefined` are skipped. ```ts const customTwMerge = createTailwindMerge(getDefaultConfig, (config) => mergeConfigs(config, { - classGroups: { - // ↓ Adding new class group - mySpecialClassGroup: [{ special: ['1', '2'] }], + override: { + classGroups: { + // ↓ Overriding existing class group + shadow: [{ shadow: ['100', '200', '300', '400', '500'] }], + }, + } + extend: { // ↓ Adding value to existing class group - animate: ['animate-magic'], + animate: ['animate-shimmer'], + // ↓ Adding new class group + prose: [{ prose: ['', validators.isTshirtSize] }], }, }), ) diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index 1d00dae5..cf452a39 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -14,8 +14,10 @@ import { mergeConfigs, validators, Config } from 'tailwind-merge' export function withMagic(config: Config): Config { return mergeConfigs(config, { - classGroups: { - 'magic.my-group': [{ magic: [validators.isLength, 'wow'] }], + extend: { + classGroups: { + 'magic.my-group': [{ magic: [validators.isLength, 'wow'] }], + }, }, }) } From 941ac19bc524006d63083bbcb39f66e903dd8577 Mon Sep 17 00:00:00 2001 From: Dany Castillo <31006608+dcastil@users.noreply.github.com> Date: Sun, 20 Aug 2023 13:47:38 +0200 Subject: [PATCH 6/6] adjust docs to extendTailwindMerge usage --- docs/api-reference.md | 122 ++++++++++++++++++++++++++++-------------- docs/configuration.md | 42 ++++++++------- docs/recipes.md | 32 +++++------ 3 files changed, 121 insertions(+), 75 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 7ec6fc26..dd8e0c42 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -59,13 +59,15 @@ It can be used like this: ```ts extendTailwindMerge({ - theme: { - 'my-scale': ['foo', 'bar'] + extend: { + theme: { + 'my-scale': ['foo', 'bar'], + }, + classGroups: { + 'my-group': [{ 'my-group': [fromTheme('my-scale'), fromTheme('spacing')] }], + 'my-group-x': [{ 'my-group-x': [fromTheme('my-scale')] }], + }, }, - classGroups: { - 'my-group': [{ 'my-group': [fromTheme('my-scale'), fromTheme('spacing')] }] - 'my-group-x': [{ 'my-group-x': [fromTheme('my-scale')] }] - } }) ``` @@ -73,13 +75,13 @@ extendTailwindMerge({ ```ts function extendTailwindMerge( - configExtension: Partial, - ...createConfig: Array<(config: Config) => Config> + configExtension: ConfigExtension, + ...createConfig: ((config: Config) => Config)[] ): TailwindMerge -function extendTailwindMerge(...createConfig: Array<(config: Config) => Config>): TailwindMerge +function extendTailwindMerge(...createConfig: ((config: Config) => Config)[]): TailwindMerge ``` -Function to create merge function with custom config which extends the default config. Use this if you use the default Tailwind config and just extend it in some places. +Function to create merge function with custom config which extends the default config. Use this if you use the default Tailwind config and just modified it in some places. > **Note** > The function `extendTailwindMerge` computes a large data structure based on the config passed to it. I recommend to call it only once and store the result in a top-level variable instead of calling it inline within another repeatedly called function. @@ -88,40 +90,82 @@ You provide it a `configExtension` object which gets [merged](#mergeconfigs) wit ```ts const customTwMerge = extendTailwindMerge({ - cacheSize: 0, // ← Disabling cache + // ↓ Optional cache size + // Here we're disabling the cache + cacheSize: 0, // ↓ Optional prefix from TaiLwind config prefix: 'tw-', // ↓ Optional separator from TaiLwind config separator: '_', - // ↓ Add values to existing theme scale or create a new one - // Not all theme keys form the Tailwind config are supported by default. - theme: { - spacing: ['sm', 'md', 'lg'], - }, - // ↓ Here you define class groups - classGroups: { - // ↓ The `foo` key here is the class group ID - // ↓ Creates group of classes which have conflicting styles - // Classes here: foo, foo-2, bar-baz, bar-baz-1, bar-baz-2 - foo: ['foo', 'foo-2', { 'bar-baz': ['', '1', '2'] }], - // ↓ Functions can also be used to match classes. - // Classes here: qux-auto, qux-1000, qux-1001,… - bar: [{ qux: ['auto', (value) => Number(value) >= 1000] }], - baz: ['baz-sm', 'baz-md', 'baz-lg'], - }, - // ↓ Here you can define additional conflicts across different groups - conflictingClassGroups: { - // ↓ ID of class group which creates a conflict with… - // ↓ …classes from groups with these IDs - // In this case `twMerge('qux-auto foo') → 'foo'` - foo: ['bar'], + + // ↓ Optional config overrides + // Only elements from the second level onwards are overridden + override: { + // ↓ Theme scales to override + // Not all theme keys from the Tailwind config are supported by default. + theme: { + colors: ['black', 'white', 'yellow-500'], + }, + // ↓ Class groups to override + classGroups: { + // ↓ The `shadow` key here is the class group ID + // ↓ Creates group of classes which have conflicting styles + // Classes here: shadow-100, shadow-200, shadow-300, shadow-400, shadow-500 + shadow: [{ shadow: ['100', '200', '300', '400', '500'] }], + }, + // ↓ Conflicts across different groups to override + conflictingClassGroups: { + // ↓ ID of class group which creates a conflict with… + // ↓ …classes from groups with these IDs + // Here we remove the default conflict between the font-size and leading class + // groups. + 'font-size': [], + }, + // ↓ Conflicts between the postfix modifier of a group and a different class group to + // override + conflictingClassGroupModifiers: { + // You probably won't need this, but it follows the same shape as + // `conflictingClassGroups`. + }, }, - // ↓ Here you can define conflicts between the postfix modifier of a group and a different class group. - conflictingClassGroupModifiers: { - // ↓ ID of class group whose postfix modifier creates a conflict with… - // ↓ …classes from groups with these IDs - // In this case `twMerge('qux-auto baz-sm/1000') → 'baz-sm/1000'` - baz: ['bar'], + + // ↓ Optional config extensions + // Follows same shape as the `override` object. + extend: { + // ↓ Theme scales to extend or create + // Not all theme keys from the Tailwind config are supported by default. + theme: { + spacing: ['sm', 'md', 'lg'], + }, + // ↓ Class groups to extend or create + classGroups: { + // ↓ The `animate` key here is the class group ID + // ↓ Adds class animate-shimmer to existing group with ID `animate` or creates + // new class group if it doesn't exist. + animate: ['animate-shimmer'], + // ↓ Functions can also be used to match classes + // They take the class part value as argument and return a boolean defining whether + // it is a match. + // Here we accept all string classes starting with `aspec-w-` followed by a number. + 'aspect-w': [{ 'aspect-w': [(value) => Boolean(value) && !isNaN(value)] }], + 'aspect-h': [{ 'aspect-h': [(value) => Boolean(value) && !isNaN(value)] }], + 'aspect-reset': ['aspect-none'], + // ↓ You can also use validators exported by tailwind-merge + 'prose-size': [{ prose: ['base', validators.isTshirtSize] }], + }, + // ↓ Conflicts across different groups to extend or create + conflictingClassGroups: { + // ↓ ID of class group which creates a conflict with… + // ↓ …classes from groups with these IDs + // In this case `twMerge('aspect-w-5 aspect-none') → 'aspect-none'` + 'aspect-reset': ['aspect-w', 'aspect-h'], + }, + // ↓ Conflicts between the postfix modifier of a group and a different class group to + // extend or create + conflictingClassGroupModifiers: { + // You probably won't need this, but it follows the same shape as + // `conflictingClassGroups`. + }, }, }) ``` diff --git a/docs/configuration.md b/docs/configuration.md index 12e7f24a..62c58a19 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -160,29 +160,35 @@ If you modified one of these theme scales in your Tailwind config, you can add a ### Extending the tailwind-merge config -If you only need to extend the default tailwind-merge config, [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) is the easiest way to extend the config. You provide it a `configExtension` object which gets [merged](./api-reference.md#mergeconfigs) with the default config. Therefore, all keys here are optional. +If you only need to slightly modify the default tailwind-merge config, [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge) is the easiest way to extend the config. You provide it a `configExtension` object which gets [merged](./api-reference.md#mergeconfigs) with the default config. Therefore, all keys here are optional. ```ts import { extendTailwindMerge } from 'tailwind-merge' const customTwMerge = extendTailwindMerge({ - // ↓ Add values to existing theme scale or create a new one - theme: { - spacing: ['sm', 'md', 'lg'], - }, - // ↓ Add values to existing class groups or define new ones - classGroups: { - foo: ['foo', 'foo-2', { 'bar-baz': ['', '1', '2'] }], - bar: [{ qux: ['auto', (value) => Number(value) >= 1000] }], - baz: ['baz-sm', 'baz-md', 'baz-lg'], - }, - // ↓ Here you can define additional conflicts across class groups - conflictingClassGroups: { - foo: ['bar'], - }, - // ↓ Define conflicts between postfix modifiers and class groups - conflictingClassGroupModifiers: { - baz: ['bar'], + // ↓ Override eleemnts from the default config + // It has the same shape as the `extend` object, so we're going to skip it here. + override: {}, + // ↓ Extend values from the default config + extend: { + // ↓ Add values to existing theme scale or create a new one + theme: { + spacing: ['sm', 'md', 'lg'], + }, + // ↓ Add values to existing class groups or define new ones + classGroups: { + foo: ['foo', 'foo-2', { 'bar-baz': ['', '1', '2'] }], + bar: [{ qux: ['auto', (value) => Number(value) >= 1000] }], + baz: ['baz-sm', 'baz-md', 'baz-lg'], + }, + // ↓ Here you can define additional conflicts across class groups + conflictingClassGroups: { + foo: ['bar'], + }, + // ↓ Define conflicts between postfix modifiers and class groups + conflictingClassGroupModifiers: { + baz: ['bar'], + }, }, }) ``` diff --git a/docs/recipes.md b/docs/recipes.md index bf0f5d41..134478a8 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -6,15 +6,19 @@ How to configure tailwind-merge with some common patterns. > I have a custom shadow scale with the keys 100, 200 and 300 configured in Tailwind. How do I make tailwind-merge resolve conflicts among those? -You'll be able to do this by creating a custom `twMerge` functon with [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge). +We'll be able to do this by creating a custom `twMerge` functon with [`extendTailwindMerge`](./api-reference.md#extendtailwindmerge). -First, check whether your particular theme scale is included in tailwind-merge's theme config object [here](./configuration.md#theme). In the hypothetical case that tailwind-merge supported Tailwind's `boxShadow` theme scale, you could add it to the tailwind-merge config like this: +First, we need to know whether we want to override or extend the default scale. Let's say we extended the default config by putting the scale into the `extend` key in the Tailwind config. + +Then we check whether our particular theme scale is included in tailwind-merge's theme config object [here](./configuration.md#theme). In the hypothetical case that tailwind-merge supported Tailwind's `boxShadow` theme scale, we could add it to the tailwind-merge config like this: ```js const customTwMerge = extendTailwindMerge({ - theme: { - // The `boxShadow` key isn't actually supported - boxShadow: [{ shadow: ['100', '200', '300'] }], + extend: { + theme: { + // The `boxShadow` key isn't actually supported + boxShadow: [{ shadow: ['100', '200', '300'] }], + }, }, }) ``` @@ -23,23 +27,15 @@ In the case of the `boxShadow` scale, tailwind-merge doesn't include it in the t ```js const customTwMerge = extendTailwindMerge({ - classGroups: { - shadow: [{ shadow: ['100', '200', '300'] }], + extend: { + classGroups: { + shadow: [{ shadow: ['100', '200', '300'] }], + }, }, }) ``` -Note that by using `extendTailwindMerge` we're only adding our custom classes to the existing ones in the config, so `twMerge('shadow-200 shadow-lg')` will return the string `shadow-lg`. In most cases that's fine because you won't use that class in your project. - -If you expect classes like `shadow-lg` to be input in `twMerge` and don't want the class to cause incorrect merges, you can explicitly override the class group with [`createTailwindMerge`](./api-reference.md#createtailwindmerge), removing the default classes. - -```js -const customTwMerge = createTailwindMerge(() => { - const config = getDefaultConfig() - config.classGroups.shadow = [{ shadow: ['100', '200', '300'] }] - return config -}) -``` +Note that by using the `extend` object we're only adding our custom classes to the existing ones in the config, so `twMerge('shadow-200 shadow-lg')` will return the string `shadow-lg`. If we want to override the class instead, we need to use the `override` object instead. ## Extracting classes with Tailwind's [`@apply`](https://tailwindcss.com/docs/reusing-styles#extracting-classes-with-apply)