Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Knobs addon: new knob type button #2004

Merged
merged 13 commits into from
Oct 18, 2017
Merged
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way this is used, is quite different from how the other knobs are used. This confuses me, and I'd assume other as well. Having a fully 'working' real-world example would help a lot understanding this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the documentation as of today, I don't get nothin' 😆. How is this supposed to be used ? 🤔

```

### 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));
});
});
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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be in the constructor?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very clear example by the way, Thanks!

Copy link
Contributor Author

@derrickpelletier derrickpelletier Oct 17, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HMR kills it when it is in the constructor. The button stays present in the knob panel but the instance of the example component is new, so the actual component tied to the knob was unmounted. I think there'd need to be modifications to the knobManager to override existing knobs of this type, instead of just returning the existing instance

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, if this is the best solution, that's fine.

Can you take a look at why the CI is disapproving of this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I restarted the build tasks

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