Skip to content

Commit b751375

Browse files
author
Andrii Kirmas
committed
#40 Add values array to bem interface. Add support for mods that are both bool and valueable
1 parent b22d974 commit b751375

File tree

10 files changed

+198
-51
lines changed

10 files changed

+198
-51
lines changed

README.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ import type {
7070

7171
## Basic usage
7272

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

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

115115
### Condition is strictly `boolean`
116116

117-
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)
117+
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)
118118

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

121121
### Single source of truth
122122

123-
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)
123+
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)
124124

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

127127
### Declare own component's CSS classes
128128

129-
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)
129+
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)
130130

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

146146
### BEM
147147

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

150150
```diff
151151
import {
@@ -215,21 +215,22 @@ On `const` hovering will be tooltip with already conditioned classes under this
215215

216216
### function `classBeming`
217217

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

220220
```typescript
221+
// .src/bem.types.ts#L84-L90
221222
type BemInGeneral = {
222-
[__Block_or_Element__]: undefined | boolean | __Block_Mod__ | {
223-
[__Mod__]: false | (true | __BE_Mod_Value__ )
223+
[base: string]: undefined | boolean | string
224+
| (false|string)[]
225+
| {
226+
[mod: string]: undefined | boolean | string
224227
}
225228
}
226229
```
227230
228-
Table of output logic:
231+
Output logic: [./src/bem.core.test.ts:13](https://github.com/askirmas/react-classnaming/blob/main/src/bem.core.test.ts#L13-L35)
229232
230-
> Tests @ [./src/bem.core.test.ts:13](https://github.com/askirmas/react-classnaming/blob/main/src/bem.core.test.ts#L13-L35)
231-
232-
![](./images/classbeming.gif)
233+
Featured example: [\./\__tests__/readme.spec.tsx:191](./__tests__/readme.spec.tsx#L191-L221)
233234
234235
---
235236
@@ -238,7 +239,7 @@ Table of output logic:
238239
Default options BEM naming:
239240
240241
- Modifier's and value's separator is a double hyphen `"--"`
241-
- [#30](https://github.com/askirmas/react-classnaming/issues/30) ~~Element's separator is a double underscore `"__"`~~
242+
- Element's separator is a double underscore `"__"`
242243
243244
It is required to change this options twice, both on JS (`setOpts(...)`) and TS `namespace ReactClassNaming { interface BemOptions {...} }`) levels
244245
@@ -349,7 +350,7 @@ import css_module from "./some.css"; // With class `.never-used {...}`
349350
350351
#### Using CSS-modules or simulation
351352
352-
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)
353+
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)
353354
354355
```diff
355356
// CSS-module, assuming "button" will be replaced with "BTN"

__tests__/readme.spec.tsx

Lines changed: 58 additions & 2 deletions
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, { classBeming } from "../src"
3+
import classNaming, { classBeming, ClassNamed, Undefineds } from "../src"
44
import type {ClassHash, ClassNamesProperty} from "../src"
55
// import css_module from "./button.module.css"
66
const css_module = {button: "BTN"}
@@ -161,7 +161,7 @@ it("Using ClassHash", () => {
161161
</>)
162162
})
163163

164-
describe("bem leaf", () => {
164+
it("bem leaf", () => {
165165
type Props = ClassNamesProperty<MaterialClasses>
166166
& { focused?: boolean }
167167

@@ -185,6 +185,52 @@ describe("bem leaf", () => {
185185
)
186186
})
187187

188+
describe("bem from https://material.io/components/buttons/web#contained-button", () => {
189+
const CONSTS = {ripple: "ripple-upgraded", icon: {"material-icons": true}} as const
190+
191+
type Props = ClassNamed & ClassNamesProperty<MaterialClasses>
192+
& { focused?: boolean; clicking?: boolean }
193+
194+
const {ripple, icon} = CONSTS
195+
const {
196+
button__icon,
197+
button__label,
198+
button__ripple
199+
} = {} as Undefineds<MaterialClasses>
200+
201+
function Button(props: Props) {
202+
const {
203+
clicking,
204+
focused = false,
205+
} = props
206+
207+
const bem = classBeming(props)
208+
209+
return <button {...bem(true, {
210+
button: "raised",
211+
[ripple]: [
212+
"unbounded",
213+
focused && "background-focused",
214+
clicking ? "foreground-activation" : clicking === false && "foreground-deactivation"
215+
]
216+
})}>
217+
<span {...bem({button__ripple})}/>
218+
<i {...bem({button__icon, ...icon})}>bookmark</i>
219+
<span {...bem({button__label})}>Contained Button plus Icon</span>
220+
</button>
221+
}
222+
223+
expectRender(
224+
<Button className="dialog__button" clicking={false} focused={true} classnames={{} as MaterialClasses}/>
225+
).toSame(
226+
<button className="dialog__button button button--raised ripple-upgraded ripple-upgraded--unbounded ripple-upgraded--background-focused ripple-upgraded--foreground-deactivation">
227+
<span className="button__ripple"/>
228+
<i className="button__icon material-icons">bookmark</i>
229+
<span className="button__label">Contained Button plus Icon</span>
230+
</button>
231+
)
232+
})
233+
188234
type MaterialClasses = {
189235
"material-icons": ClassHash
190236
ripple: ClassHash
@@ -193,12 +239,22 @@ type MaterialClasses = {
193239
"ripple--background-focused": ClassHash
194240
"ripple--foreground-activation": ClassHash
195241
"ripple--foreground-deactivation": ClassHash
242+
243+
"ripple-upgraded": ClassHash
244+
"ripple-upgraded--bounded": ClassHash
245+
"ripple-upgraded--unbounded": ClassHash
246+
"ripple-upgraded--background-focused": ClassHash
247+
"ripple-upgraded--foreground-activation": ClassHash
248+
"ripple-upgraded--foreground-deactivation": ClassHash
249+
196250
button: ClassHash
251+
"button--raised": ClassHash
197252
"button--type--raised": ClassHash
198253
"button--type--outlined": ClassHash
199254
button__label: ClassHash
200255
button__ripple: ClassHash
201256
button__icon: ClassHash
257+
202258
dialog: ClassHash
203259
dialog__button: ClassHash
204260
}

src/bem.core.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ describe(bem2arr.name, () => {
1414
[{base: false }, "base"],
1515
[{base: true }, "base"],
1616
[{base: "mod" }, "base base--mod"],
17-
//@ts-expect-error //TODO #40
18-
[{base: ["mod"] }, "base base--mod" /* TODO #40 "base base--mod"*/],
19-
//@ts-expect-error //TODO #40
17+
[{base: ["mod"] }, "base base--mod"],
2018
[{base: [false] }, "base"],
2119
[{base: {} }, "base"],
2220
[{base: {mod: false}}, "base"],

src/bem.core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function bem2arr(query: BemInGeneral) {
3535

3636
// TODO check performance of `const in Array`
3737
for (const mod in baseQ) {
38+
//@ts-expect-error //TODO Split Array and Object?
3839
const modValue = baseQ[mod]
3940
if (!modValue)
4041
continue

src/bem.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ it("TS UX", () => {
4747
, check = {
4848
1: bem(true, {
4949
block1: {m2: "v1"},
50-
block1__el1: "m1",
50+
block1__el1: [false && "m1"],
5151
})
5252
}
5353

src/bem.types.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type {BemQuery} from "./bem.types"
1+
import type {BemQuery, Mods} from "./bem.types"
2+
import { PartDeep } from "./ts-swiss.types"
23

34
describe("BemQuery", () => {
45
it("block", () => {
@@ -86,4 +87,70 @@ describe("BemQuery", () => {
8687
}
8788
expect(checks).toBeInstanceOf(Object)
8889
})
90+
91+
it("mix on coincide", () => {
92+
const checks: Record<string, BemQuery<"block--mod"|`${"block__el--mod"}--${"val1"|"val2"}`>> = {
93+
"exact": {
94+
block: "mod",
95+
block__el: {
96+
mod: "val1"
97+
}
98+
}
99+
}
100+
expect(checks).toBeInstanceOf(Object)
101+
})
89102
})
103+
104+
105+
describe("Mods", () => {
106+
it("single bool", () => {
107+
const checks: Record<string, Mods<"b1", never>> = {
108+
"str": "b1",
109+
"arr": ["b1"],
110+
"obj": {"b1": true}
111+
}
112+
expect(checks).toBeInstanceOf(Object)
113+
})
114+
it("single val", () => {
115+
const checks: Record<string, Mods<never, {"m": "v1"|"v2"}>> = {
116+
//@ts-expect-error
117+
"arr": [],
118+
"obj": {"m": "v1"}
119+
}
120+
expect(checks).toBeInstanceOf(Object)
121+
})
122+
123+
it("mix", () => {
124+
const checks: Record<string, Mods<"b1"|"b2", {"m": "v1"|"v2", "M": "X"|"Y"}>> = {
125+
"bools arr": ["b1", "b2", false],
126+
"single obj": {"M": "X"},
127+
"mix arr": [
128+
//@ts-expect-error //TODO consider
129+
{"M": "X"}
130+
, "b1"]
131+
}
132+
expect(checks).toBeInstanceOf(Object)
133+
})
134+
135+
it("coincide", () => {
136+
const checks: Record<string, Mods<"m", {"m": "v1"|"v2"}>> = {
137+
"m": "m",
138+
"m+": {"m": true},
139+
"m: v1": {"m": "v1"}
140+
}
141+
142+
expect(checks).toBeInstanceOf(Object)
143+
})
144+
145+
it("mix with PartDeep", () => {
146+
const checks: Record<string, PartDeep<Mods<"b1"|"b2", {"m": "v1"|"v2", "M": "X"|"Y"}>>> = {
147+
"bools arr": ["b1", "b2"],
148+
"single obj": {"M": "X"},
149+
"mix arr": [
150+
//@ts-expect-error //TODO consider
151+
{"M": "X"}
152+
, "b1"]
153+
}
154+
expect(checks).toBeInstanceOf(Object)
155+
})
156+
})

src/bem.types.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { CssModule } from "./definitions.types"
22
import type {
33
Strip,
4+
Cut,
5+
NoSubString,
46
PartDeep,
57
Extends,
6-
Ever0
8+
KeyOf,
9+
Ever
710
} from "./ts-swiss.types"
811
import type { ClassNamed } from "./main.types"
912
import type {ReactClassNaming} from "."
@@ -55,34 +58,32 @@ export type BemQuery<
5558
[base in Strip<classes, delM> | Strip<Strip<classes, delM>, delE>]: true
5659
| (
5760
Extends<classes, `${base}${delM}${string}`,
58-
false
59-
| Exclude<MVs<classes, base>, `${string}${delM}${string}`>
60-
| (
61-
{[m in Strip<MVs<classes, base>, delM>]:
62-
false | (
63-
Ever0<
64-
classes extends `${base}${delM}${m}${delM}${infer V}`
65-
? V : never,
66-
true
67-
>
68-
)
61+
Mods<
62+
NoSubString<Cut<classes, `${base}${delM}`, true>, delM>,
63+
{
64+
[m in Strip<Cut<classes, `${base}${delM}`, true>, delM, true>]:
65+
classes extends `${base}${delM}${m}${delM}${infer V}`
66+
? V
67+
: never
6968
}
70-
)
69+
>
7170
>
7271
)
7372
}>
7473

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

8384
export type BemInGeneral = {
8485
[base: string]: undefined | boolean | string
85-
// TODO #40 | (false|string)[]
86+
| (false|string)[]
8687
| {
8788
[mod: string]: undefined | boolean | string
8889
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {
66
ClassHash,
77
ClassNamed,
88
ClassNamesFrom,
9+
Undefineds
910
} from "./main.types"
1011

1112
export default classNaming

src/main.types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,15 @@ export type ClassNamed = {
8989
* ```
9090
*/
9191
export type ClassNamesFrom<C extends ReactRelated> = GetClassNames<GetProps<C>, EmptyObject, EmptyObject>
92+
93+
/**
94+
* @example
95+
* ```typescript
96+
* const {primitive, array, object} = {} as Undefined<{
97+
* primitive: any
98+
* array: any[]
99+
* object: {}
100+
* }>
101+
* ```
102+
*/
103+
export type Undefineds<M> = {[K in keyof M]: undefined}

0 commit comments

Comments
 (0)