Skip to content

Commit

Permalink
#40 Add values array to bem interface. Add support for mods that are …
Browse files Browse the repository at this point in the history
…both bool and valueable
  • Loading branch information
Andrii Kirmas committed Mar 12, 2021
1 parent b22d974 commit b751375
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 51 deletions.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ import type {

## Basic usage

Example of simple CSS classes conditioning – [\__tests__/readme.spec.tsx:9](./__tests__/readme.spec.tsx#L9-L31)
Example of simple CSS classes conditioning – [./\__tests__/readme.spec.tsx:9](./__tests__/readme.spec.tsx#L9-L31)

```tsx
import classNaming from "react-classnaming"
Expand Down Expand Up @@ -114,19 +114,19 @@ You can find demonstration with all main points in folder [./\__examples__/](./_

### Condition is strictly `boolean`

Conditions with falsy values may lead to hardly caught bugs due to not obvious behavior for humans. In addition, as a possible `true` shortcut, the value can be not empty string as `class-hash` from CSS-module, and <u>`undefined`</u> for global CSS-class or modules simulation. Thus, to not keep in mind that `undefined` appears to be a truthy condition, it is prohibited on TypeScript level to mix in value type `boolean` with `ClassHash = string | undefined` and not allowed to use any other types like 0, null. [\__tests__/readme.spec.tsx:43](./__tests__/readme.spec.tsx#L43-L49)
Conditions with falsy values may lead to hardly caught bugs due to not obvious behavior for humans. In addition, as a possible `true` shortcut, the value can be not empty string as `class-hash` from CSS-module, and <u>`undefined`</u> for global CSS-class or modules simulation. Thus, to not keep in mind that `undefined` appears to be a truthy condition, it is prohibited on TypeScript level to mix in value type `boolean` with `ClassHash = string | undefined` and not allowed to use any other types like 0, null. [./\__tests__/readme.spec.tsx:43](./__tests__/readme.spec.tsx#L43-L49)

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

### Single source of truth

There can be only ONE condition for each class in call pipe. Already conditioned classes are propagated to next call type notation so you can see currently stacked with according *modality*: `true`, `false` or `boolean`. [\__tests__/readme.spec.tsx:55](./__tests__/readme.spec.tsx#L55-L63)
There can be only ONE condition for each class in call pipe. Already conditioned classes are propagated to next call type notation so you can see currently stacked with according *modality*: `true`, `false` or `boolean`. [./\__tests__/readme.spec.tsx:55](./__tests__/readme.spec.tsx#L55-L63)

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

### Declare own component's CSS classes

Only declared CSS classes will be allowed as keys with IDE hint on possibilities – [\__tests__/readme.spec.tsx:71](./__tests__/readme.spec.tsx#L71-L102)
Only declared CSS classes will be allowed as keys with IDE hint on possibilities – [./\__tests__/readme.spec.tsx:71](./__tests__/readme.spec.tsx#L71-L102)

```diff
+ import type { ClassHash, ClassNamesProperty } from "react-classnaming"
Expand All @@ -145,7 +145,7 @@ Only declared CSS classes will be allowed as keys with IDE hint on possibilities

### 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-L176)
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-L176)

```diff
import {
Expand Down Expand Up @@ -215,21 +215,22 @@ On `const` hovering will be tooltip with already conditioned classes under this

### function `classBeming`

Sets context to returned function for using BEM conditioned CSS classes queries. In general, argument's shape is
Sets context to returned function for using BEM conditioned CSS classes queries. General argument's shape is

```typescript
// .src/bem.types.ts#L84-L90
type BemInGeneral = {
[__Block_or_Element__]: undefined | boolean | __Block_Mod__ | {
[__Mod__]: false | (true | __BE_Mod_Value__ )
[base: string]: undefined | boolean | string
| (false|string)[]
| {
[mod: string]: undefined | boolean | string
}
}
```
Table of output logic:
Output logic: [./src/bem.core.test.ts:13](https://github.com/askirmas/react-classnaming/blob/main/src/bem.core.test.ts#L13-L35)
> Tests @ [./src/bem.core.test.ts:13](https://github.com/askirmas/react-classnaming/blob/main/src/bem.core.test.ts#L13-L35)
![](./images/classbeming.gif)
Featured example: [\./\__tests__/readme.spec.tsx:191](./__tests__/readme.spec.tsx#L191-L221)
---
Expand All @@ -238,7 +239,7 @@ Table of output logic:
Default options BEM naming:
- Modifier's and value's separator is a double hyphen `"--"`
- [#30](https://github.com/askirmas/react-classnaming/issues/30) ~~Element's separator is a double underscore `"__"`~~
- Element's separator is a double underscore `"__"`
It is required to change this options twice, both on JS (`setOpts(...)`) and TS `namespace ReactClassNaming { interface BemOptions {...} }`) levels
Expand Down Expand Up @@ -349,7 +350,7 @@ import css_module from "./some.css"; // With class `.never-used {...}`
#### Using CSS-modules or simulation
It is possible to use CSS modules or simulation without "context" by supplying class-hash value with variable [\__tests__/readme.spec.tsx:114](./__tests__/readme.spec.tsx#L114-L153)
It is possible to use CSS modules or simulation without "context" by supplying class-hash value with variable [./\__tests__/readme.spec.tsx:114](./__tests__/readme.spec.tsx#L114-L153)
```diff
// CSS-module, assuming "button" will be replaced with "BTN"
Expand Down
60 changes: 58 additions & 2 deletions __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, { classBeming } from "../src"
import classNaming, { classBeming, ClassNamed, Undefineds } from "../src"
import type {ClassHash, ClassNamesProperty} from "../src"
// import css_module from "./button.module.css"
const css_module = {button: "BTN"}
Expand Down Expand Up @@ -161,7 +161,7 @@ it("Using ClassHash", () => {
</>)
})

describe("bem leaf", () => {
it("bem leaf", () => {
type Props = ClassNamesProperty<MaterialClasses>
& { focused?: boolean }

Expand All @@ -185,6 +185,52 @@ describe("bem leaf", () => {
)
})

describe("bem from https://material.io/components/buttons/web#contained-button", () => {
const CONSTS = {ripple: "ripple-upgraded", icon: {"material-icons": true}} as const

type Props = ClassNamed & ClassNamesProperty<MaterialClasses>
& { focused?: boolean; clicking?: boolean }

const {ripple, icon} = CONSTS
const {
button__icon,
button__label,
button__ripple
} = {} as Undefineds<MaterialClasses>

function Button(props: Props) {
const {
clicking,
focused = false,
} = props

const bem = classBeming(props)

return <button {...bem(true, {
button: "raised",
[ripple]: [
"unbounded",
focused && "background-focused",
clicking ? "foreground-activation" : clicking === false && "foreground-deactivation"
]
})}>
<span {...bem({button__ripple})}/>
<i {...bem({button__icon, ...icon})}>bookmark</i>
<span {...bem({button__label})}>Contained Button plus Icon</span>
</button>
}

expectRender(
<Button className="dialog__button" clicking={false} focused={true} classnames={{} as MaterialClasses}/>
).toSame(
<button className="dialog__button button button--raised ripple-upgraded ripple-upgraded--unbounded ripple-upgraded--background-focused ripple-upgraded--foreground-deactivation">
<span className="button__ripple"/>
<i className="button__icon material-icons">bookmark</i>
<span className="button__label">Contained Button plus Icon</span>
</button>
)
})

type MaterialClasses = {
"material-icons": ClassHash
ripple: ClassHash
Expand All @@ -193,12 +239,22 @@ type MaterialClasses = {
"ripple--background-focused": ClassHash
"ripple--foreground-activation": ClassHash
"ripple--foreground-deactivation": ClassHash

"ripple-upgraded": ClassHash
"ripple-upgraded--bounded": ClassHash
"ripple-upgraded--unbounded": ClassHash
"ripple-upgraded--background-focused": ClassHash
"ripple-upgraded--foreground-activation": ClassHash
"ripple-upgraded--foreground-deactivation": ClassHash

button: ClassHash
"button--raised": ClassHash
"button--type--raised": ClassHash
"button--type--outlined": ClassHash
button__label: ClassHash
button__ripple: ClassHash
button__icon: ClassHash

dialog: ClassHash
dialog__button: ClassHash
}
4 changes: 1 addition & 3 deletions src/bem.core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ describe(bem2arr.name, () => {
[{base: false }, "base"],
[{base: true }, "base"],
[{base: "mod" }, "base base--mod"],
//@ts-expect-error //TODO #40
[{base: ["mod"] }, "base base--mod" /* TODO #40 "base base--mod"*/],
//@ts-expect-error //TODO #40
[{base: ["mod"] }, "base base--mod"],
[{base: [false] }, "base"],
[{base: {} }, "base"],
[{base: {mod: false}}, "base"],
Expand Down
1 change: 1 addition & 0 deletions src/bem.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ function bem2arr(query: BemInGeneral) {

// TODO check performance of `const in Array`
for (const mod in baseQ) {
//@ts-expect-error //TODO Split Array and Object?
const modValue = baseQ[mod]
if (!modValue)
continue
Expand Down
2 changes: 1 addition & 1 deletion src/bem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ it("TS UX", () => {
, check = {
1: bem(true, {
block1: {m2: "v1"},
block1__el1: "m1",
block1__el1: [false && "m1"],
})
}

Expand Down
69 changes: 68 additions & 1 deletion src/bem.types.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {BemQuery} from "./bem.types"
import type {BemQuery, Mods} from "./bem.types"
import { PartDeep } from "./ts-swiss.types"

describe("BemQuery", () => {
it("block", () => {
Expand Down Expand Up @@ -86,4 +87,70 @@ describe("BemQuery", () => {
}
expect(checks).toBeInstanceOf(Object)
})

it("mix on coincide", () => {
const checks: Record<string, BemQuery<"block--mod"|`${"block__el--mod"}--${"val1"|"val2"}`>> = {
"exact": {
block: "mod",
block__el: {
mod: "val1"
}
}
}
expect(checks).toBeInstanceOf(Object)
})
})


describe("Mods", () => {
it("single bool", () => {
const checks: Record<string, Mods<"b1", never>> = {
"str": "b1",
"arr": ["b1"],
"obj": {"b1": true}
}
expect(checks).toBeInstanceOf(Object)
})
it("single val", () => {
const checks: Record<string, Mods<never, {"m": "v1"|"v2"}>> = {
//@ts-expect-error
"arr": [],
"obj": {"m": "v1"}
}
expect(checks).toBeInstanceOf(Object)
})

it("mix", () => {
const checks: Record<string, Mods<"b1"|"b2", {"m": "v1"|"v2", "M": "X"|"Y"}>> = {
"bools arr": ["b1", "b2", false],
"single obj": {"M": "X"},
"mix arr": [
//@ts-expect-error //TODO consider
{"M": "X"}
, "b1"]
}
expect(checks).toBeInstanceOf(Object)
})

it("coincide", () => {
const checks: Record<string, Mods<"m", {"m": "v1"|"v2"}>> = {
"m": "m",
"m+": {"m": true},
"m: v1": {"m": "v1"}
}

expect(checks).toBeInstanceOf(Object)
})

it("mix with PartDeep", () => {
const checks: Record<string, PartDeep<Mods<"b1"|"b2", {"m": "v1"|"v2", "M": "X"|"Y"}>>> = {
"bools arr": ["b1", "b2"],
"single obj": {"M": "X"},
"mix arr": [
//@ts-expect-error //TODO consider
{"M": "X"}
, "b1"]
}
expect(checks).toBeInstanceOf(Object)
})
})
43 changes: 22 additions & 21 deletions src/bem.types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { CssModule } from "./definitions.types"
import type {
Strip,
Cut,
NoSubString,
PartDeep,
Extends,
Ever0
KeyOf,
Ever
} from "./ts-swiss.types"
import type { ClassNamed } from "./main.types"
import type {ReactClassNaming} from "."
Expand Down Expand Up @@ -55,34 +58,32 @@ export type BemQuery<
[base in Strip<classes, delM> | Strip<Strip<classes, delM>, delE>]: true
| (
Extends<classes, `${base}${delM}${string}`,
false
| Exclude<MVs<classes, base>, `${string}${delM}${string}`>
| (
{[m in Strip<MVs<classes, base>, delM>]:
false | (
Ever0<
classes extends `${base}${delM}${m}${delM}${infer V}`
? V : never,
true
>
)
Mods<
NoSubString<Cut<classes, `${base}${delM}`, true>, delM>,
{
[m in Strip<Cut<classes, `${base}${delM}`, true>, delM, true>]:
classes extends `${base}${delM}${m}${delM}${infer V}`
? V
: never
}
)
>
>
)
}>

type MVs<
classes extends string,
b extends string,
delM extends string = "modDelimiter" extends keyof ReactClassNaming.BemOptions
? ReactClassNaming.BemOptions["modDelimiter"]
: ReactClassNaming.BemOptions["$default"]["modDelimiter"],
> = classes extends `${b}${delM}${infer MV}` ? MV : never
export type Mods<Bools extends string, Enums extends Record<string, string>>
= false
//TODO #42 [false|Bools|Enum, ...Bools]
| Ever<Bools, Bools|(false | Bools)[], never>
| {[m in Bools | KeyOf<Enums>]?:
false
| (m extends Bools ? true : never)
| (m extends KeyOf<Enums> ? Enums[m] : never)
}

export type BemInGeneral = {
[base: string]: undefined | boolean | string
// TODO #40 | (false|string)[]
| (false|string)[]
| {
[mod: string]: undefined | boolean | string
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
ClassHash,
ClassNamed,
ClassNamesFrom,
Undefineds
} from "./main.types"

export default classNaming
Expand Down
12 changes: 12 additions & 0 deletions src/main.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,15 @@ export type ClassNamed = {
* ```
*/
export type ClassNamesFrom<C extends ReactRelated> = GetClassNames<GetProps<C>, EmptyObject, EmptyObject>

/**
* @example
* ```typescript
* const {primitive, array, object} = {} as Undefined<{
* primitive: any
* array: any[]
* object: {}
* }>
* ```
*/
export type Undefineds<M> = {[K in keyof M]: undefined}
Loading

0 comments on commit b751375

Please sign in to comment.