Skip to content

Commit 18e30b8

Browse files
author
Andrii Kirmas
committed
#24 Decline mod--val to set shape mod: true. Doc bem
1 parent 372b3fa commit 18e30b8

File tree

11 files changed

+176
-29
lines changed

11 files changed

+176
-29
lines changed

README.md

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ Tools to establish CSS classes as an explicit [abstraction layer](https://en.wik
2424
1. Enforce <u>single source of truth</u> of class appending – treat as TypeScript-driven dedupe
2525
2. Require strict `boolean` for value of class condition
2626
3. Use IDE type hints as developers' UX for faster issues resolving
27-
4. CSS-modules agnostic
27+
4. BEM
28+
5. CSS-modules agnostic
2829

2930
Use package like [`postcss-plugin-d-ts`](https://www.npmjs.com/package/postcss-plugin-d-ts) to prepare strict declaration of CSS
3031

@@ -43,17 +44,15 @@ import {
4344
classNamesMap,
4445

4546
// Identical function for TS restriction on classes determed in CSS and not used in component
46-
classNamesCheck
47+
classNamesCheck,
48+
49+
// Works with BEM conditional object
50+
classBeming
4751
} from "react-classnaming"
4852

4953
// Default export is the most frequently used function
5054
import classNaming from "react-classnaming"
5155

52-
// Import module with specific function only
53-
import { classNaming } from "react-classnaming/naming"
54-
import { classNamesCheck } from "react-classnaming/check"
55-
import { classNamesMap } from "react-classnaming/map"
56-
5756
import type {
5857
// Type to declare component's self CSS classes
5958
ClassNamesProperty,
@@ -144,6 +143,22 @@ Only declared CSS classes will be allowed as keys with IDE hint on possibilities
144143

145144
![classnaming_declared](./images/classnaming_declared.gif)
146145

146+
### BEM
147+
148+
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)
149+
150+
```diff
151+
import {
152+
- classNaming
153+
+ classBeming
154+
} from "react-classnaming"
155+
156+
- const cssClasses = classNaming<MyClassNames>()
157+
+ const bemClasses = classBeming<MyClassNames>()
158+
```
159+
160+
![](./images/classbeming.gif)
161+
147162
## Reference
148163

149164
### type `ClassNamed`
@@ -198,6 +213,53 @@ const withClassNameTwice = containerClass(
198213

199214
On `const` hovering will be tooltip with already conditioned classes under this chain
200215

216+
### function `classBeming`
217+
218+
Sets context to returned function for using BEM conditioned CSS classes queries. In general, argument's shape is
219+
220+
```typescript
221+
type BemInGeneral = {
222+
[__Block__]: boolean | __Block_Mod__ | {
223+
[__Element__ | $ /*key for block mods*/]: boolean | __BE_Mod__ | {
224+
[__Mod__]: false | (true | __BE_Mod_Value__ )
225+
}
226+
}
227+
}
228+
```
229+
230+
Table of output logic:
231+
232+
> Tests @ [./src/bem.core.test.ts:13](https://github.com/askirmas/react-classnaming/blob/main/src/bem.core.test.ts#L13-L35)
233+
234+
| Returned `className` | Query argument |
235+
| --------------------------------- | ------------------------------------------------------------ |
236+
| `""` | `{block: false}`<br />`{block: {el: false}}` |
237+
| | |
238+
| `"block"` | `{block: true}`<br />`{block: {$: boolean | {} | {[mod]: false} }}` |
239+
| `"block__el"` | `{block: {el: true | {} | {[mod]: false} }}` |
240+
| | |
241+
| `"block block--mod"` | `{block: "mod"}`<br/>`{block: {$: "mod" | {mod: true} }}` |
242+
| `"block__el block__el--mod"` | `{block: {el: "mod" | {mod: true} }}` |
243+
| | |
244+
| `"block block--mod--val"` | `{block: {$: {mod: "val"}}}` |
245+
| `"block__el block__el--mod--val"` | `{block: {el: {mod: "val"}}}` |
246+
247+
Mixins are deep merge of single possibilities in table
248+
249+
![](./images/classbeming.gif)
250+
251+
---
252+
253+
#### Setting options
254+
255+
Default options BEM naming:
256+
257+
- Element's separator is a double underscore `"__"`
258+
- Modifier's and value's separator is a double hyphen `"--"`
259+
- Key for block modifiers is `"$"`
260+
261+
It is required to change this options twice, both on JS (`setOpts(...)`) and TS `namespace ReactClassNaming { interface BemOptions {...} }`) levels
262+
201263
### function [`classNamesMap`](https://github.com/askirmas/react-classnaming/projects/5)
202264
203265
Function to map `classnames` to string props of some (i.e. 3rd-party) component.

__tests__/readme.spec.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react"
22
import expectRender from "../expect-to-same-render"
3-
import classNaming from "../src"
3+
import classNaming, { classBeming, ClassNamed } from "../src"
44
import type {ClassHash, ClassNamesProperty} from "../src"
55
// import css_module from "./button.module.css"
66
const css_module = {button: "BTN"}
@@ -160,3 +160,29 @@ it("Using ClassHash", () => {
160160
<button type="submit" className="BTN button_submit button--disabled">Submit</button>
161161
</>)
162162
})
163+
164+
it("bem", () => {
165+
type MyClassNames = ClassNamed & ClassNamesProperty<{
166+
form__item: ClassHash
167+
button: ClassHash
168+
"button--status--warning": ClassHash
169+
"button--status--danger": ClassHash
170+
button__icon: ClassHash
171+
"button__icon--hover": ClassHash
172+
"button__icon--focus": ClassHash
173+
}>
174+
const props = {className: "${props.className}"} as MyClassNames
175+
176+
const bem = classBeming(props)
177+
expectRender(
178+
<div {...bem(true, {
179+
form: {item: true},
180+
button: {
181+
$: {status: "danger"},
182+
icon: {hover: true}
183+
}
184+
})}/>
185+
).toSame(
186+
<div className="${props.className} form__item button button--status--danger button__icon button__icon--hover" />
187+
)
188+
})

images/classbeming.gif

149 KB
Loading

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
"typescript",
2929
"declarative",
3030
"css-classes",
31-
"css",
3231
"react",
32+
"bem",
33+
"css",
3334
"classname",
3435
"css-modules",
3536
"classnames",

src/bem.core.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {BemAbsraction} from "./bem.types"
1+
import type {BemInGeneral} from "./bem.types"
22
import type {BemOptions} from "./bem.core";
33
import {
44
bem2arr,
@@ -9,7 +9,7 @@ import {
99
describe(bem2arr.name, () => {
1010
describe("singletons", () => {
1111
const mod = undefined
12-
const suites: Record<string, [BemAbsraction, string][]> = {
12+
const suites: Record<string, [BemInGeneral, string][]> = {
1313
"block singleton": [
1414
[{block: false }, ""],
1515
[{block: true }, "block"],

src/bem.core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { BemAbsraction } from "./bem.types"
1+
import type { BemInGeneral } from "./bem.types"
22

33
let elementDelimiter = "__"
44
, modDelimiter = "--"
@@ -16,7 +16,7 @@ export {
1616
getOptions
1717
}
1818

19-
function bem2arr(query: BemAbsraction) {
19+
function bem2arr(query: BemInGeneral) {
2020
const $return: string[] = []
2121

2222
for (const block in query) {

src/bem.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CssModule } from "./definitions.types";
2-
import type { ClassBeming, BemAbsraction } from "./bem.types";
2+
import type { ClassBeming, BemInGeneral } from "./bem.types";
33
import { bem2arr } from "./bem.core";
44
import { joinWithLead, picker, wrapper } from "./core"
55
import { EMPTY_OBJECT } from "./consts.json"
@@ -8,6 +8,15 @@ export {
88
classBeming
99
}
1010

11+
/** Set context
12+
* @example
13+
* ```typescript
14+
* const bem = classBeming({classnames: require("./some.css"), className?})
15+
* const bem = classBeming(this.props)
16+
* const bem = classBeming<Props>()
17+
* const bem = classBeming<MyClassNames>()
18+
* ```
19+
*/
1120
function classBeming<
1221
Ctx extends {classnames: Source, className?: string},
1322
Source extends CssModule = Ctx["classnames"],
@@ -31,8 +40,8 @@ function bem<
3140
className?: string,
3241
classnames?: Source,
3342
},
34-
arg0?: boolean | BemAbsraction,
35-
arg1?: BemAbsraction
43+
arg0?: boolean | BemInGeneral,
44+
arg1?: BemInGeneral
3645
) {
3746
const source = typeof arg0 === "object" ? arg0 : arg1
3847
, debemed = source && bem2arr(source)

src/bem.types.ts

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,61 @@ import type {
44
Strip,
55
PartDeep,
66
Extends,
7-
// PartDeep
7+
Ever0
88
} from "./ts-swiss.types"
99
import type { ClassNamed } from "./main.types"
1010
import type {ReactClassNaming} from "."
1111

1212
export type ClassBeming<
1313
ClassNames extends CssModule,
1414
> =
15+
/**
16+
* Makes `string`-className from conditioned BEM query based on supplied CSS classes.
17+
* Destructed to singleton `{className: string}`, stringifyable object
18+
* @returns
19+
* ```typescript
20+
* // ""
21+
* {block: false}
22+
* {block: {el: false}}
23+
* // "block"
24+
* {block: true}
25+
* {block: {$: boolean | {} | {[mod]: false} }}
26+
* // "block__el"
27+
* {block: {el: true | {} | {[mod]: false} }}
28+
* // "block block--mod"
29+
* {block: "mod"}
30+
* {block: {$: "mod" | {mod: true} }}
31+
* // "block__el block__el--mod"
32+
* {block: {el: "mod" | {mod: true} }}
33+
* // "block block--mod--val"
34+
* {block: {$: {mod: "val"}}}
35+
* // "block__el block__el--mod--val"
36+
* {block: {el: {mod: "val"}}}
37+
* ```
38+
* @example
39+
* ```typescript
40+
* bem(true) // `${props.className}`
41+
* bem({button: true}) // "button"
42+
* bem({button: {icon: true}}) // "button__icon"
43+
* bem({button: "disabled"}) // "button button--disabled"
44+
* bem({button: {icon: {size: "big"}}}) // "button__icon button__icon--size--big"
45+
* bem(true, {
46+
* form: {item: true},
47+
* button: {
48+
* $: {status: "danger"},
49+
* icon: "hover"
50+
* }
51+
* }) // `${props.className} form__item button button--status--danger button__icon button__icon--hover`
52+
* ```
53+
* @example
54+
* ```typescript
55+
* <div {...bem(...)} />;
56+
* <div data-block={`${bem(...)}`} />
57+
* ```
58+
*/
1559
<
1660
Q1 extends undefined | boolean | BemQuery<keyof ClassNames>,
17-
// Q2 extends BemQuery<keyof ClassNames>,
61+
// Q2 extends BemQuery<keyof ClassNames> will be needed for #31
1862
>(
1963
arg0?: Q1 extends undefined | boolean ? Q1 : Subest<BemQuery<keyof ClassNames>, Q1> ,
2064
arg1?: Q1 extends undefined | boolean ? BemQuery<keyof ClassNames> : never
@@ -31,7 +75,7 @@ export type BemQuery<
3175
bModKey extends string = "blockModKey" extends keyof ReactClassNaming.BemOptions
3276
? ReactClassNaming.BemOptions["blockModKey"]
3377
: ReactClassNaming.BemOptions["$default"]["blockModKey"],
34-
> = string extends classes ? BemAbsraction : PartDeep<{
78+
> = string extends classes ? BemInGeneral : PartDeep<{
3579
[b in Strip<Strip<classes, delM>, delE>]: boolean
3680
| Exclude<MVs<classes, b, bModKey>, `${string}${delM}${string}`>
3781
| (
@@ -41,11 +85,15 @@ export type BemQuery<
4185
| Exclude<MVs<classes, b, e>, `${string}${delM}${string}`>
4286
| (
4387
{[m in Strip<MVs<classes, b, e>, delM>]:
44-
classes extends `${b}${
45-
e extends bModKey ? "" : `${delE}${e}`
46-
}${delM}${m}${delM}${infer V}`
47-
? false | V
48-
: boolean
88+
false | (
89+
Ever0<
90+
classes extends `${b}${
91+
e extends bModKey ? "" : `${delE}${e}`
92+
}${delM}${m}${delM}${infer V}`
93+
? V : never,
94+
true
95+
>
96+
)
4997
}
5098
)
5199
}
@@ -88,7 +136,7 @@ type MVs<
88136
e extends bModKey ? "" : `${delE}${e}`
89137
}${delM}${infer MV}` ? MV : never
90138

91-
export type BemAbsraction = {
139+
export type BemInGeneral = {
92140
[block: string]: undefined | boolean | string | {
93141
[el: string]: undefined | boolean | string | {
94142
[mod: string]: undefined | boolean | string

src/naming.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export { classNaming }
2222
/** Set context
2323
* @example
2424
* ```typescript
25-
* const classes = classNaming(this.props)
2625
* const classes = classNaming({classnames: require("./some.css"), className?})
26+
* const classes = classNaming(this.props)
2727
* const classes = classNaming<Props>()
2828
* const classes = classNaming<MyClassNames>()
2929
* ```

src/naming.types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ import type {
2323
// Making as interface make ts-errors much worth
2424
export type ClassNamingFn<Source extends CssModule, Used extends BoolDict, WithClassName extends boolean> =
2525
/**
26-
* Makes `string` from conditioned CSS classes as keys.
27-
* Destructed to singleton `{className: string}`, stringifyable, re-callable with propagation of previously stacked
26+
* Makes `string`-className from conditioned CSS classes as keys.
27+
* Destructed to singleton `{className: string}`, stringifyable object, re-callable with propagation of previously stacked
2828
* @example
2929
* ```typescript
30-
* classes({App}); // "App"
3130
* classes(true); // `${props.className}`
31+
* classes({App}); // "App"
3232
* classes(true && {App: true, "App--bad": false}); // `${props.className} App`
3333
* classes(); // `== classes`
3434
*

0 commit comments

Comments
 (0)