Skip to content

Commit

Permalink
fix failing test; remove (unused) extraMods options from elem
Browse files Browse the repository at this point in the history
  • Loading branch information
Eszter Hofmann committed Aug 16, 2019
1 parent 78a9f2a commit 2d58ea9
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 79 deletions.
122 changes: 81 additions & 41 deletions src/packages/bem/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
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 */
.ComponentName--modName {}
/* component's root node class name with string/number modifyer applied */
.ComponentName--modName_modValue {}
.ComponentName {
}
/* component's root node class name with boolean modifier applied */
.ComponentName--modName {
}
/* 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 */
.ComponentName__elem--modName {}
/* component's root node class name with string/number modifyer + value applied */
.ComponentName__elem--modName_modValue {}
.ComponentName__elem {
}
/* component's root node class name with boolean modifier + value applied */
.ComponentName__elem--modName {
}
/* 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 +70,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 +117,7 @@ export default bem(classnamesMap)(Button)
### Stateless component

ButtonStateless.js

```js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
Expand All @@ -133,7 +142,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 Down Expand Up @@ -165,11 +174,10 @@ export default ButtonStateless;
```

Button.scss
```css

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

display: inline-block;

/*
Expand Down Expand Up @@ -228,8 +236,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,17 +246,45 @@ Button.scss
}
```

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

If you wish to enrich an existing HTML 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 />
↓ ↓ ↓
<button />
↓ ↓ ↓
<button class="Button">
<span class="Button__label" />
</button>
Expand All @@ -258,9 +293,9 @@ respectively exists in classnames map.
### Prop `active` is set:

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

↓ ↓ ↓
↓ ↓ ↓

<button class="Button Button--active">
<span class="Button__label Button__label--active" />
Expand All @@ -269,36 +304,41 @@ respectively exists in classnames map.

### 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 active="{false}" />

↓ ↓ ↓
↓ ↓ ↓

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

### Clicked state

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

↓ ↓ ↓
↓ ↓ ↓

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

describe('BEM decorator', () => {
describe('Decorate stateFUL class component', () => {
Expand Down Expand Up @@ -120,7 +127,7 @@ describe('BEM decorator', () => {
expect(buttonWrapper.hasClass('custom-class-name')).toBe(true);
});

it.skip('should respect className property and pass it to the decorated element also on children', () => {
it('should respect className property and pass it to the decorated element also on children', () => {
const listWrapper = shallow(
<List>
<li className="custom-class-name" />
Expand All @@ -131,7 +138,7 @@ describe('BEM decorator', () => {
expect(liElement.hasClass('custom-class-name')).toBe(true);
});

it('should pass all props to the decorarted component', () => {
it('should pass all props to the decorated component', () => {
const ButtonInstance = (
<ButtonStateless someCustom="property" someOtherCustom="thing" />
);
Expand Down Expand Up @@ -166,23 +173,17 @@ describe('BEM decorator', () => {

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)", () => {
Expand All @@ -209,5 +210,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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,18 @@ const { block, elem } = bem({
propsToMods: [],
});

const calculateOutlineColor = (match) => {
const calculateOutlineColor = match => {
if (match > 77) return 'green';
if (match > 33) return 'yellow';
if (match >= 0) return 'red';
return 'red';
}

const calculateIsPerfect = (match) => match === 100;
};

const calculateUnmatchScore = (match) => 100 - match;
const calculateIsPerfect = match => match === 100;

const AvatarStateless = (props) => {
const calculateUnmatchScore = match => 100 - match;

const AvatarStateless = props => {
const { match, children } = props;
const outlineColor = calculateOutlineColor(match);
const isPerfect = calculateIsPerfect(match);
Expand All @@ -34,22 +33,19 @@ const AvatarStateless = (props) => {

return (
<div {...block(props, extraMods)}>
<div {...elem('image', props, extraMods)}>
{children}
</div>
<div {...elem('image', props)}>{children}</div>
</div>
);

};

AvatarStateless.displayName = 'AvatarStateless';

AvatarStateless.propTypes = {
match: PropTypes.number,
}
};

AvatarStateless.defaultProps = {
match: 0,
}
};

export default AvatarStateless;
17 changes: 10 additions & 7 deletions src/packages/bem/__tests__/dummy-components/List/List.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ const { block, elem } = bem({
classnames: styles,
});

const List = props => (
<ul {...block(props)}>
{React.Children.map(props.children, child =>
child ? React.cloneElement(child, elem('item', props)) : null
)}
</ul>
);
const List = props => {
const { children } = props;
return (
<ul {...block(props)}>
{React.Children.map(children, child =>
child ? React.cloneElement(child, elem('item', props, child.props.className)) : null
)}
</ul>
);
};

List.displayName = 'List';

Expand Down

0 comments on commit 2d58ea9

Please sign in to comment.