Skip to content

Commit

Permalink
Merge pull request #2004 from derrickpelletier/issue#1914-button-knob
Browse files Browse the repository at this point in the history
Issue#1914 button knob
  • Loading branch information
ndelangen authored Oct 18, 2017
2 parents 5457fdb + 58e0860 commit b0fabc3
Show file tree
Hide file tree
Showing 13 changed files with 132 additions and 5 deletions.
12 changes: 12 additions & 0 deletions addons/knobs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,18 @@ const value = date(label, defaultValue);

> Note: the default value must not change - e.g., do not do `date('Label', new Date())` or `date('Label')`
### button

Allows you to include a button and associated handler.

```js
import { button } from '@storybook/addon-knobs';

const label = 'Do Something';
const handler = () => doSomething('foobar');
button(label, handler);
```

### withKnobs vs withKnobsOptions

If you feel like this addon is not performing well enough there is an option to use `withKnobsOptions` instead of `withKnobs`.
Expand Down
11 changes: 10 additions & 1 deletion addons/knobs/src/components/Panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default class Panel extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleClick = this.handleClick.bind(this);
this.setKnobs = this.setKnobs.bind(this);
this.reset = this.reset.bind(this);
this.setOptions = this.setOptions.bind(this);
Expand Down Expand Up @@ -133,6 +134,10 @@ export default class Panel extends React.Component {
this.setState({ knobs: newKnobs }, this.emitChange(changedKnob));
}

handleClick(knob) {
this.props.channel.emit('addon:knobs:knobClick', knob);
}

render() {
const { knobs } = this.state;
const knobsArray = Object.keys(knobs)
Expand All @@ -146,7 +151,11 @@ export default class Panel extends React.Component {
return (
<div style={styles.panelWrapper}>
<div style={styles.panel}>
<PropForm knobs={knobsArray} onFieldChange={this.handleChange} />
<PropForm
knobs={knobsArray}
onFieldChange={this.handleChange}
onFieldClick={this.handleClick}
/>
</div>
<button style={styles.resetButton} onClick={this.reset}>
RESET
Expand Down
7 changes: 4 additions & 3 deletions addons/knobs/src/components/PropField.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ export default class PropField extends React.Component {
}

render() {
const { onChange, knob } = this.props;
const { onChange, onClick, knob } = this.props;

const InputType = TypeMap[knob.type] || InvalidType;

return (
<div style={stylesheet.field}>
<label htmlFor={knob.name} style={stylesheet.label}>
{`${knob.name}`}
{!knob.hideLabel && `${knob.name}`}
</label>
<InputType knob={knob} onChange={onChange} />
<InputType knob={knob} onChange={onChange} onClick={onClick} />
</div>
);
}
Expand All @@ -67,4 +67,5 @@ PropField.propTypes = {
value: PropTypes.any,
}).isRequired,
onChange: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
};
2 changes: 2 additions & 0 deletions addons/knobs/src/components/PropForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export default class propForm extends React.Component {
value={knob.value}
knob={knob}
onChange={changeHandler}
onClick={this.props.onFieldClick}
/>
);
})}
Expand All @@ -68,4 +69,5 @@ propForm.propTypes = {
})
),
onFieldChange: PropTypes.func.isRequired,
onFieldClick: PropTypes.func.isRequired,
};
41 changes: 41 additions & 0 deletions addons/knobs/src/components/types/Button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import PropTypes from 'prop-types';
import React from 'react';

const styles = {
height: '26px',
};

class ButtonType extends React.Component {
render() {
const { knob, onClick } = this.props;
return (
<button
type="button"
id={knob.name}
ref={c => {
this.input = c;
}}
style={styles}
onClick={() => onClick(knob)}
>
{knob.name}
</button>
);
}
}

ButtonType.defaultProps = {
knob: {},
};

ButtonType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
}),
onClick: PropTypes.func.isRequired,
};

ButtonType.serialize = value => value;
ButtonType.deserialize = value => value;

export default ButtonType;
2 changes: 2 additions & 0 deletions addons/knobs/src/components/types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ObjectType from './Object';
import SelectType from './Select';
import ArrayType from './Array';
import DateType from './Date';
import ButtonType from './Button';

export default {
text: TextType,
Expand All @@ -16,4 +17,5 @@ export default {
select: SelectType,
array: ArrayType,
date: DateType,
button: ButtonType,
};
4 changes: 4 additions & 0 deletions addons/knobs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export function date(name, value = new Date()) {
return manager.knob(name, { type: 'date', value: proxyValue });
}

export function button(name, callback) {
return manager.knob(name, { type: 'button', callback, hideLabel: true });
}

// "Higher order component" / wrapper style API
// In 3.3, this will become `withKnobs`, once our decorator API supports it.
// See https://github.com/storybooks/storybook/pull/1527
Expand Down
10 changes: 10 additions & 0 deletions addons/knobs/src/react/WrapStory.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default class WrapStory extends React.Component {
constructor(props) {
super(props);
this.knobChanged = this.knobChanged.bind(this);
this.knobClicked = this.knobClicked.bind(this);
this.resetKnobs = this.resetKnobs.bind(this);
this.setPaneKnobs = this.setPaneKnobs.bind(this);
this._knobsAreReset = false;
Expand All @@ -16,6 +17,8 @@ export default class WrapStory extends React.Component {
componentDidMount() {
// Watch for changes in knob editor.
this.props.channel.on('addon:knobs:knobChange', this.knobChanged);
// Watch for clicks in knob editor.
this.props.channel.on('addon:knobs:knobClick', this.knobClicked);
// Watch for the reset event and reset knobs.
this.props.channel.on('addon:knobs:reset', this.resetKnobs);
// Watch for any change in the knobStore and set the panel again for those
Expand All @@ -31,6 +34,7 @@ export default class WrapStory extends React.Component {

componentWillUnmount() {
this.props.channel.removeListener('addon:knobs:knobChange', this.knobChanged);
this.props.channel.removeListener('addon:knobs:knobClick', this.knobClicked);
this.props.channel.removeListener('addon:knobs:reset', this.resetKnobs);
this.props.knobStore.unsubscribe(this.setPaneKnobs);
}
Expand All @@ -45,11 +49,17 @@ export default class WrapStory extends React.Component {
const { knobStore, storyFn, context } = this.props;
// Update the related knob and it's value.
const knobOptions = knobStore.get(name);

knobOptions.value = value;
knobStore.markAllUnused();
this.setState({ storyContent: storyFn(context) });
}

knobClicked(knob) {
const knobOptions = this.props.knobStore.get(knob.name);
knobOptions.callback();
}

resetKnobs() {
const { knobStore, storyFn, context } = this.props;
knobStore.reset();
Expand Down
8 changes: 8 additions & 0 deletions addons/knobs/src/vue/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({
const { name, value } = change;
// Update the related knob and it's value.
const knobOptions = knobStore.get(name);

knobOptions.value = value;
this.$forceUpdate();
},

onKnobClick(knob) {
const knobOptions = knobStore.get(knob.name);
knobOptions.callback();
},

onKnobReset() {
knobStore.reset();
this.setPaneKnobs(false);
Expand All @@ -26,12 +32,14 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({
created() {
channel.on('addon:knobs:reset', this.onKnobReset);
channel.on('addon:knobs:knobChange', this.onKnobChange);
channel.on('addon:knobs:knobClick', this.onKnobClick);
knobStore.subscribe(this.setPaneKnobs);
},

beforeDestroy() {
channel.removeListener('addon:knobs:reset', this.onKnobReset);
channel.removeListener('addon:knobs:knobChange', this.onKnobChange);
channel.removeListener('addon:knobs:knobClick', this.onKnobClick);
knobStore.unsubscribe(this.setPaneKnobs);
},
});
3 changes: 2 additions & 1 deletion addons/knobs/src/vue/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ describe('Vue handler', () => {
const testStore = new KnobStore();
new Vue(vueHandler(testChannel, testStore)(testStory)(testContext)).$mount();

expect(testChannel.on).toHaveBeenCalledTimes(2);
expect(testChannel.on).toHaveBeenCalledTimes(3);
expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:reset', expect.any(Function));
expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:knobChange', expect.any(Function));
expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:knobClick', expect.any(Function));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,13 @@ exports[`Storyshots Button with knobs 1`] = `
<p>
Nice to meet you!
</p>
<hr />
<p>
PS. My shirt pocket contains:
</p>
<li>
No items!
</li>
</div>
`;

Expand Down
27 changes: 27 additions & 0 deletions examples/cra-kitchen-sink/src/stories/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import EventEmiter from 'eventemitter3';

import { storiesOf } from '@storybook/react';
Expand All @@ -16,6 +17,7 @@ import {
select,
array,
date,
button,
object,
} from '@storybook/addon-knobs';
import centered from '@storybook/addon-centered';
Expand Down Expand Up @@ -59,6 +61,23 @@ const InfoButton = () => (
</span>
);

class AsyncItemLoader extends React.Component {
constructor() {
super();
this.state = { items: [] };
}

loadItems() {
setTimeout(() => this.setState({ items: ['pencil', 'pen', 'eraser'] }), 1500);
}

render() {
button('Load the items', () => this.loadItems());
return this.props.children(this.state.items);
}
}
AsyncItemLoader.propTypes = { children: PropTypes.func.isRequired };

storiesOf('Button', module)
.addDecorator(withKnobs)
.add('with text', () => (
Expand Down Expand Up @@ -118,6 +137,14 @@ storiesOf('Button', module)
<p>In my backpack, I have:</p>
<ul>{items.map(item => <li key={item}>{item}</li>)}</ul>
<p>{salutation}</p>
<hr />
<p>PS. My shirt pocket contains: </p>
<AsyncItemLoader>
{loadedItems => {
if (!loadedItems.length) return <li>No items!</li>;
return <ul>{loadedItems.map(i => <li key={i}>{i}</li>)}</ul>;
}}
</AsyncItemLoader>
</div>
);
})
Expand Down
3 changes: 3 additions & 0 deletions examples/vue-kitchen-sink/src/stories/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
select,
color,
date,
button,
} from '@storybook/addon-knobs';
import Centered from '@storybook/addon-centered';

Expand Down Expand Up @@ -231,6 +232,8 @@ storiesOf('Addon Knobs', module)
: `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`;
const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';

button('Arbitrary action', action('You clicked it!'));

return {
template: `
<div style="border:2px dotted ${colour}; padding: 8px 22px; border-radius: 8px">
Expand Down

0 comments on commit b0fabc3

Please sign in to comment.