}
```
@@ -94,7 +94,7 @@ function Parent({ bem: { className, element } }) {
```html
```
@@ -102,12 +102,12 @@ function Parent({ bem: { className, element } }) {
### Using elements with modifiers
```jsx
-function Acme({ bem: { block, element } }) {
- return
-
-
-
-
+function Acme({ bem: { className, element } }) {
+ return
}
```
diff --git a/src/BemFactory.ts b/src/BemFactory.ts
index 67d5891..0716df4 100644
--- a/src/BemFactory.ts
+++ b/src/BemFactory.ts
@@ -3,13 +3,13 @@ import classNames, { ClassName } from "./classNames";
export default class BemFactory {
constructor(
private readonly name: string,
- private autoMix: string | undefined = undefined,
+ private autoMix: object | string = "",
) {}
block(...modifiers: ClassName[]): string {
return classNames(
this.name,
- this.autoMix,
+ String(this.autoMix),
this.prefixWith(this.name, modifiers),
);
}
diff --git a/src/index.spec.tsx b/src/index.spec.tsx
index a583be2..701837c 100644
--- a/src/index.spec.tsx
+++ b/src/index.spec.tsx
@@ -99,4 +99,28 @@ describe("withBem", () => {
"alpha__bravo px-2",
);
});
+
+ test("automatically mixing with parent block casts to string", () => {
+ const Child = withBem.named(
+ "Child",
+ function Child({ bem: { className } }) {
+ return
;
+ },
+ );
+
+ const Parent = withBem.named(
+ "Parent",
+ function Parent({ bem: { className, element } }) {
+ return (
+
+
+
+ );
+ },
+ );
+ const { getByTestId } = render(
);
+ const { className } = getByTestId("component");
+
+ expect(className).toBe("child parent__element");
+ });
});
diff --git a/src/index.tsx b/src/index.tsx
index 708beac..72025ba 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,9 +1,9 @@
import React, { useMemo } from "react";
-import classNames, { ClassNameHash } from "./classNames";
+import classNames, { ClassName } from "./classNames";
import BemFactory from "./BemFactory";
-type TemplateArgs = [TemplateStringsArray, ...ClassNameHash[]];
-type TemplateArgsZipped = (string | ClassNameHash)[];
+type TemplateArgs = [TemplateStringsArray, ...ClassName[]];
+type TemplateArgsZipped = (string | ClassName)[];
type TemplateFn = (...args: TemplateArgs) => string;
type MixableTemplateFn = (...args: TemplateArgs) => Mixable;
type TemplateFnZipped = (args: TemplateArgsZipped) => string;
@@ -14,17 +14,17 @@ type Mixable = string & {
};
function taggedLiteral(fn: TemplateFnZipped): TemplateFn {
- function* zip(strings: string[], params: ClassNameHash[]) {
+ function* zip(strings: string[], params: ClassName[]) {
yield strings.shift() as string;
while (strings.length) {
- yield params.shift() as ClassNameHash;
+ yield params.shift() as ClassName;
yield strings.shift() as string;
}
}
return (
modifiers: TemplateStringsArray | undefined = undefined,
- ...dynamic: ClassNameHash[]
+ ...dynamic: ClassName[]
) => fn([...zip([...(modifiers || [])], [...dynamic])]);
}
@@ -48,7 +48,10 @@ type BemHelper = string & {
mix: TemplateFn;
};
-function helperFactory(name: string, autoMix: string | undefined): BemHelper {
+function helperFactory(
+ name: string,
+ autoMix: object | string | undefined,
+): BemHelper {
const snakeName = name.replace(/([a-z])(?=[A-Z])/g, "$1-").toLowerCase();
const bemFactory = new BemFactory(snakeName, autoMix);
const className = bemFactory.toString();
@@ -97,7 +100,10 @@ function createWrappedComponent
(
): React.ComponentType
{
const WrappedComponent = (args: P) => {
const parentMix = (args as OptionalClassName)?.className;
- const bem = useMemo(() => helperFactory(name, parentMix), [parentMix]);
+ const bem = useMemo(
+ () => helperFactory(name, parentMix),
+ [String(parentMix)],
+ );
return ;
};
WrappedComponent.displayName = `Bem(${name})`;
diff --git a/src/showcase.spec.tsx b/src/showcase.spec.tsx
new file mode 100644
index 0000000..c4108b6
--- /dev/null
+++ b/src/showcase.spec.tsx
@@ -0,0 +1,115 @@
+import React from "react";
+import { withBem } from "./index";
+import { describe, expect, test } from "@jest/globals";
+import { toMatchJSX } from "./toMatchJSX";
+
+expect.extend({
+ toMatchJSX,
+});
+
+describe("showcase", () => {
+ test("Simplest way to create a block with some elements", () => {
+ const Acme = withBem.named(
+ "Acme",
+ function ({ bem: { className, element } }) {
+ return (
+
+
Hello
+
+ );
+ },
+ );
+
+ expect(
).toMatchJSX(
+
+
Hello
+ ,
+ );
+ });
+
+ test("BEM helper as a shorthand if there are no elements", () => {
+ const Acme = withBem.named("Acme", function ({ bem }) {
+ return
Hello
;
+ });
+
+ expect(
).toMatchJSX(
Hello
);
+ });
+
+ test("Adding block modifiers", () => {
+ const Acme = withBem.named("Acme", function ({ bem: { block } }) {
+ const [toggle, setToggle] = React.useState(true);
+ const onClick = React.useCallback(
+ () => setToggle((current) => !current),
+ [setToggle],
+ );
+
+ return (
+
+
+
+ );
+ });
+
+ expect(
).toMatchJSX(
+
+
+
,
+ );
+ });
+
+ test("Mixing the block with other classes", () => {
+ const Acme = withBem.named("Acme", function ({ bem: { block } }) {
+ return
;
+ });
+
+ expect(
).toMatchJSX(
);
+ });
+
+ test("Mixing with parent block", () => {
+ const Child = withBem.named("Child", function Child({ bem: { block } }) {
+ return
;
+ });
+
+ const Parent = withBem.named(
+ "Parent",
+ function Parent({ bem: { className, element } }) {
+ return (
+
+
+
+ );
+ },
+ );
+
+ expect(
).toMatchJSX(
+
,
+ );
+ });
+
+ test("Using elements with modifiers", () => {
+ const Acme = withBem.named(
+ "Acme",
+ function ({ bem: { className, element } }: withBem.props) {
+ return (
+
+ );
+ },
+ );
+
+ expect(
).toMatchJSX(
+
,
+ );
+ });
+});
diff --git a/src/toMatchJSX.tsx b/src/toMatchJSX.tsx
new file mode 100644
index 0000000..fdda99d
--- /dev/null
+++ b/src/toMatchJSX.tsx
@@ -0,0 +1,46 @@
+import React from "react";
+import { expect } from "@jest/globals";
+import { render } from "@testing-library/react";
+
+function isReactJSXElement(received: unknown): received is React.ReactElement {
+ return (
+ typeof received === "object" &&
+ received !== null &&
+ "$$typeof" in received &&
+ received.$$typeof === Symbol.for("react.element")
+ );
+}
+
+export function toMatchJSX(received: unknown, expected: unknown) {
+ if (false === isReactJSXElement(received)) {
+ return {
+ pass: false,
+ message: () => "Expected a JSX element",
+ };
+ }
+
+ if (false === isReactJSXElement(expected)) {
+ return {
+ pass: false,
+ message: () => "Expected a JSX element",
+ };
+ }
+
+ expect(render(received).asFragment().firstChild).toEqual(
+ render(expected).asFragment().firstChild,
+ );
+
+ return {
+ pass: true,
+ message: () => "loo",
+ };
+}
+
+declare module "expect" {
+ interface AsymmetricMatchers {
+ toMatchJSX(expected: React.JSX.Element): void;
+ }
+ interface Matchers
{
+ toMatchJSX(expected: React.JSX.Element): R;
+ }
+}