Skip to content

Commit

Permalink
#24 Decline mod--val to set shape mod: true. Doc bem
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrii Kirmas committed Mar 11, 2021
1 parent 372b3fa commit 18e30b8
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 29 deletions.
76 changes: 69 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Tools to establish CSS classes as an explicit [abstraction layer](https://en.wik
1. Enforce <u>single source of truth</u> 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

Expand All @@ -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,
Expand Down Expand Up @@ -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<MyClassNames>()
+ const bemClasses = classBeming<MyClassNames>()
```

![](./images/classbeming.gif)

## Reference

### type `ClassNamed`
Expand Down Expand Up @@ -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}`<br />`{block: {el: false}}` |
| | |
| `"block"` | `{block: true}`<br />`{block: {$: boolean | {} | {[mod]: false} }}` |
| `"block__el"` | `{block: {el: true | {} | {[mod]: false} }}` |
| | |
| `"block block--mod"` | `{block: "mod"}`<br/>`{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.
Expand Down
28 changes: 27 additions & 1 deletion __tests__/readme.spec.tsx
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down Expand Up @@ -160,3 +160,29 @@ it("Using ClassHash", () => {
<button type="submit" className="BTN button_submit button--disabled">Submit</button>
</>)
})

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(
<div {...bem(true, {
form: {item: true},
button: {
$: {status: "danger"},
icon: {hover: true}
}
})}/>
).toSame(
<div className="${props.className} form__item button button--status--danger button__icon button__icon--hover" />
)
})
Binary file added images/classbeming.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
"typescript",
"declarative",
"css-classes",
"css",
"react",
"bem",
"css",
"classname",
"css-modules",
"classnames",
Expand Down
4 changes: 2 additions & 2 deletions src/bem.core.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {BemAbsraction} from "./bem.types"
import type {BemInGeneral} from "./bem.types"
import type {BemOptions} from "./bem.core";
import {
bem2arr,
Expand All @@ -9,7 +9,7 @@ import {
describe(bem2arr.name, () => {
describe("singletons", () => {
const mod = undefined
const suites: Record<string, [BemAbsraction, string][]> = {
const suites: Record<string, [BemInGeneral, string][]> = {
"block singleton": [
[{block: false }, ""],
[{block: true }, "block"],
Expand Down
4 changes: 2 additions & 2 deletions src/bem.core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { BemAbsraction } from "./bem.types"
import type { BemInGeneral } from "./bem.types"

let elementDelimiter = "__"
, modDelimiter = "--"
Expand All @@ -16,7 +16,7 @@ export {
getOptions
}

function bem2arr(query: BemAbsraction) {
function bem2arr(query: BemInGeneral) {
const $return: string[] = []

for (const block in query) {
Expand Down
15 changes: 12 additions & 3 deletions src/bem.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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<Props>()
* const bem = classBeming<MyClassNames>()
* ```
*/
function classBeming<
Ctx extends {classnames: Source, className?: string},
Source extends CssModule = Ctx["classnames"],
Expand All @@ -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)
Expand Down
66 changes: 57 additions & 9 deletions src/bem.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,61 @@ import type {
Strip,
PartDeep,
Extends,
// PartDeep
Ever0
} from "./ts-swiss.types"
import type { ClassNamed } from "./main.types"
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
* <div {...bem(...)} />;
* <div data-block={`${bem(...)}`} />
* ```
*/
<
Q1 extends undefined | boolean | BemQuery<keyof ClassNames>,
// Q2 extends BemQuery<keyof ClassNames>,
// Q2 extends BemQuery<keyof ClassNames> will be needed for #31
>(
arg0?: Q1 extends undefined | boolean ? Q1 : Subest<BemQuery<keyof ClassNames>, Q1> ,
arg1?: Q1 extends undefined | boolean ? BemQuery<keyof ClassNames> : never
Expand All @@ -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<Strip<classes, delM>, delE>]: boolean
| Exclude<MVs<classes, b, bModKey>, `${string}${delM}${string}`>
| (
Expand All @@ -41,11 +85,15 @@ export type BemQuery<
| Exclude<MVs<classes, b, e>, `${string}${delM}${string}`>
| (
{[m in Strip<MVs<classes, b, e>, 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
>
)
}
)
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/naming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props>()
* const classes = classNaming<MyClassNames>()
* ```
Expand Down
6 changes: 3 additions & 3 deletions src/naming.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ import type {
// Making as interface make ts-errors much worth
export type ClassNamingFn<Source extends CssModule, Used extends BoolDict, WithClassName extends boolean> =
/**
* 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`
*
Expand Down
1 change: 1 addition & 0 deletions src/ts-swiss.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type Ever<T, V, D = EmptyObject> = [T] extends [never] ? D : V
export type Ever0<T, D = never> = [T] extends [never] ? D : T
export type EmptyObject = Record<never, never>
export type AnyObject = Record<string, any>
export type Falsy = undefined|null|false|0|""
Expand Down

0 comments on commit 18e30b8

Please sign in to comment.