Skip to content

Commit

Permalink
Merge 3f08123 into aefc9e7
Browse files Browse the repository at this point in the history
  • Loading branch information
eszthoff committed Aug 16, 2019
2 parents aefc9e7 + 3f08123 commit eddd875
Show file tree
Hide file tree
Showing 19 changed files with 187 additions and 277 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ exports[`<Checkbox> that renders a checkbox should render default checkbox corre
size="normal"
>
<span
className=" Checkbox__text"
className="Checkbox__text"
>
Check this out
</span>
Expand Down
11 changes: 9 additions & 2 deletions src/components/List/ListItem/ListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ import styles from './ListItem.scss';
const { block, elem } = bem({
name: 'ListItem',
classnames: styles,
propsToMods: ['isSelected', 'isHighlighted', 'onClick', 'disabled', 'highlightContext'],
propsToMods: [
'isSelected',
'isHighlighted',
'onClick',
'disabled',
'highlightContext',
'clickable',
],
});

const ListItem = React.forwardRef((props, ref) => {
Expand All @@ -24,7 +31,7 @@ const ListItem = React.forwardRef((props, ref) => {
const customBlockMod = { clickable: typeof onClick === 'function' };

return (
<li {...rest} ref={ref} {...block(props, customBlockMod)}>
<li {...rest} ref={ref} {...block({ ...props, ...customBlockMod })}>
<div onClick={onClick} role="presentation" {...elem('container', props)}>
{React.Children.map(children, child =>
typeof child === 'string' ? <Text inline>{child}</Text> : child
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ exports[`<RadioButton> that renders a radio button should render radio button wi
size="normal"
>
<span
className=" RadioButton__text"
className="RadioButton__text"
>
Choose me
</span>
Expand Down
109 changes: 67 additions & 42 deletions src/packages/bem/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
BEM
===
# BEM

bem.js automatically produces a list of classnames for your component based on its props and state.

Classnames being generated based on BEM convention with the following assumtions:
* As a block name we use React component name.
* We can declare elements with `{ ...this.elem('elementName') }` construction
* Modifyer is a component's prop or state name and its value (if value is of boolean type, it is ommited).
Classnames being generated based on BEM convention with the following assumptions:

- As a block name we use React component name.
- We can declare elements with `{ ...this.elem('elementName') }` construction
- Modifier is a component's prop or state name and its value (if value is of boolean type, it is omitted).

As a separators we use:
* element prefix: `__` (double underscore)
* modifyer prefix `--` (double dash)
* modifyer's value prefix is `_` (single underscore)

- element prefix: `__` (double underscore)
- modifier prefix `--` (double dash)
- modifier's value prefix is `_` (single underscore)

In terms of CSS classnames it looks like this:

```css
/* component's root node class name */
.ComponentName {}
/* component's root node class name with boolean modifyer applied */
/* component's root node class name with boolean modifier applied */
.ComponentName--modName {}
/* component's root node class name with string/number modifyer applied */
/* component's root node class name with string/number modifier applied */
.ComponentName--modName_modValue {}
/* component's sub node (element) class name */
.ComponentName__elem {}
/* component's root node class name with boolean modifyer + value applied */
/* component's root node class name with boolean modifier + value applied */
.ComponentName__elem--modName {}
/* component's root node class name with string/number modifyer + value applied */
/* component's root node class name with string/number modifier + value applied */
.ComponentName__elem--modName_modValue {}
```

Example of usage
----------------
## Example of usage

### Statefull component

Button.js

```js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
Expand Down Expand Up @@ -62,7 +64,7 @@ class Button extends Component {
return (
{/*
3. Add { ...this.block() } construction to declare node as a block root
Note! If needed, {...this.props} should be spreaded before { ...this.block() } in order
Note! If needed, {...this.props} should be spread before { ...this.block() } in order
to avoid className overwriting.
*/}
<button {...this.props} { ...this.block() } onClick={this.handleClick}>
Expand Down Expand Up @@ -109,6 +111,7 @@ export default bem(classnamesMap)(Button)
### Stateless component

ButtonStateless.js

```js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
Expand All @@ -120,7 +123,8 @@ const { block, elem } = bem({
classnames: classnamesMap,
// 1. If you need to have class name (.ButtonStateless--active) that depends on
// `active` prop, just list this prop in propsToMods list.
propsToMods: ['active']
// Make sure to list custom modifiers as well, if you have any (see point 4. below)
propsToMods: ['active', 'almostRandomValue']
});

const almostRandomValue = (props) => {
Expand All @@ -133,7 +137,7 @@ const ButtonStateless = (props) => {
return (
{/*
2. Add { ...block(props) } construction to declare node as a block
Note! If needed, {...props} should be spreaded before { ...block(props) } in order
Note! If needed, {...props} should be spread before { ...block(props) } in order
to avoid className overwriting.
*/}
<button {...props} { ...block(props) }>
Expand All @@ -144,10 +148,9 @@ const ButtonStateless = (props) => {
{props.children}
</span>
{/*
4. If you need to add some custom modifiers, you can pass it as 3rd argument to the elem function.
Or as a 2nd argument to block function. E.g. { ...block(props, { custom: 'modifier' }) }
4. If you need to add some custom modifiers, you can add it to the props.
*/}
<span { ...elem('icon', props, { almostRandomValue: almostRandomValue(props) }) }>
<span { ...elem('icon', { ...props, almostRandomValue: almostRandomValue(props) }) }>
{props.children}
</span>
</button>
Expand All @@ -165,11 +168,10 @@ export default ButtonStateless;
```

Button.scss
```css

```css
/* Component's root node class name */
.Button {

display: inline-block;

/*
Expand Down Expand Up @@ -228,8 +230,7 @@ Button.scss
color: yellow;
}


/*
/*
Block "Button", element "label", modifier "extraordinary" (based on props.type), value "extraordinary".
Is applied to the component's label node when `props.type = "extraordinary"` is set.
*/
Expand All @@ -239,14 +240,42 @@ Button.scss
}
```

Examples of outcome
-------------------
### Using elem to enrich existing elements

If you wish to enrich an existing element (e.g. child of the component) with extra classes, you need to make sure to preserve already existing classes on that element. To achieve that you can list existing classes as the 3rd argument for `elem`. For example:

```jsx
import React from 'react';
import bem from '../../..';
import styles from './styles.json';

const { block, elem } = bem({
name: 'List',
classnames: styles,
});

const List = props => (
<ul {...block(props)}>
{React.Children.map(props.children, child =>
// Note the 3rd argument when calling 'elem'
child ? React.cloneElement(child, elem('item', props, child.props.className)) : null
)}
</ul>
);

List.displayName = 'List';

export default List;
```

## Examples of outcome

Having the example above we can get the following results.
`bem` decorator adds only classnames that are declared in a stylesheet and
respectively exists in classnames map.

### No props:

```html
<Button />
↓ ↓ ↓
Expand All @@ -259,47 +288,43 @@ respectively exists in classnames map.

```html
<Button active={true} />

↓ ↓ ↓

↓ ↓ ↓
<button class="Button Button--active">
<span class="Button__label Button__label--active" />
</button>
```

### Prop `active` and `type` are set:

**Note** that property of a boolean type `active={true}` produces `Button__label--active` (*without* mod value), when property of a string type `type='extraordinary'` gives us two classnameas: `Button__label--type` (*without* mod value) and `Button__label--type_extraordinary` (*with* mod value).
**Note** that property of a boolean type `active={true}` produces `Button__label--active` (_without_ mod value), when property of a string type `type='extraordinary'` gives us two classnames: `Button__label--type` (_without_ mod value) and `Button__label--type_extraordinary` (_with_ mod value).

```html
<Button active={true} type='extraordinary' />

↓ ↓ ↓

<Button active={true} type="extraordinary" />
↓ ↓ ↓
<button class="Button Button--active Button--type Button--type_extraordinary">
<span class="Button__label Button__label--active Button__label--type Button__label--type_extraordinary" />
<span
class="Button__label Button__label--active Button__label--type Button__label--type_extraordinary"
/>
</button>
```

### Prop `active` equals false

No classnames will be produced if boolean property has `false` value.

```html
<Button active={false} />

↓ ↓ ↓

↓ ↓ ↓
<button class="Button">
<span class="Button__label" />
</button>
```

### Clicked state

```html
<Button /> <!-- this.setState({ clicked: true }) -->

↓ ↓ ↓

↓ ↓ ↓
<button class="Button Button--clicked">
<span class="Button__label Button__label--clicked" />
</button>
Expand Down
85 changes: 25 additions & 60 deletions src/packages/bem/__tests__/bem.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Button, ButtonStateless, Avatar, AvatarStateless } from './dummy-components';
import { Button, ButtonStateless, List, Unstyled } from './dummy-components';

describe('BEM decorator', () => {
describe('Decorate stateFUL class component', () => {
Expand Down Expand Up @@ -47,35 +47,7 @@ describe('BEM decorator', () => {
expect(buttonLabel.hasClass('Button__label--size')).toBe(false);
});

it('should add proper class names based on extra mods with string value', () => {
const avatarWrapper = shallow(<Avatar match={80} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('Avatar--outlineColor_green')).toBe(true);
expect(avatarImage.hasClass('Avatar__image--outlineColor_green')).toBe(true);
});

it('should add proper class names based on extra mods with number value', () => {
const avatarWrapper = shallow(<Avatar match={58} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('Avatar--unmatchScore_42')).toBe(true);
expect(avatarImage.hasClass('Avatar__image--unmatchScore_42')).toBe(true);
});

it('should add proper class names based on extra mods with boolean value', () => {
const avatarWrapper = shallow(<Avatar match={100} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('Avatar--isPerfect')).toBe(true);
expect(avatarImage.hasClass('Avatar__image--isPerfect')).toBe(true);
});

it("should not add a class name for boolean mod if mod's value is false (fpr extra mods)", () => {
const avatarWrapper = shallow(<Avatar match={99} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('Avatar--isPerfect')).toBe(false);
expect(avatarImage.hasClass('Avatar--isPerfect')).toBe(false);
});

it("should not add a class name for boolean mod if mod's value is false (for props)", () => {
it("should not add a class name for boolean mod if mod's value is false", () => {
const buttonWrapper = shallow(<Button size={3} active={false} />);
const buttonIcon = buttonWrapper.childAt(0);
const buttonLabel = buttonWrapper.childAt(1);
Expand Down Expand Up @@ -120,7 +92,18 @@ describe('BEM decorator', () => {
expect(buttonWrapper.hasClass('custom-class-name')).toBe(true);
});

it('should pass all props to the decorarted component', () => {
it('should respect className property and pass it to the decorated element also on children', () => {
const listWrapper = shallow(
<List>
<li className="custom-class-name" />
</List>
);
const liElement = listWrapper.find('li');
expect(liElement.hasClass('List__item')).toBe(true);
expect(liElement.hasClass('custom-class-name')).toBe(true);
});

it('should pass all props to the decorated component', () => {
const ButtonInstance = (
<ButtonStateless someCustom="property" someOtherCustom="thing" />
);
Expand Down Expand Up @@ -153,35 +136,7 @@ describe('BEM decorator', () => {
expect(buttonLabel.hasClass('ButtonStateless__label--size')).toBe(false);
});

it('should add proper class names based on extra mods with string value', () => {
const avatarWrapper = shallow(<AvatarStateless match={80} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('AvatarStateless--outlineColor_green')).toBe(true);
expect(avatarImage.hasClass('AvatarStateless__image--outlineColor_green')).toBe(true);
});

it('should add proper class names based on extra mods with number value', () => {
const avatarWrapper = shallow(<AvatarStateless match={58} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('AvatarStateless--unmatchScore_42')).toBe(true);
expect(avatarImage.hasClass('AvatarStateless__image--unmatchScore_42')).toBe(true);
});

it('should add proper class names based on extra mods with boolean value', () => {
const avatarWrapper = shallow(<AvatarStateless match={100} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('AvatarStateless--isPerfect')).toBe(true);
expect(avatarImage.hasClass('AvatarStateless__image--isPerfect')).toBe(true);
});

it("should not add a class name for boolean mod if mod's value is false (for extra mods)", () => {
const avatarWrapper = shallow(<AvatarStateless match={99} />);
const avatarImage = avatarWrapper.childAt(0);
expect(avatarWrapper.hasClass('AvatarStateless--isPerfect')).toBe(false);
expect(avatarImage.hasClass('AvatarStateless__image--isPerfect')).toBe(false);
});

it("should not add a class name for boolean mod if mod's value is false (for props)", () => {
it("should not add a class name for boolean mod if mod's value is false", () => {
const buttonWrapper = shallow(<ButtonStateless size={3} active={false} />);
const buttonIcon = buttonWrapper.childAt(0);
const buttonLabel = buttonWrapper.childAt(1);
Expand All @@ -198,5 +153,15 @@ describe('BEM decorator', () => {
expect(buttonIcon.hasClass('ButtonStateless__icon--disabled')).toBe(false);
expect(buttonLabel.hasClass('ButtonStateless__label--disabled')).toBe(false);
});

it('should not add classnames if none is applicable', () => {
const wrapper = shallow(
<Unstyled>
<p>some text</p>
</Unstyled>
);
expect(wrapper.find('div').props().className).toBe(undefined);
expect(wrapper.find('p').props().className).toBe(undefined);
});
});
});

0 comments on commit eddd875

Please sign in to comment.