Skip to content

Commit 37a7938

Browse files
authored
Merge pull request #4 from WonderNetwork/feature/fix-automix
add a showcase test
2 parents 7f21902 + b27c6ee commit 37a7938

File tree

6 files changed

+210
-19
lines changed

6 files changed

+210
-19
lines changed

Readme.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
```jsx
1919
function Acme({ bem: { className, element } }) {
2020
return <div className={className}>
21-
<h1 class={element`heading`}>Hello</h1>
21+
<h1 className={element`heading`}>Hello</h1>
2222
</div>
2323
}
2424
```
@@ -66,7 +66,7 @@ function Acme({ bem: { block } }) {
6666

6767
```jsx
6868
function Acme({ bem: { block } }) {
69-
return <div className={mix`me-2 d-flex`}>
69+
return <div className={block``.mix`me-2 d-flex`}>
7070
</div>
7171
}
7272
```
@@ -94,20 +94,20 @@ function Parent({ bem: { className, element } }) {
9494

9595
```html
9696
<div class="parent">
97-
<div class="child parent__element child--active me2"/>
97+
<div class="child parent__element child--active me-2"/>
9898
</div>
9999
```
100100

101101

102102
### Using elements with modifiers
103103

104104
```jsx
105-
function Acme({ bem: { block, element } }) {
106-
return <div className={block}>
107-
<div class={element`item ${{ selected: true }} me-2`} />
108-
<div class={element`item ${{ variant: 'primary' }}`} />
109-
<div class={element`item ${['theme-dark']}`} />
110-
<div class={element`item`.mix`d-flex`} />
105+
function Acme({ bem: { className, element } }) {
106+
return <div className={className}>
107+
<div className={element`item ${{ selected: true }} me-2`} />
108+
<div className={element`item ${{ variant: 'primary' }}`} />
109+
<div className={element`item ${['theme-dark']}`} />
110+
<div className={element`item`.mix`d-flex`} />
111111
</div>
112112
}
113113
```

src/BemFactory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import classNames, { ClassName } from "./classNames";
33
export default class BemFactory {
44
constructor(
55
private readonly name: string,
6-
private autoMix: string | undefined = undefined,
6+
private autoMix: object | string = "",
77
) {}
88

99
block(...modifiers: ClassName[]): string {
1010
return classNames(
1111
this.name,
12-
this.autoMix,
12+
String(this.autoMix),
1313
this.prefixWith(this.name, modifiers),
1414
);
1515
}

src/index.spec.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,28 @@ describe("withBem", () => {
9999
"alpha__bravo px-2",
100100
);
101101
});
102+
103+
test("automatically mixing with parent block casts to string", () => {
104+
const Child = withBem.named(
105+
"Child",
106+
function Child({ bem: { className } }) {
107+
return <div className={className} data-testid="component" />;
108+
},
109+
);
110+
111+
const Parent = withBem.named(
112+
"Parent",
113+
function Parent({ bem: { className, element } }) {
114+
return (
115+
<div className={className}>
116+
<Child className={element`element`} />
117+
</div>
118+
);
119+
},
120+
);
121+
const { getByTestId } = render(<Parent />);
122+
const { className } = getByTestId("component");
123+
124+
expect(className).toBe("child parent__element");
125+
});
102126
});

src/index.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React, { useMemo } from "react";
2-
import classNames, { ClassNameHash } from "./classNames";
2+
import classNames, { ClassName } from "./classNames";
33
import BemFactory from "./BemFactory";
44

5-
type TemplateArgs = [TemplateStringsArray, ...ClassNameHash[]];
6-
type TemplateArgsZipped = (string | ClassNameHash)[];
5+
type TemplateArgs = [TemplateStringsArray, ...ClassName[]];
6+
type TemplateArgsZipped = (string | ClassName)[];
77
type TemplateFn = (...args: TemplateArgs) => string;
88
type MixableTemplateFn = (...args: TemplateArgs) => Mixable;
99
type TemplateFnZipped = (args: TemplateArgsZipped) => string;
@@ -14,17 +14,17 @@ type Mixable = string & {
1414
};
1515

1616
function taggedLiteral(fn: TemplateFnZipped): TemplateFn {
17-
function* zip(strings: string[], params: ClassNameHash[]) {
17+
function* zip(strings: string[], params: ClassName[]) {
1818
yield strings.shift() as string;
1919
while (strings.length) {
20-
yield params.shift() as ClassNameHash;
20+
yield params.shift() as ClassName;
2121
yield strings.shift() as string;
2222
}
2323
}
2424

2525
return (
2626
modifiers: TemplateStringsArray | undefined = undefined,
27-
...dynamic: ClassNameHash[]
27+
...dynamic: ClassName[]
2828
) => fn([...zip([...(modifiers || [])], [...dynamic])]);
2929
}
3030

@@ -48,7 +48,10 @@ type BemHelper = string & {
4848
mix: TemplateFn;
4949
};
5050

51-
function helperFactory(name: string, autoMix: string | undefined): BemHelper {
51+
function helperFactory(
52+
name: string,
53+
autoMix: object | string | undefined,
54+
): BemHelper {
5255
const snakeName = name.replace(/([a-z])(?=[A-Z])/g, "$1-").toLowerCase();
5356
const bemFactory = new BemFactory(snakeName, autoMix);
5457
const className = bemFactory.toString();
@@ -97,7 +100,10 @@ function createWrappedComponent<P>(
97100
): React.ComponentType<P & OptionalClassName> {
98101
const WrappedComponent = (args: P) => {
99102
const parentMix = (args as OptionalClassName)?.className;
100-
const bem = useMemo(() => helperFactory(name, parentMix), [parentMix]);
103+
const bem = useMemo(
104+
() => helperFactory(name, parentMix),
105+
[String(parentMix)],
106+
);
101107
return <Component {...args} bem={bem} />;
102108
};
103109
WrappedComponent.displayName = `Bem(${name})`;

src/showcase.spec.tsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import React from "react";
2+
import { withBem } from "./index";
3+
import { describe, expect, test } from "@jest/globals";
4+
import { toMatchJSX } from "./toMatchJSX";
5+
6+
expect.extend({
7+
toMatchJSX,
8+
});
9+
10+
describe("showcase", () => {
11+
test("Simplest way to create a block with some elements", () => {
12+
const Acme = withBem.named(
13+
"Acme",
14+
function ({ bem: { className, element } }) {
15+
return (
16+
<div className={className}>
17+
<h1 className={element`heading`}>Hello</h1>
18+
</div>
19+
);
20+
},
21+
);
22+
23+
expect(<Acme />).toMatchJSX(
24+
<div className="acme">
25+
<h1 className="acme__heading">Hello</h1>
26+
</div>,
27+
);
28+
});
29+
30+
test("BEM helper as a shorthand if there are no elements", () => {
31+
const Acme = withBem.named("Acme", function ({ bem }) {
32+
return <div className={bem}>Hello</div>;
33+
});
34+
35+
expect(<Acme />).toMatchJSX(<div className="acme">Hello</div>);
36+
});
37+
38+
test("Adding block modifiers", () => {
39+
const Acme = withBem.named("Acme", function ({ bem: { block } }) {
40+
const [toggle, setToggle] = React.useState(true);
41+
const onClick = React.useCallback(
42+
() => setToggle((current) => !current),
43+
[setToggle],
44+
);
45+
46+
return (
47+
<div className={block`${{ toggle }} always-enabled`}>
48+
<button onClick={onClick}>Toggle</button>
49+
</div>
50+
);
51+
});
52+
53+
expect(<Acme />).toMatchJSX(
54+
<div className="acme acme--toggle acme--always-enabled">
55+
<button>Toggle</button>
56+
</div>,
57+
);
58+
});
59+
60+
test("Mixing the block with other classes", () => {
61+
const Acme = withBem.named("Acme", function ({ bem: { block } }) {
62+
return <div className={block``.mix`me-2 d-flex`}></div>;
63+
});
64+
65+
expect(<Acme />).toMatchJSX(<div className="acme me-2 d-flex" />);
66+
});
67+
68+
test("Mixing with parent block", () => {
69+
const Child = withBem.named("Child", function Child({ bem: { block } }) {
70+
return <div className={block`${{ active: true }}`.mix`me-2`} />;
71+
});
72+
73+
const Parent = withBem.named(
74+
"Parent",
75+
function Parent({ bem: { className, element } }) {
76+
return (
77+
<div className={className}>
78+
<Child className={element`element`} />
79+
</div>
80+
);
81+
},
82+
);
83+
84+
expect(<Parent />).toMatchJSX(
85+
<div className="parent">
86+
<div className="child parent__element child--active me-2" />
87+
</div>,
88+
);
89+
});
90+
91+
test("Using elements with modifiers", () => {
92+
const Acme = withBem.named(
93+
"Acme",
94+
function ({ bem: { className, element } }: withBem.props) {
95+
return (
96+
<div className={className}>
97+
<div className={element`item ${{ selected: true }} me-2`} />
98+
<div className={element`item ${{ variant: "primary" }}`} />
99+
<div className={element`item ${["theme-dark"]}`} />
100+
<div className={element`item`.mix`d-flex`} />
101+
</div>
102+
);
103+
},
104+
);
105+
106+
expect(<Acme />).toMatchJSX(
107+
<div className="acme">
108+
<div className="acme__item acme__item--selected me-2" />
109+
<div className="acme__item acme__item--variant-primary" />
110+
<div className="acme__item acme__item--theme-dark" />
111+
<div className="acme__item d-flex" />
112+
</div>,
113+
);
114+
});
115+
});

src/toMatchJSX.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from "react";
2+
import { expect } from "@jest/globals";
3+
import { render } from "@testing-library/react";
4+
5+
function isReactJSXElement(received: unknown): received is React.ReactElement {
6+
return (
7+
typeof received === "object" &&
8+
received !== null &&
9+
"$$typeof" in received &&
10+
received.$$typeof === Symbol.for("react.element")
11+
);
12+
}
13+
14+
export function toMatchJSX(received: unknown, expected: unknown) {
15+
if (false === isReactJSXElement(received)) {
16+
return {
17+
pass: false,
18+
message: () => "Expected a JSX element",
19+
};
20+
}
21+
22+
if (false === isReactJSXElement(expected)) {
23+
return {
24+
pass: false,
25+
message: () => "Expected a JSX element",
26+
};
27+
}
28+
29+
expect(render(received).asFragment().firstChild).toEqual(
30+
render(expected).asFragment().firstChild,
31+
);
32+
33+
return {
34+
pass: true,
35+
message: () => "loo",
36+
};
37+
}
38+
39+
declare module "expect" {
40+
interface AsymmetricMatchers {
41+
toMatchJSX(expected: React.JSX.Element): void;
42+
}
43+
interface Matchers<R> {
44+
toMatchJSX(expected: React.JSX.Element): R;
45+
}
46+
}

0 commit comments

Comments
 (0)