Storybook: 通过动作改变旋钮 [idea]

创建于 2018-07-09  ·  55评论  ·  资料来源: storybookjs/storybook

我刚刚开始使用故事书,到目前为止我很喜欢它。 我一直在努力解决的一件事是如何处理需要将状态包含在父级中的纯“无状态”组件。 例如,我有一个复选框,它需要一个checked道具。 单击复选框不会切换其状态,而是触发onChange ,并等待获取更新的checked属性。 似乎没有关于处理这些类型组件的最佳实践的任何文档,并且诸如https://github.com/storybooks/storybook/issues/197等问题中的建议是创建一个包装组件或添加另一个附加组件。 如果可以的话,我宁愿不制作组件包装器,因为我想让我的故事尽可能简单。

我必须处理的一个想法是将actions附加组件与knobs ,允许通过操作以编程方式切换旋钮。 正如我所说,我是故事书的新手,所以我不知道这是否可行,但我想至少提出建议。

这对我来说是实现故事的绊脚石,我想其他人也可能如此。 如果创建包装器组件真的是最好的选择,也许可以添加一些文档来澄清这一点,以及如何完成它?

knobs feature request todo

最有用的评论

我们几乎完成了 5.3(1 月初发布),并研究了 6.0(3 月下旬发布)的旋钮重写,这将解决一系列长期存在的旋钮问题。 我会看看我们是否可以解决这个问题! 谢谢你的耐心!

所有55条评论

在这个#3701 PR 中引入了某种类似的想法,这是有争议的(并且没有合并)。

我们可以再次展开讨论,并听取 API 建议 =)。

啊谢谢你,我没有看到那个公关。 感谢@aherriot把这些放在一起,看来我们有相同的想法。

在深入研究 API 之前,似乎需要讨论和商定基本概念。 PR 中的评论之一来自@Hypnosphi

我不喜欢它引入了多个事实来源(组件回调和 UI 旋钮)的事实

在我看来,这个想法不是引入不同的事实来源,而是允许为组件状态维护一个单一的事实来源——旋钮。 我见过的所有其他方法(包装组件、状态附加组件、重构)实际上都会引入另一个事实来源。 在我的复选框示例中,我无法为checked设置旋钮,并且还允许包装器组件提供checked道具。 我将旋钮控制面板视为组件的父级,但目前它无法从组件获得任何回调,这有点片面,而不是通常构建 React 应用程序的方式。

通过允许以编程方式控制旋钮,可以将故事与正在演示的组件隔离开来,使实际的状态管理机制不透明,就像复选框等简单的展示组件一样。 复选框本身并不关心它如何获取它的 props,它可能会连接到 redux,它的父级可能使用setState ,也许它使用来自 recompose 的withState ,或者 props 可能被控制故事书旋钮。

无论如何,就像我说的,我对此很陌生,所以我只是分享我的直觉想法。 如果我不在基地并且有一个普遍接受的“最佳实践”来处理这些类型的纯无状态组件,也许有人可以指出我可以遵循的一个很好的例子?

你好 :)

我只是想补充一点,我偶然发现了这一点,因为我的组件正在接收它们是否应该从 props 显示它们的移动布局(或不显示),我的目标是将视口更改绑定到我的旋钮,这会非常好;)

只是想在这里添加我的用例,作为@IanVS,即使是回调也会很好(所以如果我切换我的 isMobile 道具,我可以触发视口更改)

我听说有几个人对更新旋钮状态的方法感兴趣。 如果我们能就一个好的架构达成一致,我认为这对很多人来说都是有价值的。 特别是因为这是人们可以选择使用或不使用的额外功能,并且因为它不会对不想使用它的人产生负面影响。

@Hypnosphi ,您对之前的实现 WDYT 有异议吗?

在此处发表评论以保持此问题的开放性。 我仍然很好奇@Hypnosphi对之前在这里提出的@igor-dv 有什么

在我看来,这个想法不是引入不同的事实来源,而是允许为组件状态维护单一的事实来源——旋钮。 我见过的所有其他方法(包装组件、状态附加组件、重构)实际上都会引入另一个事实来源。 在我的复选框示例中,我无法使用选中的旋钮,并且还允许包装器组件提供选中的道具。 我将旋钮控制面板视为组件的父级,但目前它无法从组件获得任何回调,这有点片面,而不是通常构建 React 应用程序的方式。

对我来说听起来很合理。 让我们讨论一下 API

这将是一个很棒的功能。 我只是在这里使用一个非常基本的受控组件遇到了同样的需求,并且之前在其他组件中也遇到过。 谢谢你看它。

跟上当前的语法似乎有点挑战。 也许你可以使用类似的东西:

const {value: name, change: setName} = text('Name', 'Kent');

@brunoreis ,我担心更改旋钮的返回签名将是一个重大更改。 我的即兴建议是做这样的事情:

import {boolean, changeBoolean} from '@storybook/addon-knobs/react';

stories.add('custom checkbox', () => (
    <MyCheckbox
        checked={boolean('checked', false)}
        onChange={(isChecked) => changeBoolean('checked', isChecked)} />
));

onChange回调甚至可能允许柯里化,因此这也可以工作:

onChange={(isChecked) => changeBoolean('checked')(isChecked)}

// which of course simplifies down to
onChange={changeBoolean('checked')}

重要的部分是第一个参数必须与应该更改的旋钮的标签相同。 我相信这将允许用户选择加入此行为,而无需更改当前使用旋钮的任何方式。 (除非标签目前不需要是唯一的?我认为它们是......)

@IanVS ,很好。 我同意你关于不改变工作方式的看法。 我找不到这样做的方法,因为我没有考虑使用标签作为密钥。 那可能会奏效。 让我们看看@Hypnosphi的想法。

改变旋钮的返回签名将是一个突破性的变化

从技术上讲,这现在不是问题。 我们即将发布一个主要版本。 但允许一些向后兼容性确实会更好。

我喜欢支持支持的想法。

除非标签目前不需要是唯一的?

是的,它们是,实际上它们在不同类型中都是独一无二的。 所以应该不需要单独的change<Type>导出,只需change就足够了。 这基本上是在https://github.com/storybooks/storybook/pull/3701中所做的
我想我会重新打开那个公关,让@aherriot在你的帮助下完成它

我们可以这样:

const {value: name, change: setName} = text('Name', 'Kent');

无需更改text的返回类型。

Javascript 函数是对象,因此可以具有属性。
对象可以被解构。

screen shot 2018-09-19 at 00 08 35

@ndelangen text函数确实是一个对象,但它的返回值不是。 这在您的示例中不起作用:

const { foo, bar } = x() // note the parens

我们可以有这样的东西(这个名字是有争议的):

const {value: name, change: setName} = text.mutable('Name', 'Kent');

为什么这行不通?

const { foo, bar } = x() // note the parens

x 的返回是一个函数,它也可以具有属性。

screen shot 2018-09-23 at 14 12 28

我是在你原来的例子中谈论x = () => {}

如果我们让text返回一个函数,用户将需要更改他们的代码:

// Before
<Foo bar={text('Bar', '')}>

// After
<Foo bar={text('Bar', '')()}>
                         ^^ this

我懂了

@IanVS对此的建议怎么样?

有这个功能会很酷。

“破坏”怎么样,比如“反应钩子”?:

const [foo, setFoo] = useString('foo', 'default');

和@DimaRGB 说的一样
将能够保留现有语法并添加类似钩子的调用,例如:

.add('example', () => {
  const [selected, setSelected] = useBool(false);

  return <SomeComponent selected={selected} onSelected={setSelected} />
})

偶尔需要反应组件不应该更新他们自己的props ,而是告诉他们的父母他们需要改变。 有时,一个组件包含一些应该切换它的道具的元素(我遇到了这个问题,因为有时如果单击子元素,我需要打开自己的导航栏菜单)。

@IanVS我有完全相同的情况,我有一个仅通过父道具更新的切换组件,一旦故事书故事的用户切换它,UI 状态与旋钮面板中显示的内容不一致。 我确实通过使用@storybook/addons-knobs私有方法对其进行了破解 - 一个更简单的建议是提供官方 API 来执行以下操作:

import { manager } from '@storybook/addons-knob/dist/registerKnobs.js'

const { knobStore } = manager
// The name given to your knob - i.e:  `select("Checked", options, defaultValue)`
knobStore.store['Checked'].value = newValue
// Danger zone! _mayCallChannel() triggers a re-render of the _whole_ knobs form.
manager._mayCallChannel()

@erickwilder我尝试了您的方法,但似乎从未更新旋钮表单(即使它确实更新了提供给组件的道具)。

编辑:

从头开始; 我只是没有看到更新,因为我使用的是 options() 和复选框,这些复选框显然以“不受控制”的方式操作。 通过切换到多选并在回调中使用此方法,我确实将更新的值放入了旋钮表单中:

// Ditch when https://github.com/storybooks/storybook/issues/3855 gets resolved properly.
function FixMeKnobUpdate(name, value) {
    addons.getChannel().emit(CHANGE, { name, value });
}

我可能省略了我的设置的一些细节:

  • 我们在 Storybook 中使用 Vue.js
  • 改变值并触发重新渲染的代码是在故事包装组件的方法中完成的。

我不知道这种方法是否适用于每个人。

我完全同意@IanVS ,我喜欢故事书,但我真的很想念通过动作更新旋钮的能力。 就我而言,我有两个单独的组件 A 和 B,但在我的一个故事中,我想展示使用它们的组合,以便每当我更改 AI 时都会发出一个可以修改 B 的动作,反之亦然。

我用我的设置(Angular 7)尝试了@erickwilder的 hack,但是每当我尝试更新旋钮存储时,我都会收到以下错误:

错误:预计不在 Angular Zone 中,但确实如此!

我试图以某种方式在 Angular 之外运行它,但我无法做到……最坏的情况是,我将创建第三个组件 C,它将是 A 和 B 的包装器。

来自@erickwilder 的@davidolivefarga临时解决方案应该适用于任何框架,因为它与框架/库(react、angular、vue 等)无关,而是与 Knobs 插件的重新渲染有关。

我们工作使用做出同样的反应所提到的上述

+1 添加公开可见的触发方法,包含旋钮名称作为要更新的字符串和新值,例如:

import { manager } from "@storybook/addon-knobs"

manager.updateKnob(
  propName, // knobs property, example from above "Checked"
  newValue, // new value to set programmatically, ex. true
)

我真的很希望得到官方支持。

与此同时,在 Storybook v5 中,我不得不采用这个非常糟糕的解决方案:

window.__STORYBOOK_ADDONS.channel.emit('storybookjs/knobs/change', {
  name: 'The name of my knob',
  value: 'the_new_value'
})

🙈

🙈

相关:#6916

这会很酷...尤其是因为模态。

我认为这会很有用。

例子:

storiesOf('input', module)
  .addDecorator(withKnobs)
  .add('default', () => <input type={text('type', 'text')} value={text('value', '')} disabled={boolean('disabled', false)} placeholder={text('placeholder', '')} onChange={action('onChange')} />)

这目前有效,但我们希望将 onChange 事件目标值连接到测试元素的 props 中设置的值似乎很自然。 这将更好地展示元素的应用。

这会很有用

+1 @mathieuk/ @raspo指出这里的解决方案。

我们也可以通过另一种方式访问​​它。 在helpers模块中创建一些通用方法?

import addons from "@storybook/addons";

export const emitter = (type, options) => addons.getChannel().emit(type, options);

export const updateKnob = (name, value) => (
  emitter("storybookjs/knobs/change", { name, value })
);

并在故事中根据需要调用...

import { text } from "@storybook/addon-knobs";
import { updateKnob } from 'helpers';

// ...
const value = text("value", "Initial value");
<select
  value={value}
  onChange={({ target }) => updateKnob("value", target.value)}
>

...但仍然感觉像一些时髦的双向绑定解决方法可以内置到旋钮 API 中

以上有更新吗? 将是一个非常有用的功能👍

正在实施的此功能将使我们能够非常轻松地完成一些非常强大的交叉依赖旋钮。 想象一下,如果您可以让一个旋钮根据另一个旋钮动态更新,那么制作一个分页组件故事,在您的故事中是这样的:

const resultsCount = number(
    'Results count',
    currentPage, {
        max: 100,
        min: 0,
        range: true
    }
);
const resultsArray: React.ReactNode[] = new Array(resultsCount)
    .fill(true)
    .map((_, idx) => <div key={idx + 1}>Test Pagination Result #{idx + 1}</div>);
const childrenPerPage = 10;
const currentPage = number(
    'Current page index',
    0, {
        max: Math.ceil(resultsCount / childrenPerPage) - 1,
        min: 0,
        range: true
    }
);

我基本上尝试这样做,希望currentPage旋钮上的最大范围在以 10 为增量增加resultsCount旋钮时会动态更新,但是似乎每个旋钮的初始值在创建时缓存,不会用于覆盖后续渲染中的内部状态值。 使用上面的代码,当我将resultsCount增加 10+ 时,我预计maxcurrentPage也会增加 1,但是尽管潜在值是用于计算它已更改(记录Math.ceil(resultsCount / childrenPerPage) - 1显示预期的新值)。

我们几乎完成了 5.3(1 月初发布),并研究了 6.0(3 月下旬发布)的旋钮重写,这将解决一系列长期存在的旋钮问题。 我会看看我们是否可以解决这个问题! 谢谢你的耐心!

@shilman我对这个功能很兴奋😄

我, @ atanasster@PlayMa256已经为此工作了一段时间。 需要更多的迭代才能正确,但我非常有信心我们可以在 6.0.0 中达到 100%,并真正彻底改变故事书的数据和反应性。

革命? 热死的数字。 别逗我了,我的身体已经准备好了。

+1 模态 :)

不敢相信从一开始就不可能。 等待更新...

嗨伙计!
作为我在我的应用程序下一个代码中使用的临时解决方案:

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

仍在等待此功能将在本地实现。

我使用 observable 解决了这个问题。 我正在使用带有角度的故事书

`
.add('更新图表数据', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

@norbert-doofus 谢谢你的例子帮助了我👍

大家好,我们刚刚在 6.0-beta 版中发布了插件控件!

控件是便携式的、自动生成的旋钮,旨在长期取代附加旋钮。

请立即升级并试用它们。 感谢您的帮助和支持,让这个稳定版本发布!

那太棒了! 我无法通过阅读自述文件(我可能错过了)来判断,这些新的controls可以以编程方式更改,这是在此问题中提出的请求?

它们可以,尽管我不确定 API 是否得到官方支持(虽然我们正在为插件文档中的控件做同样的事情)。 我将与@tmeasday 合作,找出在第一轮 Controls 错误稳定后获得它的最佳方法。

  • [ ] 将updateArgs到故事上下文中?
  • [ ] 使故事上下文可用于从故事中调用的回调? ( this.context?.updateArgs(....) )

对于任何对 Controls 感兴趣但不知道从哪里开始的人,我创建了一个快速而肮脏的分步演练,从一个新的 CRA 项目到一个工作演示。 一探究竟:

=>带有 CRA 和 TypeScript 的故事书控件

Controls README 中还有一些“控制旋钮”迁移文档:

=>我如何从插件旋钮迁移?

嗨伙计!
作为我在我的应用程序下一个代码中使用的临时解决方案:

import { addons } from '@storybook/addons';
import { CHANGE } from '@storybook/addon-knobs';

const channel = addons.getChannel();

channel.emit(CHANGE, {
  name: 'prop_name',
  value: prop_value,
});

仍在等待此功能将在本地实现。

请记住,在使用groupId您需要将组 ID 添加到name ,如下所示:

 const show = boolean('Show Something', true, 'Group')

channel.emit(CHANGE, {
  name: 'Show Something_Group',
  value: prop_value,
});

此外, channel是一个 EventEmitter,因此您可以通过addListener来检查接收到的参数是什么。

channel.addListener(CHANGE, console.log)

这是一个代码片段,适用于对如何在 v6 中使用addon-controls执行此操作感兴趣的任何人。

import { useArgs } from '@storybook/client-api';

// Inside a story
export const Basic = ({ label, counter }) => {
    const [args, updateArgs] = useArgs();
    return <Button onClick={() => updateArgs({ counter: counter+1 })>{label}: {counter}<Button>;
}

我不知道这是否是用于此目的的最佳 API,但它应该可以工作。 monorepo 中的示例:

https://github.com/storybookjs/storybook/blob/next/examples/official-storybook/stories/core/args.stories.js#L34 -L43

谢谢,@shilman! 这就是诀窍。

对于感兴趣的人,我们碰巧有确切的Checkbox故事checked开始整个线程。 这是我们使用 Storybook 6.0.0-rc.9 编写的新故事:

export const checkbox = (args) => {
  const [{ checked }, updateArgs] = useArgs();
  const toggleChecked = () => updateArgs({ checked: !checked });
  return <Checkbox {...args} onChange={toggleChecked} />;
};
checkbox.args = {
  checked: false,
  label: 'hello checkbox!',
};
checkbox.argTypes = {
  checked: { control: 'boolean' },
};

cb-arg

@shilman我试图在故事中使用useArgs进行受控文本输入(在这种情况下,我们通常可能使用useState钩子通过以下方式更新组件的value prop它的onChange事件)。 但是,我遇到了一个问题,每次用户键入字符时,组件都会失去焦点。 这可能是由于每次我们更新 args 时它都会刷新/重新渲染故事吗?

对于具有受控文本输入的组件,是否有不同的推荐方法来使用参数/控件?

这是 6.0.0-rc.13

@jcquseArgs的主要用例,但肯定是我们想要支持的用例,所以我们很乐意深入研究它。

@shilman没有问题 - 这是新问题:
https://github.com/storybookjs/storybook/issues/11657

我还应该澄清错误的行为显示在文档中,而它在正常的画布模式下正常工作。

我使用 observable 解决了这个问题。 我正在使用带有角度的故事书

`
.add('更新图表数据', () => {

const myObservable= new BehaviorSubject([{a: 'a', b: 'b'}]);
return {
  template: '
  <my-component
    myInput: myData,
    (myEvent)="myEventProp($event)"
  ></my-component>
  ',
  props: {
    myData: myObservable,
    myEventProp: $event => {
      myObservable.next([]);
      action('(myEvent)')($event);
    }
  }
};

})
`

这对我使用 Angular 有用,但在第 5 行更改为myData.value

我还没有试过这个(现在停留在旧版本的故事书上)但现在这个问题似乎可以关闭。 感谢您在参数/控件方面所做的出色工作!

此页面是否有帮助?
0 / 5 - 0 等级