Skip to content

Commit 1bf2cc5

Browse files
committed
init
0 parents  commit 1bf2cc5

10 files changed

Lines changed: 5022 additions & 0 deletions

BemFactory.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import classNames, { ClassName } from "./classNames";
2+
3+
export default class BemFactory {
4+
constructor(
5+
private readonly name: string,
6+
private autoMix: string | undefined = undefined,
7+
) {}
8+
9+
block(...modifiers: ClassName[]): string {
10+
return classNames(
11+
this.name,
12+
this.autoMix,
13+
this.prefixWith(this.name, modifiers),
14+
);
15+
}
16+
17+
element(block: string, ...modifiers: ClassName[]): string {
18+
const blockName = `${this.name}__${block.trim()}`;
19+
return classNames(blockName, this.prefixWith(blockName, modifiers));
20+
}
21+
22+
toString() {
23+
return this.block();
24+
}
25+
26+
valueOf() {
27+
return this.block();
28+
}
29+
30+
private prefixWith(prefix: string, modifier: ClassName[]) {
31+
return classNames(...modifier).replace(/^(?=.)|\s+/g, `$&${prefix}--`);
32+
}
33+
}

Readme.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# React BEM template functions
2+
3+
* [Installation](#installation)
4+
* [Usage](#usage)
5+
* [Configuring your component](#adding-the-bem-helper-to-your-components)
6+
7+
## Installation
8+
9+
`npm install react-bem-template-functions`
10+
11+
## Usage
12+
13+
> [!NOTE]
14+
> Learn how to inject the `bem` prop in [the next chapter](#adding-the-bem-helper-to-your-components)
15+
16+
<table>
17+
<tr><td colspan=2>Simplest way to create a block with some elements</td></tr>
18+
<tr>
19+
<td>
20+
21+
```jsx
22+
function Acme({ bem: { className, element } }) {
23+
return <div className={className}>
24+
<h1 class={element`heading`}>Hello</h1>
25+
</div>
26+
}
27+
```
28+
</td>
29+
<td>
30+
31+
```html
32+
<div class="acme">
33+
<h1 class="acme__heading">Hello</h1>
34+
</div>
35+
```
36+
</td>
37+
</tr>
38+
<tr><td colspan=2>BEM helper as a shorthand if there are no elements</td></tr>
39+
<tr>
40+
<td>
41+
42+
```jsx
43+
function Acme({ bem }) {
44+
return <div className={bem}>Hello</div>
45+
}
46+
```
47+
</td>
48+
<td>
49+
50+
```html
51+
<div class="acme">Hello</div>
52+
```
53+
</tr>
54+
55+
<tr><td colspan=2>Adding block modifiers</td></tr>
56+
<tr>
57+
<td>
58+
59+
```jsx
60+
function Acme({ bem: { block } }) {
61+
const [on, setOn] = useState(true);
62+
const onClick = useCallback(
63+
() => setOn(current => !current),
64+
[setOn],
65+
);
66+
67+
return <div className={block`${{ on }} mod`}>
68+
<button onClick={onClick}>Toggle</button>
69+
</div>
70+
}
71+
```
72+
</td>
73+
<td>
74+
75+
```html
76+
<div class="acme acme--on acme--mod"/>
77+
```
78+
</tr>
79+
80+
<tr><td colspan=2>Mixing the block with other classes</td></tr>
81+
<tr>
82+
<td>
83+
84+
```jsx
85+
function Acme({ bem: { block } }) {
86+
return <div className={mix`me-2 d-flex`}>
87+
</div>
88+
}
89+
```
90+
</td>
91+
<td>
92+
93+
```html
94+
<div class="acme me-2 d-flex">...</div>
95+
```
96+
</tr>
97+
98+
<tr><td colspan=2>
99+
To mix a block with a parent element just pass the element name as `className`
100+
and it will be appended automatically
101+
</td></tr>
102+
<tr>
103+
<td>
104+
105+
```jsx
106+
function Child({ bem: { block } }) {
107+
const mod = { active: true }
108+
return <div className={block`${mod}`.mix`me-2`}/>
109+
}
110+
111+
function Parent({ bem: { className, element } }) {
112+
return <div className={className}>
113+
<Child className={element`element`}/>
114+
</div>
115+
}
116+
```
117+
</td>
118+
<td>
119+
120+
```html
121+
<div class="parent">
122+
<div class="
123+
child
124+
parent__element
125+
child--active
126+
me2
127+
"/>
128+
</div>
129+
```
130+
</tr>
131+
132+
133+
<tr><td colspan=2>Using elements with modifiers</td></tr>
134+
<tr>
135+
<td>
136+
137+
```jsx
138+
function Acme({ bem: { block, element } }) {
139+
return <div className={block}>
140+
<div class={
141+
element`item ${{ selected: true }} me-2`
142+
}/>
143+
<div class={
144+
element`item ${{ variant: 'primary' }}`
145+
}/>
146+
<div class={
147+
element`item ${['theme-dark']}`
148+
}/>
149+
<div class={
150+
element`item`.mix`d-flex`
151+
}/>
152+
</div>
153+
}
154+
```
155+
</td>
156+
<td>
157+
158+
```html
159+
<div class="acme">
160+
<div class="
161+
acme__item acme__item--selected me-2
162+
"/>
163+
<div class="
164+
acme__item acme__item--variant-primary
165+
"/>
166+
<div class="
167+
acme__item acme__item--theme-dark
168+
"/>
169+
<div class="
170+
acme__item d-flex
171+
"/>
172+
</div>
173+
```
174+
</tr>
175+
176+
177+
<tbody>
178+
</table>
179+
180+
## Adding the BEM helper to your components
181+
182+
Let’s assume you have a component you’d like to use BEM with:
183+
184+
```tsx
185+
type Props = {
186+
title: string;
187+
}
188+
189+
export default function AcmeBanner({ title }: Props) {
190+
return <div>
191+
Hello {title}
192+
</div>;
193+
}
194+
```
195+
196+
BEM is a naming strategy, so let’s reuse the same name between your React component
197+
display name and your CSS block name. The CSS name will be converted to `snake-case`.
198+
199+
1. Import the `withBem` Higher Order Component
200+
2. Add a `displayName` to your component
201+
3. (Typescript): Wrap your prop types with `withBem.props<>` type
202+
4. Wrap your export in a `withBem()` HOC
203+
204+
```tsx
205+
import { withBem } from 'react-bem-template-functions'
206+
207+
type Props = {
208+
title: string;
209+
}
210+
211+
function AcmeBanner({ bem: { className }, title }: withBem.props<Props>) {
212+
return <div className={className}>
213+
Hello {title}
214+
</div>;
215+
}
216+
AcmeBanner.displayName = 'AcmeBanner';
217+
export default withBem(AcmeBanner);
218+
```
219+
220+
221+
Optionally, you might pass the component name explicitly using `withBem.named()`:
222+
223+
```jsx
224+
export default withBem.named('AcmeComponent', ({ bem, title }) => {
225+
return <div className={bem}>
226+
Hello {title}
227+
</div>;
228+
});
229+
```

classNames.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import classNames from "./classNames";
2+
3+
describe("classNames", () => {
4+
test("gigo", () => {
5+
expect(classNames(null)).toBe("");
6+
expect(classNames(undefined)).toBe("");
7+
expect(classNames(false)).toBe("");
8+
expect(classNames({})).toBe("");
9+
expect(classNames("")).toBe("");
10+
expect(classNames([""])).toBe("");
11+
expect(classNames(["", false, undefined, {}])).toBe("");
12+
});
13+
14+
test("simple", () => {
15+
expect(
16+
classNames("alpha", "", ["bravo", { charlie: true }], { delta: false }),
17+
).toBe("alpha bravo charlie");
18+
});
19+
20+
test("prefixed objects", () => {
21+
expect(classNames({ variant: "blue" })).toBe("variant-blue");
22+
});
23+
});

classNames.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const identity = Boolean;
2+
3+
export type ClassNameHash = Record<string, unknown>;
4+
export type ClassNameSpec = string | undefined | null | false | ClassNameHash;
5+
export type ClassName = ClassNameSpec | ClassNameSpec[];
6+
7+
export default function classNames(...args: ClassName[]): string {
8+
const toString = (input: ClassName) => {
9+
if (typeof input === "string") {
10+
return input.trim();
11+
}
12+
13+
if (null === input || undefined === input || false === input) {
14+
return "";
15+
}
16+
17+
if (Array.isArray(input)) {
18+
return classNames(...input);
19+
}
20+
21+
const value = (condition: true | unknown, name: string) =>
22+
true === condition ? name : `${name}-${condition}`;
23+
24+
// leaves us with `object`
25+
return Object.entries(input)
26+
.map(([name, condition]) => (condition ? value(condition, name) : null))
27+
.filter(identity)
28+
.join(" ");
29+
};
30+
31+
return args.map(toString).filter(identity).join(" ");
32+
}

0 commit comments

Comments
 (0)