diff --git a/README.md b/README.md index 35fc5e8..aca1ae7 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ Tools to establish CSS classes as an explicit [abstraction layer](https://en.wik 1. Enforce single source of truth of class appending – treat as TypeScript-driven dedupe 2. Require strict `boolean` for value of class condition 3. Use IDE type hints as developers' UX for faster issues resolving -4. CSS-modules agnostic +4. BEM +5. CSS-modules agnostic Use package like [`postcss-plugin-d-ts`](https://www.npmjs.com/package/postcss-plugin-d-ts) to prepare strict declaration of CSS @@ -43,17 +44,15 @@ import { classNamesMap, // Identical function for TS restriction on classes determed in CSS and not used in component - classNamesCheck + classNamesCheck, + + // Works with BEM conditional object + classBeming } from "react-classnaming" // Default export is the most frequently used function import classNaming from "react-classnaming" -// Import module with specific function only -import { classNaming } from "react-classnaming/naming" -import { classNamesCheck } from "react-classnaming/check" -import { classNamesMap } from "react-classnaming/map" - import type { // Type to declare component's self CSS classes ClassNamesProperty, @@ -144,6 +143,22 @@ Only declared CSS classes will be allowed as keys with IDE hint on possibilities ![classnaming_declared](./images/classnaming_declared.gif) +### BEM + +It is possible to use BEM as condition query. With explicitly declared CSS classes (i.e. via [`postcss-plugin-d-ts`](https://www.npmjs.com/package/postcss-plugin-d-ts)) TS and IDE will check and hint on available blocks, elements, modifiers and values. [\__tests__/readme.spec.tsx:165](./__tests__/readme.spec.tsx#L165-L186) + +```diff +import { +- classNaming ++ classBeming +} from "react-classnaming" + +- const cssClasses = classNaming() ++ const bemClasses = classBeming() +``` + +![](./images/classbeming.gif) + ## Reference ### type `ClassNamed` @@ -198,6 +213,53 @@ const withClassNameTwice = containerClass( On `const` hovering will be tooltip with already conditioned classes under this chain +### function `classBeming` + +Sets context to returned function for using BEM conditioned CSS classes queries. In general, argument's shape is + +```typescript +type BemInGeneral = { + [__Block__]: boolean | __Block_Mod__ | { + [__Element__ | $ /*key for block mods*/]: boolean | __BE_Mod__ | { + [__Mod__]: false | (true | __BE_Mod_Value__ ) + } + } +} +``` + +Table of output logic: + +> Tests @ [./src/bem.core.test.ts:13](https://github.com/askirmas/react-classnaming/blob/main/src/bem.core.test.ts#L13-L35) + +| Returned `className` | Query argument | +| --------------------------------- | ------------------------------------------------------------ | +| `""` | `{block: false}`
`{block: {el: false}}` | +| | | +| `"block"` | `{block: true}`
`{block: {$: boolean | {} | {[mod]: false} }}` | +| `"block__el"` | `{block: {el: true | {} | {[mod]: false} }}` | +| | | +| `"block block--mod"` | `{block: "mod"}`
`{block: {$: "mod" | {mod: true} }}` | +| `"block__el block__el--mod"` | `{block: {el: "mod" | {mod: true} }}` | +| | | +| `"block block--mod--val"` | `{block: {$: {mod: "val"}}}` | +| `"block__el block__el--mod--val"` | `{block: {el: {mod: "val"}}}` | + +Mixins are deep merge of single possibilities in table + +![](./images/classbeming.gif) + +--- + +#### Setting options + +Default options BEM naming: + +- Element's separator is a double underscore `"__"` +- Modifier's and value's separator is a double hyphen `"--"` +- Key for block modifiers is `"$"` + +It is required to change this options twice, both on JS (`setOpts(...)`) and TS `namespace ReactClassNaming { interface BemOptions {...} }`) levels + ### function [`classNamesMap`](https://github.com/askirmas/react-classnaming/projects/5) Function to map `classnames` to string props of some (i.e. 3rd-party) component. diff --git a/__tests__/readme.spec.tsx b/__tests__/readme.spec.tsx index 632ac42..d80eb50 100644 --- a/__tests__/readme.spec.tsx +++ b/__tests__/readme.spec.tsx @@ -1,6 +1,6 @@ import React from "react" import expectRender from "../expect-to-same-render" -import classNaming from "../src" +import classNaming, { classBeming, ClassNamed } from "../src" import type {ClassHash, ClassNamesProperty} from "../src" // import css_module from "./button.module.css" const css_module = {button: "BTN"} @@ -160,3 +160,29 @@ it("Using ClassHash", () => { ) }) + +it("bem", () => { + type MyClassNames = ClassNamed & ClassNamesProperty<{ + form__item: ClassHash + button: ClassHash + "button--status--warning": ClassHash + "button--status--danger": ClassHash + button__icon: ClassHash + "button__icon--hover": ClassHash + "button__icon--focus": ClassHash + }> + const props = {className: "${props.className}"} as MyClassNames + + const bem = classBeming(props) + expectRender( +
+ ).toSame( +
+ ) +}) diff --git a/images/classbeming.gif b/images/classbeming.gif new file mode 100644 index 0000000..cc320d7 Binary files /dev/null and b/images/classbeming.gif differ diff --git a/package.json b/package.json index 4613512..5ac08b1 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,9 @@ "typescript", "declarative", "css-classes", - "css", "react", + "bem", + "css", "classname", "css-modules", "classnames", diff --git a/src/bem.core.test.ts b/src/bem.core.test.ts index e837608..9d8de77 100644 --- a/src/bem.core.test.ts +++ b/src/bem.core.test.ts @@ -1,4 +1,4 @@ -import type {BemAbsraction} from "./bem.types" +import type {BemInGeneral} from "./bem.types" import type {BemOptions} from "./bem.core"; import { bem2arr, @@ -9,7 +9,7 @@ import { describe(bem2arr.name, () => { describe("singletons", () => { const mod = undefined - const suites: Record = { + const suites: Record = { "block singleton": [ [{block: false }, ""], [{block: true }, "block"], diff --git a/src/bem.core.ts b/src/bem.core.ts index 0c5a238..558410c 100644 --- a/src/bem.core.ts +++ b/src/bem.core.ts @@ -1,4 +1,4 @@ -import type { BemAbsraction } from "./bem.types" +import type { BemInGeneral } from "./bem.types" let elementDelimiter = "__" , modDelimiter = "--" @@ -16,7 +16,7 @@ export { getOptions } -function bem2arr(query: BemAbsraction) { +function bem2arr(query: BemInGeneral) { const $return: string[] = [] for (const block in query) { diff --git a/src/bem.ts b/src/bem.ts index 589f5d9..7194db0 100644 --- a/src/bem.ts +++ b/src/bem.ts @@ -1,5 +1,5 @@ import type { CssModule } from "./definitions.types"; -import type { ClassBeming, BemAbsraction } from "./bem.types"; +import type { ClassBeming, BemInGeneral } from "./bem.types"; import { bem2arr } from "./bem.core"; import { joinWithLead, picker, wrapper } from "./core" import { EMPTY_OBJECT } from "./consts.json" @@ -8,6 +8,15 @@ export { classBeming } +/** Set context + * @example + * ```typescript + * const bem = classBeming({classnames: require("./some.css"), className?}) + * const bem = classBeming(this.props) + * const bem = classBeming() + * const bem = classBeming() + * ``` + */ function classBeming< Ctx extends {classnames: Source, className?: string}, Source extends CssModule = Ctx["classnames"], @@ -31,8 +40,8 @@ function bem< className?: string, classnames?: Source, }, - arg0?: boolean | BemAbsraction, - arg1?: BemAbsraction + arg0?: boolean | BemInGeneral, + arg1?: BemInGeneral ) { const source = typeof arg0 === "object" ? arg0 : arg1 , debemed = source && bem2arr(source) diff --git a/src/bem.types.ts b/src/bem.types.ts index 307e5f0..1d2917f 100644 --- a/src/bem.types.ts +++ b/src/bem.types.ts @@ -4,7 +4,7 @@ import type { Strip, PartDeep, Extends, - // PartDeep + Ever0 } from "./ts-swiss.types" import type { ClassNamed } from "./main.types" import type {ReactClassNaming} from "." @@ -12,9 +12,53 @@ import type {ReactClassNaming} from "." export type ClassBeming< ClassNames extends CssModule, > = +/** + * Makes `string`-className from conditioned BEM query based on supplied CSS classes. + * Destructed to singleton `{className: string}`, stringifyable object + * @returns + * ```typescript + * // "" + * {block: false} + * {block: {el: false}} + * // "block" + * {block: true} + * {block: {$: boolean | {} | {[mod]: false} }} + * // "block__el" + * {block: {el: true | {} | {[mod]: false} }} + * // "block block--mod" + * {block: "mod"} + * {block: {$: "mod" | {mod: true} }} + * // "block__el block__el--mod" + * {block: {el: "mod" | {mod: true} }} + * // "block block--mod--val" + * {block: {$: {mod: "val"}}} + * // "block__el block__el--mod--val" + * {block: {el: {mod: "val"}}} + * ``` + * @example + * ```typescript + * bem(true) // `${props.className}` + * bem({button: true}) // "button" + * bem({button: {icon: true}}) // "button__icon" + * bem({button: "disabled"}) // "button button--disabled" + * bem({button: {icon: {size: "big"}}}) // "button__icon button__icon--size--big" + * bem(true, { + * form: {item: true}, + * button: { + * $: {status: "danger"}, + * icon: "hover" + * } + * }) // `${props.className} form__item button button--status--danger button__icon button__icon--hover` + * ``` + * @example + * ```typescript + *
; + *
+ * ``` +*/ < Q1 extends undefined | boolean | BemQuery, - // Q2 extends BemQuery, + // Q2 extends BemQuery will be needed for #31 >( arg0?: Q1 extends undefined | boolean ? Q1 : Subest, Q1> , arg1?: Q1 extends undefined | boolean ? BemQuery : never @@ -31,7 +75,7 @@ export type BemQuery< bModKey extends string = "blockModKey" extends keyof ReactClassNaming.BemOptions ? ReactClassNaming.BemOptions["blockModKey"] : ReactClassNaming.BemOptions["$default"]["blockModKey"], -> = string extends classes ? BemAbsraction : PartDeep<{ +> = string extends classes ? BemInGeneral : PartDeep<{ [b in Strip, delE>]: boolean | Exclude, `${string}${delM}${string}`> | ( @@ -41,11 +85,15 @@ export type BemQuery< | Exclude, `${string}${delM}${string}`> | ( {[m in Strip, delM>]: - classes extends `${b}${ - e extends bModKey ? "" : `${delE}${e}` - }${delM}${m}${delM}${infer V}` - ? false | V - : boolean + false | ( + Ever0< + classes extends `${b}${ + e extends bModKey ? "" : `${delE}${e}` + }${delM}${m}${delM}${infer V}` + ? V : never, + true + > + ) } ) } @@ -88,7 +136,7 @@ type MVs< e extends bModKey ? "" : `${delE}${e}` }${delM}${infer MV}` ? MV : never -export type BemAbsraction = { +export type BemInGeneral = { [block: string]: undefined | boolean | string | { [el: string]: undefined | boolean | string | { [mod: string]: undefined | boolean | string diff --git a/src/naming.ts b/src/naming.ts index 3be98c9..67bad2f 100644 --- a/src/naming.ts +++ b/src/naming.ts @@ -22,8 +22,8 @@ export { classNaming } /** Set context * @example * ```typescript - * const classes = classNaming(this.props) * const classes = classNaming({classnames: require("./some.css"), className?}) + * const classes = classNaming(this.props) * const classes = classNaming() * const classes = classNaming() * ``` diff --git a/src/naming.types.ts b/src/naming.types.ts index 6ae6838..188a076 100644 --- a/src/naming.types.ts +++ b/src/naming.types.ts @@ -23,12 +23,12 @@ import type { // Making as interface make ts-errors much worth export type ClassNamingFn = /** - * Makes `string` from conditioned CSS classes as keys. - * Destructed to singleton `{className: string}`, stringifyable, re-callable with propagation of previously stacked + * Makes `string`-className from conditioned CSS classes as keys. + * Destructed to singleton `{className: string}`, stringifyable object, re-callable with propagation of previously stacked * @example * ```typescript - * classes({App}); // "App" * classes(true); // `${props.className}` + * classes({App}); // "App" * classes(true && {App: true, "App--bad": false}); // `${props.className} App` * classes(); // `== classes` * diff --git a/src/ts-swiss.types.ts b/src/ts-swiss.types.ts index a565741..fd4127b 100644 --- a/src/ts-swiss.types.ts +++ b/src/ts-swiss.types.ts @@ -1,4 +1,5 @@ export type Ever = [T] extends [never] ? D : V +export type Ever0 = [T] extends [never] ? D : T export type EmptyObject = Record export type AnyObject = Record export type Falsy = undefined|null|false|0|""